Click here to Skip to main content
65,938 articles
CodeProject is changing. Read more.
Articles
(untagged)

WTL Toolbar Helper

0.00/5 (No votes)
11 Dec 2006 7  
Add text, drop-down menus, and comboboxes to a WTL toolbar.

Sample Image - toolbar_helper.png

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:

  1. Make sure the toolbar has the TBSTYLE_EX_MIXEDBUTTONS style bit set.
  2. Call the CToolBarCtrl::AddStrings method to add the button text to the toolbar's internal list.
  3. Change the button info to ensure the TBSTYLE_AUTOSIZE and BTNS_SHOWTEXT bits are set.
  4. 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:

  1. Make sure the button itself has the TBSTYLE_EX_DRAWDDARROWS style assigned.
  2. Handle the TBN_DROPDOWN notification message.
  3. 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)
{
 // Check combo selection is accurate

}

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:

Sample screenshot

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

// Define various toolbar button styles in case they are missing

#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)

/// Class used to expost useful toolbar
/// functionality to a WTL CFrameWnd-derived class

template <class T>
class CToolBarHelper
{
private:
 /// Wrapper class for the Win32 TBBUTTONINFO structure.

 class CTBButtonInfo : public TBBUTTONINFO
 {
 public:
  /// Constructor

  CTBButtonInfo(DWORD dwInitialMask = 0)
  {
   memset(this, 0, sizeof(TBBUTTONINFO));
   cbSize = sizeof(TBBUTTONINFO);
   dwMask = dwInitialMask;
  }
 };

 /// Wrapper class for the Win32 TBBUTTON structure.

 class CTBButton : public TBBUTTON
 {
 public:
  /// Constructor

  CTBButton()
  {
   memset(this, 0, sizeof(TBBUTTON));
  }
 };
private:
 CFont m_fontCombo;   ///< Font to use for comboboxes
 CSimpleMap<UINT, UINT> m_mapMenu; ///< Map of command IDs -> menu IDs

public:
 /// Message map

 BEGIN_MSG_MAP(CToolbarHelper<T>)
  COMMAND_CODE_HANDLER(CBN_SELCHANGE, OnSelChangeToolBarCombo)
  NOTIFY_CODE_HANDLER(TBN_DROPDOWN, OnToolbarDropDown)
 END_MSG_MAP()

 /// Modify a toolbar button to have a drop-down button

 void AddToolBarDropDownMenu(HWND hWndToolBar, UINT nButtonID, UINT nMenuID)
 {
  ATLASSERT(hWndToolBar != NULL);
  ATLASSERT(nButtonID > 0);
  // Use built-in WTL toolbar wrapper class

  CToolBarCtrl toolbar(hWndToolBar);
  // Add the necessary style bit (TBSTYLE_EX_DRAWDDARROWS) if

  // not already present

  if ((toolbar.GetExtendedStyle() & TBSTYLE_EX_DRAWDDARROWS) 
                                  != TBSTYLE_EX_DRAWDDARROWS)
   toolbar.SetExtendedStyle(toolbar.GetExtendedStyle() | 
                            TBSTYLE_EX_DRAWDDARROWS);
  // Get existing button style

  CTBButtonInfo tbi(TBIF_STYLE);
  if (toolbar.GetButtonInfo(nButtonID, &tbi) != -1)
  {
   // Modify the button

   tbi.fsStyle |= TBSTYLE_DROPDOWN;
   toolbar.SetButtonInfo(nButtonID, &tbi);
   // We need to remember that this menu
   // ID is associated with the button ID
   // so use a basic map for this.

   m_mapMenu.Add(nButtonID, nMenuID);
  }
 }

 LRESULT OnToolbarDropDown(int /*idCtrl*/, LPNMHDR pnmh, BOOL& /*bHandled*/)
 {
  // Get the toolbar data

  NMTOOLBAR* ptb = reinterpret_cast<NMTOOLBAR*>(pnmh);
  // See if the button ID has an asscociated menu ID

  UINT nMenuID = m_mapMenu.Lookup(ptb->iItem);
  if (nMenuID)
  {
   // Get the toolbar control

   CToolBarCtrl toolbar(pnmh->hwndFrom);
   // Get the button rect

   CRect rect;
   toolbar.GetItemRect(toolbar.CommandToIndex(ptb->iItem), &rect);
   // Create a point

   CPoint pt(rect.left, rect.bottom);
   // Map the points

   toolbar.MapWindowPoints(HWND_DESKTOP, &pt, 1);
   // Load the menu

   CMenu menu;
   if (menu.LoadMenu(nMenuID))
   {
    CMenuHandle menuPopup = menu.GetSubMenu(0);
    ATLASSERT(menuPopup != NULL);
    T* pT = static_cast<T*>(this);
    // Allow the menu items to be initialised (for example,

    // new items could be added here for example)

    pT->PrepareToolBarMenu(nMenuID, menuPopup);
    // Display the menu    

    // Using command bar TrackPopupMenu method means menu icons are displayed

    pT->m_CmdBar.TrackPopupMenu(menuPopup, 
       TPM_RIGHTBUTTON|TPM_VERTICAL, pt.x, pt.y);
   }
  }
  return 0;
 }

 /// Override this Allow the menu items to be enabled/checked/etc.

 virtual void PrepareToolBarMenu(UINT /*nMenuID*/, HMENU /*hMenu*/)
 {
 }

 /// Add text to a toolbar button

 void AddToolbarButtonText(HWND hWndToolBar, UINT nID, LPCTSTR lpsz)
 {
  // Use built-in WTL toolbar wrapper class

  CToolBarCtrl toolbar(hWndToolBar);
  // Set extended style

  if ((toolbar.GetExtendedStyle() & TBSTYLE_EX_MIXEDBUTTONS) 
                                  != TBSTYLE_EX_MIXEDBUTTONS)
   toolbar.SetExtendedStyle(toolbar.GetExtendedStyle() | 
                            TBSTYLE_EX_MIXEDBUTTONS);
  // Get the button index

  int nIndex = toolbar.CommandToIndex(nID);
  CTBButton tb;
  toolbar.GetButton(nIndex, &tb);
  int nStringID = toolbar.AddStrings(lpsz);
  // Alter the button style

  tb.iString = nStringID;
  tb.fsStyle |= TBSTYLE_AUTOSIZE|BTNS_SHOWTEXT;
  // Delete and re-insert the button

  toolbar.DeleteButton(nIndex);
  toolbar.InsertButton(nIndex, &tb);
 }

 /// Add resource string to a toolbar button

 void AddToolbarButtonText(HWND hWndToolBar, UINT nID, UINT nStringID)
 {
  CString str;
  if (str.LoadString(nStringID))
   AddToolbarButtonText(hWndToolBar, nID, str);
 }

 /// Add text to a toolbar button (using tooltip text)

 void AddToolbarButtonText(HWND hWndToolBar, UINT nID)
 {
  TCHAR sz[256];
  if (AtlLoadString(nID, sz, 256) > 0)
  {
   // Add the text following the '\n'

   TCHAR* psz = _tcsrchr(sz, '\n');
   if (psz != NULL)
   {
    // Skip to first character of the tooltip

    psz++;
    // The tooltip text may include the accelerator, i.e.

    // Open (Ctrl+O)

    // So look for an open brace

    TCHAR* pBrace = _tcschr(psz, '(');
    if (pBrace != NULL)
     *(pBrace - 1) = '\0';
    AddToolbarButtonText(hWndToolBar, nID, psz);
   }
  }
 } 

 /// Create a combobox on a toolbar

 HWND CreateToolbarComboBox(HWND hWndToolBar, UINT nID, 
      UINT nWidth = 16, UINT nHeight = 16,
      DWORD dwComboStyle = CBS_DROPDOWNLIST)
 {
  T* pT = static_cast<T*>(this);
  // Use built-in WTL toolbar wrapper class

  CToolBarCtrl toolbar(hWndToolBar);
  // Get the size of the combobox font

  CreateComboFont();
  CSize sizeFont = GetComboFontSize();
  // Compute the width and height

  UINT cx = (nWidth + 8) * sizeFont.cx;
  UINT cy = nHeight * sizeFont.cy;
  // Set the button width

  CTBButtonInfo tbi(TBIF_SIZE|TBIF_STATE|TBIF_STYLE);
  // Make sure the underlying button is disabled

  tbi.fsState = 0;
  // BTNS_SHOWTEXT will allow the button size to be altered

  tbi.fsStyle = BTNS_SHOWTEXT;
  tbi.cx = static_cast<WORD>(cx);
  toolbar.SetButtonInfo(nID, &tbi);
  // Get the index of the toolbar button

  int nIndex = toolbar.CommandToIndex(nID);
  // Get the button rect

  CRect rc;
  toolbar.GetItemRect(nIndex, rc);
  rc.bottom = cy;
  // Create the combobox

  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);
  // The combobox might not be centred vertically, and we won't know the

  // height until it has been created. Get the size now and see if it

  // needs to be moved.

  CRect rectToolBar;
  CRect rectCombo;
  toolbar.GetClientRect(&rectToolBar);  
  combo.GetWindowRect(rectCombo);
  // Get the different between the heights of the toolbar and

  // the combobox

  int nDiff = rectToolBar.Height() - rectCombo.Height();
  // If there is a difference, then move the combobox

  if (nDiff > 1)
  {
   toolbar.ScreenToClient(&rectCombo);
   combo.MoveWindow(rectCombo.left, rc.top + (nDiff / 2), 
                    rectCombo.Width(), rectCombo.Height());
  }
  return combo;
 }

 /// Create the font to use for comboboxes

 void CreateComboFont()
 {
  if (m_fontCombo == NULL)
  {
   NONCLIENTMETRICS ncm;
   ncm.cbSize = sizeof(NONCLIENTMETRICS);
   ::SystemParametersInfo(SPI_GETNONCLIENTMETRICS, ncm.cbSize, &ncm, 0);
   // Create menu font

   m_fontCombo.CreateFontIndirect(&ncm.lfMenuFont);
   ATLASSERT(m_fontCombo != NULL);
  }
 }

 /// Get the size of the default GUI font

 CSize GetComboFontSize()
 {
  ATLASSERT(m_fontCombo != NULL);
  // We need a temporary DC

  const T* pT = static_cast<const T*>(this);
  CClientDC dc(pT->m_hWnd);
  // Select in the menu font

  CFontHandle fontOld = dc.SelectFont(m_fontCombo);
  // Get the font size

  TEXTMETRIC tm;
  dc.GetTextMetrics(&tm);
  // Done with the font

  dc.SelectFont(fontOld);
  // Return the width and height

  return CSize(tm.tmAveCharWidth, tm.tmHeight + tm.tmExternalLeading);
 }

 LRESULT OnSelChangeToolBarCombo(WORD /*wNotifyCode*/, WORD wID, 
         HWND hWndCtl, BOOL& /*bHandled*/)
 {
  T* pT = static_cast<T*>(this);
  // Get the newly selected item index

  CComboBox combo(hWndCtl);
  int nSel = combo.GetCurSel();
  // Get the item text

  CString strItemText;
  combo.GetLBText(nSel, strItemText);
  // Get the item data

  DWORD dwItemData = combo.GetItemData(nSel);
  // Call special function to handle the selection change

  pT->OnToolBarCombo(combo, wID, nSel, strItemText, dwItemData);
  // Set focus to the main window

  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.

License

This article has no explicit license attached to it but may contain usage terms in the article text or the download files themselves. If in doubt please contact the author via the discussion board below.

A list of licenses authors might use can be found here