WINAPI Example: Common Control Toolbar in a Pager

example
A toolbar is that strip of “buttons” that appears in many Windows applications, such as the one outlined in red to the left.

I had a lot of problems with programming my first toolbar. It would not appear, or not appear where I wanted it, or the buttons would behave in a strange way, or the customize dialog didn’t work right.

Following is a minimal Windows app in plain c to create a toolbar. It was compiled under Visual Studio Express 2010.

// toolbartest.cpp : minimal app to make a toolbar

#pragma comment(lib, "comctl32.lib")

#include "stdafx.h"

#include <commctrl.h>

// Globals
HINSTANCE hInst;
HWND hWndMain, hWndToolbar;

LRESULT CALLBACK WndProc(HWND, UINT, WPARAM, LPARAM);

int WINAPI WinMain( HINSTANCE hInstance, HINSTANCE hPrevInstance,
            LPSTR lpCmdLine, int nCmdShow )
{
  MSG  msg ;    
  WNDCLASS wc = {0};

  hInst = hInstance;

  wc.lpszClassName = TEXT("MainClass") ;
  wc.hInstance     = hInst ;
  wc.hbrBackground = GetSysColorBrush(COLOR_3DFACE);
  wc.lpfnWndProc   = WndProc ;
  wc.hCursor       = LoadCursor(0, IDC_ARROW);

  RegisterClass(&wc);

  hWndMain = CreateWindow(wc.lpszClassName, TEXT("ToolbarTest"),
                WS_OVERLAPPEDWINDOW | WS_VISIBLE,
                100, 100, 400, 300, 0, 0, hInst, 0);  

  DWORD status;
  INITCOMMONCONTROLSEX ic;

  ic.dwSize = sizeof(INITCOMMONCONTROLSEX);
  ic.dwICC = ICC_BAR_CLASSES;
  InitCommonControlsEx(&ic);

   // Create Toolbar.  Arbitrary ID of 231.
   hWndToolbar = CreateWindowEx(0, TOOLBARCLASSNAME,
                    TEXT("Toolbar Title"),
                    WS_VISIBLE | WS_CHILD | CCS_NORESIZE
                    | CCS_ADJUSTABLE,
                    50, 50, 200, 50, hWndMain, (HMENU) 231, hInst, 0);

	while( GetMessage(&msg, NULL, 0, 0)) {
		TranslateMessage(&msg);
		DispatchMessage(&msg);
  }
  return (int) msg.wParam;
}

LRESULT CALLBACK WndProc( HWND hwnd, UINT msg, WPARAM wParam, LPARAM lParam )
{
  switch(msg)  
  {
    case WM_DESTROY:
        PostQuitMessage(0);
        break;
    default:
        return DefWindowProc(hwnd, msg, wParam, lParam);
  }
  return 0;
}

A toolbar is one of the Windows Common Controls, so we need to include comctrl.h, and at some point, link the object with comctl32.lib . In this case, the toolbar is the only control that we wish to use, so we can set ICC_BAR_CLASSES at the appropriate place in a structure, and call InitCommonControlsEx(). And here is what shows up when the program is run:

It's invisible
Where’s the toolbar?

The thing is, just because we create a toolbar with the WS_VISIBLE style doesn’t mean that we’ll be able to see it. The toolbar is the same color as the default window background, making it invisible. In fact, the toolbar may even be overlaid over another control, effectively disabling the control under it, all while appearing invisible.

Let’s add a button to the toolbar, so that we can see where it is. The following code is added just after creating the toolbar:

status = SendMessage(hWndToolbar, TB_SETBITMAPSIZE,
            (WPARAM)0, (LPARAM)MAKELONG(24,24));
status = SendMessage(hWndToolbar, TB_LOADIMAGES,
            (WPARAM)IDB_HIST_LARGE_COLOR, (LPARAM)HINST_COMMCTRL);
status = SendMessage(hWndToolbar, TB_BUTTONSTRUCTSIZE,
            sizeof(TBBUTTON), 0);
status = SendMessage(hWndToolbar, TB_ADDBUTTONS,
            1, (LPARAM)tbButtonsAdd);
status = SendMessage(hWndToolbar, TB_AUTOSIZE, 0, 0);

The first call with TB_SETBITMAPSIZE sets the size of the images on the buttons. If this is not done, we get 16×16 pixels as the default.

The next call with TB_LOADIMAGES loads a set of images into the toolbar to be available for use on buttons. IDB_HIST_LARGE_COLOR is an identifier for a standard set of bitmap images. Usually, we will want to make up our own image list, or add images individually with TB_ADDBITMAP, but this will do for a quick demo.

TB_BUTTONSTRUCTSIZE must be sent to the toolbar before adding any buttons. This is because different versions of common controls may have different structure sizes.

TB_ADDBUTTONS adds buttons (just 1 in this case), from a list of buttons pointed to by tbButtonsAdd (more on this below).

Finally, any time buttons are added, TB_AUTOSIZE must be sent, so that the toolbar can update its dimensions.

tbButtonsAdd is an array of buttons, and is set up as follows:

// Button list for toolbar
// Note that wide strings explicitly used - TEXT("label") messes up dialog box, even
// if compiled as non-unicode
// Arbitrary command 100-104 assigned to buttons, will never be processed
TBBUTTON tbButtonsAdd[] = {
	{MAKELONG(HIST_BACK,0), 100, TBSTATE_ENABLED, BTNS_AUTOSIZE, {0}, 0, (INT_PTR) L"one"},
	{MAKELONG(HIST_FORWARD,0), 101, TBSTATE_ENABLED, BTNS_AUTOSIZE, {0}, 0, (INT_PTR) L"two"},
	{MAKELONG(HIST_VIEWTREE,0), 102, TBSTATE_ENABLED, BTNS_AUTOSIZE, {0}, 0, (INT_PTR) L"three"},
	{MAKELONG(HIST_ADDTOFAVORITES,0), 103, TBSTATE_ENABLED, BTNS_AUTOSIZE, {0}, 0, (INT_PTR) L"four"},
	{MAKELONG(HIST_FAVORITES,0), 104, TBSTATE_ENABLED, BTNS_AUTOSIZE, {0}, 0, (INT_PTR) L"five"}
}; 
const int numButtons = sizeof(tbButtonsAdd)/sizeof(TBBUTTON);

The first element (for example, MAKELONG(HIST_BACK,0) ) in the structure for each button specifies the bitmap that will be used for the button. The second element is the command associated with the button being pushed. Looking again at the first button, if that is pushed, the main window will get a message of WM_COMMAND, with wParam set to the button ID of 100. Numbers have been arbitrarily chosen for the button IDs.

Each button must have a unique ID. It’s tempting to route multiple buttons to the same ID for prototyping purposes, but don’t do it. That will cause Windows to see the second button as a duplicate, and ignore it when the customize dialog (more on this below) is opened.

With these changes to the code, the program’s window now looks like:

toolbar has wrong place and size
At least something shows up now. But wait – didn’t we ask for the toolbar to be positioned elsewhere?

Looking back at the CreateWindowEx() for the toolbar, we would expect the bar to be created at (50, 50) with a size of (200, 50). Yet, the toolbar is jammed up against the top of the window, and seems to have ignored our width and height specifications. In fact, even if we were to call SetWindowPos() on the toolbar, it would neither move nor size. Toolbars always seem to be placed at the top of the client area of their parent windows, and take up the entire width of the window. A little further down, we will move the toolbar to a non-default location.

Toolbars have an ability to customize themselves. The user can shift-drag buttons to rearrange or delete them, or double-click on a non-button area of the toolbar to open a customization dialog like this:

toolbar customize dialog

This is enabled by specifying the CCS_ADJUSTABLE style when creating the toolbar. Also, the following case needs to be added to the Windows Procedure for the main window:

case WM_NOTIFY:
    switch( ((LPNMHDR)lParam )->code) {
        case TBN_QUERYINSERT:
        case TBN_QUERYDELETE:
            // no need to check the source of the request, as
            // there is only one toolbar in the program,
            // and the answer is always yes
            return true;
            break;  // not really needed, but good discipline
        case TBN_GETBUTTONINFO:  
            LPTBNOTIFY lpTbNotify;
            lpTbNotify = (LPTBNOTIFY)lParam;

            // Pass the next button from the array. There is no need
            // to filter out buttons that are already used — they will
            // be ignored.
            if (lpTbNotify->iItem < numButtons) {
                lpTbNotify->tbButton = tbButtonsAdd[lpTbNotify->iItem];
                return TRUE;
            } else {
                return FALSE;  // No more buttons.
            }
            break;  // programming discipline
        default:
            break;
    }
    break;

It is important to return true in response to TBN_QUERYINSERT and TBN_QUERYDELETE. Without these affirmative responses, customizing the toolbar will not be possible.

When TBN_GETBUTTONINFO is received, the lParam pointer, which pointed to a general NMHDR structure, is re-characterized to point at a TBNOTIFY structure.  Same address, just additional data at the end.  At this point, the code responds with info about the buttons that should be available for adding in the customize dialog. Note that multiple TBN_GETBUTTONINFO  messages will be received, until the code returns false,  indicating that there are no further buttons to make available.

Now let’s move the toolbar to an arbitrary size and location. To do that, we will put the toolbar into another window, and move that window. The Pager common control is ideal for this. Here’s the updated window creation code:

ic.dwSize = sizeof(INITCOMMONCONTROLSEX);
ic.dwICC = ICC_BAR_CLASSES | ICC_PAGESCROLLER_CLASS;
InitCommonControlsEx(&ic);

// Create Pager.  Arbitrary ID of 343.
    hWndPager = CreateWindow(WC_PAGESCROLLER, NULL, WS_VISIBLE | WS_CHILD | PGS_DRAGNDROP | PGS_HORZ,
    50, 100, 100, 50, hWndMain, (HMENU) 343, hInst, NULL);

// Create Toolbar.  Arbitrary ID of 231.
// The parent window is the Pager.
hWndToolbar = CreateWindowEx(0, TOOLBARCLASSNAME, TEXT("Toolbar in Pager Demo"), WS_VISIBLE | WS_CHILD | CCS_NORESIZE | CCS_ADJUSTABLE,
      0, 0, 200, 50, hWndPager, (HMENU) 231, hInst, NULL);

status = SendMessage(hWndToolbar, TB_SETBITMAPSIZE ,
            (WPARAM)0, (LPARAM)MAKELONG(24,24));
status = SendMessage(hWndToolbar, TB_LOADIMAGES,
            (WPARAM)IDB_HIST_LARGE_COLOR, (LPARAM)HINST_COMMCTRL);
status = SendMessage(hWndToolbar, TB_BUTTONSTRUCTSIZE,
            sizeof(TBBUTTON), 0);
status = SendMessage(hWndToolbar, TB_ADDBUTTONS,
            5, (LPARAM)tbButtonsAdd);

status = SendMessage(hWndToolbar, TB_AUTOSIZE, 0, 0);

// Notify the pager that it contains the toolbar
// This is in addition to making the Pager Window
// the parent of the Toolbar Window in CreateWindowEx()
SendMessage(hWndPager, PGM_SETCHILD, 0, (LPARAM) hWndToolbar);

We add the ICC_PAGESCROLLER_CLASS to the set of common controls to initialize.  The parent of the Toolbar window is set to the Pager window.  Moreover, we send a PGM_SETCHILD message to set up the parent-child relationship.

The result is this:

toolbar in a pager control
The toolbar is now contained in a pager, and the pager is located where we want on the screen. Only 3 of the 5 buttons we want to present to the user are available, though. One more change is needed.

As the name implies, a Pager control will allow the user to page through its contents. In this case, we want to present 5 buttons, but the Pager is only big enough to show three. The following additional case needs to be added to the processing of WM_NOTIFY:

case PGN_CALCSIZE:
    // If more than one pager in the program
    // may need to check which one sent message
    LPNMPGCALCSIZE pCS;
    SIZE tbSize;
    if (SendMessage(hWndToolbar, TB_GETIDEALSIZE, 0, (LPARAM) &tbSize)) {
        pCS = (LPNMPGCALCSIZE)lParam;
        switch( pCS->dwFlag ){
            case PGF_CALCHEIGHT:
                pCS->iHeight = tbSize.cy;
                break;
            case PGF_CALCWIDTH:
                pCS->iWidth = tbSize.cx;
                break;
            default:
                break;
        }
}
break;

The PGN_CALCSIZE notification is asking about the size of the contents of the pager.  For example, if pCS->iWidth is larger than the actual width of the pager, arrow(s) appear to page left and/or right.  Think of iWidth as “ideal width”.  In this case, the contained control is the toolbar, so we obtain its ideal dimensions with the TB_GETIDEALSIZE message. With these changes, the window looks like this:

toolbar in pager now pageable
Here is the pager offering a page-right arrow, since the contained toolbar is too big to display all at once.
This is what it looks like after clicking the page-right arrow.
This is what it looks like after clicking the page-right arrow.

For good measure, the following is added, to insure that the pager’s scroll buttons appear or disappear properly any time buttons are added or deleted:

case TBN_TOOLBARCHANGE:
    PostMessage(hWndPager, PGM_RECALCSIZE, 0, 0);
    break;

Finally, here is the complete demo source:

// toolbartest.cpp : minimal app to demo issue

#pragma comment(lib, "comctl32.lib")

#include "stdafx.h"

#include 

// Globals
HINSTANCE hInst;
HWND hWndMain, hWndPager, hWndToolbar;
// Button list for toolbar
// Note that wide strings explicitly used - TEXT("label") messes up dialog box, even
// if compiled as non-unicode
// Arbitrary command 100-104 assigned to buttons, will never be processed
TBBUTTON tbButtonsAdd[] = {
	{MAKELONG(HIST_BACK,0), 100, TBSTATE_ENABLED, BTNS_AUTOSIZE, {0}, 0, (INT_PTR) L"one"},
	{MAKELONG(HIST_FORWARD,0), 101, TBSTATE_ENABLED, BTNS_AUTOSIZE, {0}, 0, (INT_PTR) L"two"},
	{MAKELONG(HIST_VIEWTREE,0), 102, TBSTATE_ENABLED, BTNS_AUTOSIZE, {0}, 0, (INT_PTR) L"three"},
	{MAKELONG(HIST_ADDTOFAVORITES,0), 103, TBSTATE_ENABLED, BTNS_AUTOSIZE, {0}, 0, (INT_PTR) L"four"},
	{MAKELONG(HIST_FAVORITES,0), 104, TBSTATE_ENABLED, BTNS_AUTOSIZE, {0}, 0, (INT_PTR) L"five"}
}; 
const int numButtons = sizeof(tbButtonsAdd)/sizeof(TBBUTTON);

LRESULT CALLBACK WndProc(HWND, UINT, WPARAM, LPARAM);

int WINAPI WinMain( HINSTANCE hInstance, HINSTANCE hPrevInstance,
            LPSTR lpCmdLine, int nCmdShow )
{
	MSG  msg ;    
	WNDCLASS wc = {0};

	hInst = hInstance;

	wc.lpszClassName = TEXT("MainClass") ;
	wc.hInstance     = hInst ;
	wc.hbrBackground = GetSysColorBrush(COLOR_3DFACE);
	wc.lpfnWndProc   = WndProc ;
	wc.hCursor       = LoadCursor(0, IDC_ARROW);

	RegisterClass(&wc);

	hWndMain = CreateWindow(wc.lpszClassName, TEXT("ToolbarTest"),
                WS_OVERLAPPEDWINDOW | WS_VISIBLE,
                100, 100, 400, 300, 0, 0, hInst, 0);  

	DWORD status;
	INITCOMMONCONTROLSEX ic;

	ic.dwSize = sizeof(INITCOMMONCONTROLSEX);
	ic.dwICC = ICC_BAR_CLASSES | ICC_PAGESCROLLER_CLASS;
	InitCommonControlsEx(&ic);

    // Create Pager.  Arbitrary ID of 343.
	hWndPager = CreateWindow(WC_PAGESCROLLER, NULL, WS_VISIBLE | WS_CHILD | PGS_DRAGNDROP | PGS_HORZ,
	  50, 100, 100, 50, hWndMain, (HMENU) 343, hInst, NULL);

    // Create Toolbar.  Arbitrary ID of 231.
	// The parent window is the Pager.
	hWndToolbar = CreateWindowEx(0, TOOLBARCLASSNAME, TEXT("Toolbar in Pager Demo"), WS_VISIBLE | WS_CHILD | CCS_NORESIZE | CCS_ADJUSTABLE,
      0, 0, 200, 50, hWndPager, (HMENU) 231, hInst, NULL);

	status = SendMessage(hWndToolbar, TB_SETBITMAPSIZE, (WPARAM)0, (LPARAM)MAKELONG(24,24));
	status = SendMessage(hWndToolbar, TB_LOADIMAGES, (WPARAM)IDB_HIST_LARGE_COLOR, (LPARAM)HINST_COMMCTRL);
	status = SendMessage(hWndToolbar, TB_BUTTONSTRUCTSIZE, sizeof(TBBUTTON), 0);
	status = SendMessage(hWndToolbar, TB_ADDBUTTONS, 5, (LPARAM)tbButtonsAdd);

	status = SendMessage(hWndToolbar, TB_AUTOSIZE, 0, 0);

	// Notify the pager that it contains the toolbar
	// This is in addition to making the Pager Window the parent of the Toolbar Window
	// in CreateWindowEx()
	SendMessage(hWndPager, PGM_SETCHILD, 0, (LPARAM) hWndToolbar);

	while( GetMessage(&msg, NULL, 0, 0)) {
		TranslateMessage(&msg);
		DispatchMessage(&msg);
    }

	return (int) msg.wParam;
}

LRESULT CALLBACK WndProc( HWND hwnd, UINT msg, WPARAM wParam, LPARAM lParam )
{
  switch(msg)  
  {
 	case WM_NOTIFY:
		switch( ((LPNMHDR)lParam )->code) {
			case PGN_CALCSIZE:
				// If more than one pager in the program
				// may need to check which one sent message
				LPNMPGCALCSIZE pCS;
				SIZE tbSize;
				if (SendMessage(hWndToolbar, TB_GETIDEALSIZE, 0, (LPARAM) &tbSize)) {
					pCS = (LPNMPGCALCSIZE)lParam;
					switch( pCS->dwFlag ){
						case PGF_CALCHEIGHT:
							pCS->iHeight = tbSize.cy;
							break;
						case PGF_CALCWIDTH:
							pCS->iWidth = tbSize.cx;
							break;
						default:
							break;
					}
				}
				break;
			case TBN_TOOLBARCHANGE:
				PostMessage(hWndPager, PGM_RECALCSIZE, 0, 0);
				break;
			case TBN_GETBUTTONINFO:  
                LPTBNOTIFY lpTbNotify;
				lpTbNotify = (LPTBNOTIFY)lParam;

                // Pass the next button from the array. There is no need to filter out buttons
                // that are already used—they will be ignored.
                if (lpTbNotify->iItem < numButtons) {                     lpTbNotify->tbButton = tbButtonsAdd[lpTbNotify->iItem];
                    return TRUE;
                } else {
                    return FALSE;  // No more buttons.
                }
				break;  // not really necessary, but keep programming discipline
			case TBN_QUERYINSERT:
			case TBN_QUERYDELETE:
				// no need to check the source of the request, as
				// there is only one toolbar in the program,
				// and the answer is always yes
				return true;
				break;
			default:
				break;
		}
		break;
    case WM_DESTROY:
        PostQuitMessage(0);
        break;
	default:
		return DefWindowProc(hwnd, msg, wParam, lParam);
  }
  return 0;
}
(Visited 498 times, 1 visits today)

Leave a Reply

This site uses Akismet to reduce spam. Learn how your comment data is processed.