Introduction
This is a special toolbar helper template class that you can use to achieve the following in your WTL frame window based application:
- Add text to toolbar buttons
- Add drop-down menus to toolbar buttons
- Add combo boxes to toolbars
The code presented here makes it very easy for any WTL CFrameWnd
-derived window to display professional looking toolbars, and I hope that the explanation on how it works also proves useful.
Getting Started
First, you need to include the header file, preferably in your MainFrm.h:
#include "ToolBarHelper.h"
Next, derive your window class (probably CMainFrame
) from CToolBarHelper
:
class CMainFrame :
...
public CToolBarHelper<CMainFrame>
Next, if you are going to want toolbar drop-down menus/comboboxes, you need to add a CHAIN_MSG_MAP
entry to your message map, to ensure that the CBN_SELCHANGE
and TBN_DROPDOWN
messages are processed correctly:
BEGIN_MSG_MAP(CMainFrame)
...
CHAIN_MSG_MAP(CToolBarHelper<CMainFrame>)
END_MSG_MAP()
It is probably a good idea to do this anyway, in case you want to add drop-downs/comboboxes later.
CString, etc.
Note that the code makes use of the CString
, CRect
, and CSize
classes. This means that if you are compiling under Visual Studio 6, you will need to #include <atlmisc.h>
, whereas Visual Studio 2005 users may want to use the new atlstr.h
and atltypes.h headers. The atlctrls.h file is also essential, but the WTL ClassWizard adds this to stdafx.h by default.
Next, add the relevant magic to your CMainFrame::OnCreate
function to spice up your toolbar.
Adding Text to Toolbar Buttons
To add text to toolbar buttons, you first need to ensure that your toolbar is created using ATL_SIMPLE_TOOLBAR_PANE_STYLE_EX
instead of ATL_SIMPLE_TOOLBAR_PANE_STYLE
. If you forget this step, then the text will not appear on the toolbar buttons. Simply replace this line:
HWND hWndToolBar = CreateSimpleToolBarCtrl(m_hWnd, IDR_MAINFRAME,
FALSE, ATL_SIMPLE_TOOLBAR_PANE_STYLE);
with this:
HWND hWndToolBar = CreateSimpleToolBarCtrl(m_hWnd, IDR_MAINFRAME, FALSE,
ATL_SIMPLE_TOOLBAR_PANE_STYLE_EX);
Note that ATL_SIMPLE_TOOLBAR_PANE_STYLE_EX
is simply a combination of ATL_SIMPLE_TOOLBAR_PANE_STYLE
and the magic toolbar TBSTYLE_LIST
style (which ensures text on buttons will display).
Now, all you need to do is add text to the relevant buttons, using one of the following methods:
AddToolbarButtonText(HWND hWndToolBar, UINT nID, LPCTSTR lpsz)
Use this method to set the text of a toolbar button directly from a string, for example:
AddToolbarButtonText(hWndToolBar, ID_APP_ABOUT, _T("About"));
Simply pass the toolbar window handle (the one returned by CreateSimpleToolBarCtrl
), the ID of the button you wish to change, and the text you wish to add.
AddToolbarButtonText(HWND hWndToolBar, UINT nID, UINT nStringID)
Use this method to add the text from a specified string resource. For example:
AddToolbarButtonText(hWndToolBar, ID_EDIT_PASTE, IDS_TOOLBAR_TEXT);
This will load the IDS_TOOLBAR_TEXT
string and assign the text to the specified toolbar button (in this case, ID_EDIT_PASTE
).
Finally, you can set a toolbar button to the tooltip text already assigned to a button, using the following method:
AddToolbarButtonText(HWND hWndToolBar, UINT nID)
This method will attempt to load a string with the same ID as the button, look for the tooltip text, and assign it to the button. For example:
AddToolbarButtonText(hWndToolBar, ID_FILE_SAVE);
This will load the string with the ID ID_FILE_SAVE
, which looks like this:
Save the active document\nSave (Ctrl+S)
The tooltip text can be found immediately after the \n. Now, as you can see, the accelerator key has been hard-wired into the tooltip text, so AddToolbarButtonText
will look for the first open bracket following the \n and only use the text in between. Hence, in this example, the toolbar button with an ID of ID_FILE_SAVE
will be set to the text Save.
Simple enough.
Under the Hood
Adding text to a toolbar button requires the following steps:
- Make sure the toolbar has the
TBSTYLE_EX_MIXEDBUTTONS
style bit set.
- Call the
CToolBarCtrl::AddStrings
method to add the button text to the toolbar's internal list.
- Change the button info to ensure the
TBSTYLE_AUTOSIZE
and BTNS_SHOWTEXT
bits are set.
- Delete and then re-insert the toolbar button.
Add Drop-Down Menus to Toolbar Buttons
In order to add a drop-down menu to a toolbar button, first you need to create the menu using the Visual Studio resource editor. Then, simply add a call to the the following method to CMainFrame::OnCreate
:
AddToolBarDropDownMenu(HWND hWndToolBar, UINT nButtonID, UINT nMenuID)
Specify the toolbar window handle (again, the one returned by CreateSimpleToolBarCtrl
), the ID of the button which will be assigned the drop-down button, and the ID of the menu you want to display when the button is clicked. Easy. The menu commands will be routed to the main frame window as is normal, and if you have button images assigned, they will appear (see the sample program for a demonstration of this).
If you want to modify the menu immediately before it is displayed (for example, to add new items), then override the following virtual function:
virtual void PrepareToolBarMenu(UINT nMenuID, HMENU hMenu);
This function is called by CToolBarHelper
prior to displaying a menu, and is passed the ID of the menu that is about to be displayed, and the menu handle, which will allow you to modify the menu contents.
For example, the sample program demonstrates adding a separator and two new menu items to the IDR_NEW
menu that is attached to the File|New toolbar button:
void CMainFrame::PrepareToolBarMenu(UINT nMenuID, HMENU hMenu)
{
if (nMenuID == IDR_NEW)
{
CMenuHandle menu(hMenu);
menu.AppendMenu(MF_SEPARATOR);
menu.AppendMenu(MF_STRING, ID_NEW_DOCUMENT, _T("Document..."));
menu.AppendMenu(MF_STRING, ID_NEW_TEMPLATE, _T("Template..."));
}
}
Pretty simple stuff.
Under the Hood
Adding a drop-down menu to a toolbar button involves the following steps:
- Make sure the button itself has the
TBSTYLE_EX_DRAWDDARROWS
style assigned.
- Handle the
TBN_DROPDOWN
notification message.
- Display the menu using
CMainFrame::m_CmdBar.TrackPopupMenu
to ensure the menu displays with cool little buttons next to each item, if assigned.
Note that a CSimpleMap
is used to map toolbar button IDs with menu IDs. This very simple map class has been part of ATL for many, many years, and shouldn't give you any problems. I am a big user of the STL, so would normally use std::map
, but for this very simple purpose, CSimpleMap
is perfectly adequate.
Add Comboboxes to the Toolbar
To add a combobox to a toolbar, first you need to add a button to your toolbar that acts as a placeholder. Simply add a new button using the Visual Studio toolbar editor, and make sure it has an ID - no image is required. Next, to create the combobox, call the following method from CMainFrame::OnCreate
:
CreateToolbarComboBox(HWND hWndToolBar, UINT nID, UINT nWidth, UINT nHeight, DWORD dwComboStyle)
The minimum parameters you need to supply are the toolbar window handle (again, the one returned by CreateSimpleToolBarCtrl
) and the ID of the special placeholder button you added to your toolbar earlier. The width, height, and style of the combobox will be defaulted to 16, 16
and CBS_DROPDOWNLIST
, respectively (note the width/height are specified in characters instead of pixels, and should only be used as a guide, as different flavours of Windows/visual styles can result in comboboxes that determine their own height).
The CreateToolbarComboBox
returns the HWND
of the newly created combobox which you can then use to add items. For example, to add a combobox that contains three items:
CComboBox combo = CreateToolbarComboBox(hWndToolBar, ID_COMBO_PLACEHOLDER);
combo.AddString(_T("Item 1"));
combo.AddString(_T("Item 2"));
combo.AddString(_T("Item 3"));
When a combobox item is selected, CToolBarHelper
will call the following function that you must add to your CMainFrame
class:
void OnToolBarCombo(HWND hWndCombo, UINT nID, int nSel,
LPCTSTR lpszText, DWORD dwItemData);
This is passed the combobox window handle, the ID of the button (for example, ID_COMBO_PLACEHOLDER
), the index of the newly selected combobox item, the item text, and the item data. The sample program shows this in action.
Combobox UI Updates
If you want a toolbar combobox that may need the item selection to be updated based on some program state, then you should do the following:
- Add a
CComboBox
member to your CMainFrame
class.
- Assign the
CComboBox
the HWND
returned by a call to CreateToolbarComboBox
.
- In your
CMainFrame::OnIdle
method, change the combobox selection accordingly.
For example, in your MainFrm.h:
CComboBox m_wndCombo;
Then, in CMainFrame::OnCreate
:
m_wndCombo = CreateToolbarComboBox(hWndToolBar, ID_COMBO_PLACEHOLDER);
m_wndCombo.AddString(...);
And in CMainFrame::OnIdle
:
if (GetFocus() != m_wndCombo)
{
}
Note that the OnIdle
code for updating the combobox first checks that the combobox doesn't have the focus, which avoids some potentially nasty flickering.
The sample program demonstrates this in action.
Under the Hood
Adding controls to a toolbar has been something people have wanted to do since the first common toolbar control, back when Win95 was released, and the standard practice for this works something like this:
- Change the placeholder button to be a separator, as thanks to a design quirk, the width of a separator can be changed.
- Change the width of the separator to match that of the control you want to create.
- Create the control using the separator button rect as the size, with the toolbar as the control's parent.
This has worked fine for various flavour framework apps for many years, and is the method I chose when writing this article. However, my first attempt ended up looking like this:
The problem is that if you are using Windows XP visual styles, a toolbar separator is no longer an empty space as it is in Classic mode or when running Win9x/Win2000 - instead, a vertical line is drawn. As you can see from the image above, this looks a bit odd. At first, I thought this was going to be a nightmare to solve, with a nasty and complicated NM_CUSTOMDRAW
hack. The reason people change the placeholder button to be a separator is because they can be an arbitrary width, whereas a normal button's width is fixed ... or is it? Of course not - a button with text on it can obviously change width to match the text, which provided me with the following solution:
- Change the style of the placeholder button to
BTNS_SHOWTEXT
.
- Make sure the button state is disabled. If not, then hovering the mouse below the combobox would cause a phantom button to be drawn.
Another minor issue is that the combobox height may be quite a lot smaller than the toolbar itself (especially if like me, you are running with Large Fonts enabled), so I have also added some code to centre it.
One final point is the font used for the comboboxes. At first, I just used the system GUI stock font, but this is slightly different to the font used to display toolbar button text. After some digging around, it would appear that the menu font is used, and so I use a combination of SystemParametersInfo
and SPI_GETNONCLIENTMETRICS
to get the LOGFONT
, so I can create a copy for use with comboboxes.
ToolBarHelper.h
Here is the CToolBarHelper
in full:
#pragma once
#ifndef TBSTYLE_EX_MIXEDBUTTONS
#define TBSTYLE_EX_MIXEDBUTTONS 0x00000008
#endif
#ifndef BTNS_SHOWTEXT
#define BTNS_SHOWTEXT 0x0040
#endif
#define ATL_SIMPLE_TOOLBAR_PANE_STYLE_EX
(ATL_SIMPLE_TOOLBAR_PANE_STYLE|TBSTYLE_LIST)
template <class T>
class CToolBarHelper
{
private:
class CTBButtonInfo : public TBBUTTONINFO
{
public:
CTBButtonInfo(DWORD dwInitialMask = 0)
{
memset(this, 0, sizeof(TBBUTTONINFO));
cbSize = sizeof(TBBUTTONINFO);
dwMask = dwInitialMask;
}
};
class CTBButton : public TBBUTTON
{
public:
CTBButton()
{
memset(this, 0, sizeof(TBBUTTON));
}
};
private:
CFont m_fontCombo;
public:
BEGIN_MSG_MAP(CToolbarHelper<T>)
COMMAND_CODE_HANDLER(CBN_SELCHANGE, OnSelChangeToolBarCombo)
NOTIFY_CODE_HANDLER(TBN_DROPDOWN, OnToolbarDropDown)
END_MSG_MAP()
void AddToolBarDropDownMenu(HWND hWndToolBar, UINT nButtonID, UINT nMenuID)
{
ATLASSERT(hWndToolBar != NULL);
ATLASSERT(nButtonID > 0);
CToolBarCtrl toolbar(hWndToolBar);
if ((toolbar.GetExtendedStyle() & TBSTYLE_EX_DRAWDDARROWS)
!= TBSTYLE_EX_DRAWDDARROWS)
toolbar.SetExtendedStyle(toolbar.GetExtendedStyle() |
TBSTYLE_EX_DRAWDDARROWS);
CTBButtonInfo tbi(TBIF_STYLE);
if (toolbar.GetButtonInfo(nButtonID, &tbi) != -1)
{
tbi.fsStyle |= TBSTYLE_DROPDOWN;
toolbar.SetButtonInfo(nButtonID, &tbi);
m_mapMenu.Add(nButtonID, nMenuID);
}
}
LRESULT OnToolbarDropDown(int , LPNMHDR pnmh, BOOL& )
{
NMTOOLBAR* ptb = reinterpret_cast<NMTOOLBAR*>(pnmh);
UINT nMenuID = m_mapMenu.Lookup(ptb->iItem);
if (nMenuID)
{
CToolBarCtrl toolbar(pnmh->hwndFrom);
CRect rect;
toolbar.GetItemRect(toolbar.CommandToIndex(ptb->iItem), &rect);
CPoint pt(rect.left, rect.bottom);
toolbar.MapWindowPoints(HWND_DESKTOP, &pt, 1);
CMenu menu;
if (menu.LoadMenu(nMenuID))
{
CMenuHandle menuPopup = menu.GetSubMenu(0);
ATLASSERT(menuPopup != NULL);
T* pT = static_cast<T*>(this);
pT->PrepareToolBarMenu(nMenuID, menuPopup);
pT->m_CmdBar.TrackPopupMenu(menuPopup,
TPM_RIGHTBUTTON|TPM_VERTICAL, pt.x, pt.y);
}
}
return 0;
}
virtual void PrepareToolBarMenu(UINT , HMENU )
{
}
void AddToolbarButtonText(HWND hWndToolBar, UINT nID, LPCTSTR lpsz)
{
CToolBarCtrl toolbar(hWndToolBar);
if ((toolbar.GetExtendedStyle() & TBSTYLE_EX_MIXEDBUTTONS)
!= TBSTYLE_EX_MIXEDBUTTONS)
toolbar.SetExtendedStyle(toolbar.GetExtendedStyle() |
TBSTYLE_EX_MIXEDBUTTONS);
int nIndex = toolbar.CommandToIndex(nID);
CTBButton tb;
toolbar.GetButton(nIndex, &tb);
int nStringID = toolbar.AddStrings(lpsz);
tb.iString = nStringID;
tb.fsStyle |= TBSTYLE_AUTOSIZE|BTNS_SHOWTEXT;
toolbar.DeleteButton(nIndex);
toolbar.InsertButton(nIndex, &tb);
}
void AddToolbarButtonText(HWND hWndToolBar, UINT nID, UINT nStringID)
{
CString str;
if (str.LoadString(nStringID))
AddToolbarButtonText(hWndToolBar, nID, str);
}
void AddToolbarButtonText(HWND hWndToolBar, UINT nID)
{
TCHAR sz[256];
if (AtlLoadString(nID, sz, 256) > 0)
{
TCHAR* psz = _tcsrchr(sz, '\n');
if (psz != NULL)
{
psz++;
TCHAR* pBrace = _tcschr(psz, '(');
if (pBrace != NULL)
*(pBrace - 1) = '\0';
AddToolbarButtonText(hWndToolBar, nID, psz);
}
}
}
HWND CreateToolbarComboBox(HWND hWndToolBar, UINT nID,
UINT nWidth = 16, UINT nHeight = 16,
DWORD dwComboStyle = CBS_DROPDOWNLIST)
{
T* pT = static_cast<T*>(this);
CToolBarCtrl toolbar(hWndToolBar);
CreateComboFont();
CSize sizeFont = GetComboFontSize();
UINT cx = (nWidth + 8) * sizeFont.cx;
UINT cy = nHeight * sizeFont.cy;
CTBButtonInfo tbi(TBIF_SIZE|TBIF_STATE|TBIF_STYLE);
tbi.fsState = 0;
tbi.fsStyle = BTNS_SHOWTEXT;
tbi.cx = static_cast<WORD>(cx);
toolbar.SetButtonInfo(nID, &tbi);
int nIndex = toolbar.CommandToIndex(nID);
CRect rc;
toolbar.GetItemRect(nIndex, rc);
rc.bottom = cy;
DWORD dwStyle = WS_CHILD|WS_VISIBLE|WS_VSCROLL|WS_TABSTOP|dwComboStyle;
CComboBox combo;
combo.Create(pT->m_hWnd, rc, NULL, dwStyle, 0, nID);
combo.SetFont(m_fontCombo);
combo.SetParent(toolbar);
CRect rectToolBar;
CRect rectCombo;
toolbar.GetClientRect(&rectToolBar);
combo.GetWindowRect(rectCombo);
int nDiff = rectToolBar.Height() - rectCombo.Height();
if (nDiff > 1)
{
toolbar.ScreenToClient(&rectCombo);
combo.MoveWindow(rectCombo.left, rc.top + (nDiff / 2),
rectCombo.Width(), rectCombo.Height());
}
return combo;
}
void CreateComboFont()
{
if (m_fontCombo == NULL)
{
NONCLIENTMETRICS ncm;
ncm.cbSize = sizeof(NONCLIENTMETRICS);
::SystemParametersInfo(SPI_GETNONCLIENTMETRICS, ncm.cbSize, &ncm, 0);
m_fontCombo.CreateFontIndirect(&ncm.lfMenuFont);
ATLASSERT(m_fontCombo != NULL);
}
}
CSize GetComboFontSize()
{
ATLASSERT(m_fontCombo != NULL);
const T* pT = static_cast<const T*>(this);
CClientDC dc(pT->m_hWnd);
CFontHandle fontOld = dc.SelectFont(m_fontCombo);
TEXTMETRIC tm;
dc.GetTextMetrics(&tm);
dc.SelectFont(fontOld);
return CSize(tm.tmAveCharWidth, tm.tmHeight + tm.tmExternalLeading);
}
LRESULT OnSelChangeToolBarCombo(WORD , WORD wID,
HWND hWndCtl, BOOL& )
{
T* pT = static_cast<T*>(this);
CComboBox combo(hWndCtl);
int nSel = combo.GetCurSel();
CString strItemText;
combo.GetLBText(nSel, strItemText);
DWORD dwItemData = combo.GetItemData(nSel);
pT->OnToolBarCombo(combo, wID, nSel, strItemText, dwItemData);
pT->SetFocus();
return TRUE;
}
};
Sample Application
Two sample applications are provided - one for Visual Studio 6, and one for Visual Studio 2005. I am using WTL 7.5, but it should work with earlier versions I think. Note that the code is Unicode compliant, and the Visual Studio 2005 samples include Unicode release and debug builds.
The sample application demonstrates all the available CToolBarHelper
methods:
- Click the New button drop-down to display a menu that is modified on the fly.
- Select a view colour using the first combobox.
- Click the Colour button to cycle through the colours.
- Click the Colour button drop-down to display a menu of colours, including button icons. Note how changing the colour will cause the first combobox to update.
- The second combobox is simply added in
OnCreate
and never updated via OnIdle
.
- The Save, Paste, and About button texts have been added using the various
AddToolbarButtonText
methods.
Improvements
If you have any ideas or suggestions on how I can improve this code, then please feel free to post them. One idea I have is to add support to add edit controls to toolbars, which shouldn't be too difficult.