Click here to Skip to main content
Click here to Skip to main content
Go to top

Win32 SDK C Tab Control Made Easy

, 18 Jun 2009
Rate this:
Please Sign up or sign in to vote.
This article describes formatting a non-MFC based tab control
Screenshot - Win32_SDK_C_TabCtrlDemo_1.gif

Introduction

Several years ago, I wanted to use the tab control in a Win32 SDK C project. The low-level implementation proved to be somewhat cumbersome. A search of the Web turned up a lot of examples based on MFC, but very little along the lines of plain old C. What I did find did not support keyboard navigation. After extensively using tab controls in VB.NET, I had an idea of what features would make for an easy-to-implement Win32 tab control:

  1. Tab pages should be designed as separate dialogs. These could then be hidden and shown during tab selection.
  2. All events received on controls should be handled in the parent dialog's callback procedure.
  3. Keyboard navigation of the tab control, as well as tab pages, must work.
  4. It must be fairly simple to define moving and sizing constraints within the dialog.

At the time I came up with a solution that worked fairly well and posted it here for the edification of the CodeProject community. Recently I revisited this project on account of some of the comments left here. I examined the code from the vantage point of a few more years of experience and saw some room for improvement. This version, 3.0 offers much better handling of mouse clicks and keyboard input.

Usage

A look at TabCtrl.h reveals a struct and two method prototypes. I took a pseudo object-oriented approach to this problem in order to make it easy to implement more than one tab control simultaneously. I made extensive use of pointers to functions so that it would not be necessary to add to or update prototyping in the TabCtrl.c module or parent project.

In this version, I also include a handy macro for selecting tabs from an external event such as a button click on the parent dialog.

/***************************************************************************/
// Structs

typedef struct TabControl
{
	HWND hTab;
	HWND hVisiblePage;
	HWND* hTabPages;
	LPSTR *tabNames;
	int tabPageCount;
	BOOL blStretchTabs;

	// Function pointer to Parent Dialog Proc
	BOOL (CALLBACK *ParentProc)(HWND, UINT, WPARAM, LPARAM);

	// Function pointer to Tab Page Size Proc
	void (*TabPage_OnSize)(HWND hwnd, UINT state, int cx, int cy);

	// Pointers to shared functions
	BOOL (*Notify) (LPNMHDR);
	BOOL (*StretchTabPage) (HWND, INT);
	BOOL (*CenterTabPage) (HWND, INT);

}TABCTRL, *LPTABCTRL;

/***************************************************************************/
// Exported function prototypes


void New_TabControl(LPTABCTRL,
                    HWND,
                    LPSTR*,
                    LPSTR*,
                    BOOL (CALLBACK *ParentProc)(HWND, UINT, WPARAM, LPARAM),
                    VOID (*TabPage_OnSize)(HWND, UINT, int, int),
                    BOOL fStretch);

void TabControl_Destroy(LPTABCTRL);

/****************************************************************************/
// Macros

#define TabCtrl_SelectTab(hTab,iSel) { \
	TabCtrl_SetCurSel(hTab,iSel); \
	NMHDR nmh = { hTab, GetDlgCtrlID(hTab), TCN_SELCHANGE }; \
	SendMessage(nmh.hwndFrom,WM_NOTIFY,(WPARAM)nmh.idFrom,(LPARAM)&nmh); }

Using this class is fairly straightforward. In the demo, I did the following:

  1. Placed and sized two tab controls.
  2. Created a dialog for each desired tab page. Note that dialogs should be borderless and sized to fit the tab control's client area.
  3. Declared an instance of the TabControl struct for each of my tab controls.
/** Global variables *******************************************************/

static HANDLE ghInstance;
static SIZE gMinSize;
static TABCTRL TabCtrl_1, TabCtrl_2;

I create instances of the tab control class in the WM_INITDIALOG message handler using the New_TabControl() function. Some things to keep in mind:

  1. The tabnames and dlgnames arrays must be null terminated.
  2. You supply the optional TabCtrl1_TabPages_OnSize() function discussed later in this article or NULL if not needed.
  3. The final argument, fStretch, is TRUE for a stretch-to-fit tab page and FALSE if you want to center the tab page on the control.
InitHandles (hwnd); // this line must be first!

//----This section must be second----//

static LPSTR tabnames[]= {"Tab Page 1", "Tab Page 2", "Tab Page 3", "Tab Page 4", 0};
static LPSTR dlgnames[]= {MAKEINTRESOURCE(TAB_CONTROL_1_PAGE_1),
                          MAKEINTRESOURCE(TAB_CONTROL_1_PAGE_2),
                          MAKEINTRESOURCE(TAB_CONTROL_1_PAGE_3),
                          MAKEINTRESOURCE(TAB_CONTROL_1_PAGE_4),0};

New_TabControl( &TabCtrl_1, // address of TabControl struct
                GetDlgItem(hwnd, TAB_CONTROL_1), // handle to tab control
                tabnames, // text for each tab
                dlgnames, // dialog ids of each tab page dialog
                &FormMain_DlgProc, //parent dialog proc
                &TabCtrl1_TabPages_OnSize, // optional address of size function or NULL
                TRUE); // stretch tab page to fit tab ctrl

I sort out the WM_NOTIFY messages for the tab control and route them to the class' internal OnKeyDown() and OnSelChanged() event handlers via function pointers.

static BOOL FormMain_OnNotify(HWND hwnd, INT id, LPNMHDR pnm)
{
     switch(id)
     {
          case TAB_CONTROL_1:
               return TabCtrl_1.Notify(pnm);

          case TAB_CONTROL_2:
               return TabCtrl_2.Notify(pnm);
          //
          // TODO: Add other control id case statements here. . .
          //
     }
     return FALSE;
}

Sizing is handled on two levels. First, there is the sizing of the tab control and its associated tab pages, i.e. dialogs. Second, there is the handling of the size and position of the child controls within the tab pages. When I handle the WM_SIZE message, I supply typical sizing code for the tab controls. Now look at the for loops beneath the MoveWindow() statements. I am using the functions StretchTabPage() and CenterTabPage() to keep the tab pages within the client area of their respective tab controls. No messy sizing code here. Smile | :) I use a Refresh macro one time only, right here in order to reduce flicker in the main dialog.

void FormMain_OnSize(HWND hwnd, UINT state, int cx, int cy)
{
     RECT  rc;
     GetClientRect(hwnd,&rc);

     MoveWindow(TabCtrl_1.hTab,0,0,
          (rc.right - rc.left)/2-4,rc.bottom - rc.top,FALSE);
     for(int i=0;i < TabCtrl_1.tabPageCount;i++)
          TabCtrl_1.StretchTabPage(TabCtrl_1.hTab,i);

     MoveWindow(TabCtrl_2.hTab,(rc.right - rc.left)/2,0,
          (rc.right - rc.left)/2-4,rc.bottom - rc.top,FALSE);
     for(int i=0;i < TabCtrl_2.tabPageCount;i++)
          TabCtrl_2.CenterTabPage(TabCtrl_2.hTab,i);

     Refresh(hwnd);
}

The handling of the size and position of child controls within tab pages is done in a separate function. It is necessary to clone the XXX_OnSize() function -- i.e. message cracker function -- and pass the address of that function to the New_TabControl() constructor that we discussed earlier. All tab page WM_SIZE messages are then sent to this handler. I am using if / else if statements to separate out the various tab pages and controls. All sizing on this level is relative to the client area of the parent tab control.

void TabCtrl1_TabPages_OnSize(HWND hwnd, UINT state, int cx, int cy)
{
    /////////////////////////////////////////////////////
    // Sizing and positioning of Tab Page Widgets shall
    // be handled here so that they remain consistent with
    // the tab page.
    // Remember that the hwnd changes with each successive tab page

     RECT  rc, chRc;
     int h, w;
     GetClientRect(hwnd, &rc);

     if(hwnd==TabCtrl_1.hTabPages[0])
     {
          GetWindowRect(GetDlgItem(hwnd,CMD_CLICK_1),&chRc);
          h=chRc.bottom-chRc.top;
          w=chRc.right-chRc.left;
          MoveWindow(GetDlgItem(hwnd,CMD_CLICK_1),
               rc.left+(rc.right-rc.left-w) / 2,
               rc.top+(rc.bottom-rc.top)/4-h/2,
               chRc.right - chRc.left,
               chRc.bottom - chRc.top,
               FALSE);

          GetWindowRect(GetDlgItem(hwnd,CMD_CLICK_2),&chRc);
          h=chRc.bottom-chRc.top;
          w=chRc.right-chRc.left;
          MoveWindow(GetDlgItem(hwnd,CMD_CLICK_2),
               rc.left+(rc.right-rc.left-w)/2,
               rc.top+(rc.bottom-rc.top)/2-h/2,
               chRc.right - chRc.left,chRc.bottom - chRc.top,
               FALSE);

          GetWindowRect(GetDlgItem(hwnd,CMD_CLICK_3),&chRc);
          h=chRc.bottom-chRc.top;
          w=chRc.right-chRc.left;
          MoveWindow(GetDlgItem(hwnd,CMD_CLICK_3),
               rc.left+(rc.right-rc.left-w)/2,
               rc.top+(rc.bottom-rc.top)/4*3-h/2,
               chRc.right - chRc.left,chRc.bottom - chRc.top,
               FALSE);
     }
     else if(hwnd==TabCtrl_1.hTabPages[1])
}

There is one function for housekeeping, TabControl_Destroy(), which releases the resources allocated to the tab page dialogs.

void FormMain_OnClose(HWND hwnd)
{
    PostQuitMessage(0);// turn off message loop

    TabControl_Destroy(&TabCtrl_1);
    TabControl_Destroy(&TabCtrl_2);
    EndDialog(hwnd, 0);
}

Points of Interest

Using GetClientRect() with the tab control does not always return the desired rectangle. It was necessary to calculate the client area from the Window Rectangle of the tab control.

In order to tab around inside of a tab page -- in reality, a child dialog -- it is necessary to have a message loop running for that page. However, leaving an active message loop running when navigating between tab pages or tab controls produces all kinds of weird behavior.

The solution is to create special-purpose message loop and keep track of the tab stops. I start the loop only when I enter the tab page via an arrow key and stop it when I exit that page after tabbing each of the tab stops.

Mouse clicks on a tab page or child control fire off a simulated WM_KEYDOWN event so that they might be handled in a manner consistent with the key presses. When the mouse clicks to another tab page, we post WM_SHOWWINDOW explicitly as an indicator to exit the loop.

static VOID TabPageMessageLoop(HWND hwnd)
{
     MSG msg;
     int status;
     BOOL handled = FALSE;
     BOOL fFirstStop = FALSE;
     HWND hFirstStop;

     while ((status = GetMessage(&msg, NULL, 0, 0)))
     {
          if (-1 == status)	// Exception
          {
               return;
          }
          else
          {
               //This message is explicitly posted from TabCtrl_OnSelChanged() to
               // indicate the closing of this page.  Stop the Loop.
               if (WM_SHOWWINDOW == msg.message && FALSE == msg.wParam)
                    return;

               //IsDialogMessage() dispatches WM_KEYDOWN to the tab page's child controls
               // so we'll sniff them out before they are translated and dispatched.
               if (WM_KEYDOWN == msg.message && VK_TAB == msg.wParam)
               {
                    //Tab each tabstop in a tab page once and then return to
                    // the tabCtl selected tab
                    if (!fFirstStop)
                    {
                         fFirstStop = TRUE;
                         hFirstStop = msg.hwnd;
                    }
                    else if (hFirstStop == msg.hwnd)
                    {
                         // Tab off the tab page
                         HWND hTab = (HWND)GetWindowLong
				(GetParent(msg.hwnd), GWL_USERDATA);
                         if(NULL == hTab) hTab = m_lptc->hTab;
                         SetFocus(hTab);
                         return;
                    }
               }
               // Perform default dialog message processing using IsDialogM. . .
               handled = IsDialogMessage(hwnd, &msg);

               // Non dialog message handled in the standard way.
               if (!handled)
               {
                    TranslateMessage(&msg);
                    DispatchMessage(&msg);
               }
          }
     }
     // If we get here window is closing
     PostQuitMessage(0);
     return;
}

Here, I navigate into a tab page with VK_LEFT:

VOID TabCtrl_OnKeyDown(LPARAM lParam)
{
     TC_KEYDOWN *tk = (TC_KEYDOWN *)lParam;
     int itemCount = TabCtrl_GetItemCount(tk->hdr.hwndFrom);
     int currentSel = TabCtrl_GetCurSel(tk->hdr.hwndFrom);

     if (itemCount <= 1)
          return;	// Ignore if only one TabPage

     BOOL verticalTabs = GetWindowLong(m_lptc->hTab, GWL_STYLE) & TCS_VERTICAL;

     if (verticalTabs)
     {
          switch (tk->wVKey)
          {
               //
               // A bunch of case statements (snipped)
               //
               case VK_LEFT:	//navigate within selected child tab page
               case VK_RIGHT:
                    SetFocus(m_lptc->hTabPages[currentSel]);
                    FirstTabstop_SetFocus(m_lptc->hTabPages[currentSel]);
                    TabPageMessageLoop(m_lptc->hTabPages[currentSel]);
                    break;

               default:
                    return;
          }
     }	// if(verticalTabs)

Here I enter by clicking a child control with the mouse. In addition to this, I forward all commands to the parent proc.

VOID TabPage_OnCommand(HWND hwnd, INT id, HWND hwndCtl, UINT codeNotify)
{
     //Forward all commands to the parent proc.
     //Note: We don't use SendMessage because on the receiving end,
     // that is: _OnCommand(HWND hwnd, INT id, HWND hwndCtl, UINT codeNotify),
     // hwnd would = addressee and not the parent of the control.  Intuition
     // favors the notion that hwnd is the parent of hwndCtl.
     FORWARD_WM_COMMAND(hwnd, id, hwndCtl, codeNotify, m_lptc->ParentProc);

     // If this WM_COMMAND message is a notification to parent window
     // i.e.: EN_SETFOCUS being sent when an edit control is initialized
     // do not engage the Message Loop or send any messages.
     if (codeNotify != 0)
          return;

     // Mouse clicks on a control should engage the Message Loop
     SetFocus(hwndCtl);
     FirstTabstop_SetFocus(hwnd);
     TabPageMessageLoop(hwnd);
}

Here I enter by clicking a tab page with the mouse and simulate a keyboard entry.

VOID TabPage_OnLButtonDown(HWND hwnd, BOOL fDoubleClick, INT x, INT y, UINT keyFlags)
{
     // If Mouse click in tab page but not on control simulate keypress access
     //  so that everything is handled in the same consistent way
     BOOL verticalTabs = GetWindowLong(m_lptc->hTab, GWL_STYLE) & TCS_VERTICAL;

     if (verticalTabs)
     {
          NMTCKEYDOWN nm = { m_lptc->hTab, GetDlgCtrlID(m_lptc->hTab), 
					TCN_KEYDOWN, VK_LEFT, 0 };
          FORWARD_WM_NOTIFY(nm.hdr.hwndFrom,nm.hdr.idFrom, &nm, SendMessage);
     }
     else
     {
          NMTCKEYDOWN nm = { m_lptc->hTab, GetDlgCtrlID(m_lptc->hTab), 
					TCN_KEYDOWN, VK_DOWN, 0 };
          FORWARD_WM_NOTIFY(nm.hdr.hwndFrom,nm.hdr.idFrom, &nm, SendMessage);
     }
}

Finally, whenever there is more than one message loop active and a quit message is posted, that quit message must be re-posted to ensure that any child dialog process is discontinued. This is why I have the PostQuitMessage(0) instruction at the end of TabPageMessageLoop().

History

  • July 5, 2006 version 1.0.0.0
  • May 5, 2007 version 2.0.0.0: Some refactoring and small bug fixes
  • July 22, 2007 version 2.1: Refactored to make code more C++-friendly and updated article code snippets to reflect the current code.
  • June 12, 2009 version 3.0: Refactored to make tabbing more natural and handle mouse clicks in a manner consistent with keyboard input and updated article code snippets to reflect the current code.

License

This article, along with any associated source code and files, is licensed under The Code Project Open License (CPOL)

Share

About the Author

David MacDermot

United States United States
No Biography provided

Comments and Discussions

 
GeneralMy vote of 5 Pinmemberikillmeba4-Mar-13 4:38 
QuestionSomething wrong... Pingroupggggfjeicfh3-Apr-12 21:48 
GeneralCompile error Pinmembermcodeproject334313-Feb-11 22:55 
GeneralRe: Compile error PinmemberDavid MacDermot14-Feb-11 4:55 
GeneralRe: Compile error Pinmembermcodeproject334314-Feb-11 22:16 
GeneralRe: Compile error PinmemberDavid MacDermot15-Feb-11 5:48 
GeneralRe: Compile error PinmemberRASPeter19-Apr-12 16:34 
GeneralUsing this project with Mingw Pinmemberjlemon10-Feb-11 11:33 
Generalvs 2008 compilation errors Pinmemberdheeraj1218-May-10 1:38 
GeneralRe: vs 2008 compilation errors PinmemberDavid MacDermot18-May-10 7:20 
GeneralFreezes on rapid button presses Pinmemberdpefqspkfdu117023-Oct-09 4:41 
GeneralRe: Freezes on rapid button presses PinmemberDavid MacDermot23-Oct-09 5:52 
GeneralRe: Freezes on rapid button presses Pinmemberdpefqspkfdu117030-Oct-09 5:12 
GeneralRe: Freezes on rapid button presses PinmemberWANGQuan14-Oct-10 20:10 
QuestionXP Look and Gray tabs? Pinmembertootall10-Oct-09 15:08 
AnswerRe: XP Look and Gray tabs? PinmemberDavid MacDermot14-Oct-09 12:14 
GeneralRe: XP Look and Gray tabs? Pinmembertootall15-Oct-09 16:29 
GeneralUseless and beginner code Pinmemberkilt20-Aug-09 1:17 
GeneralRe: Useless and beginner code PinmvpDavidCrow14-Sep-09 4:07 
GeneralRe: Useless and beginner code PinmemberDavid MacDermot14-Sep-09 7:25 
GeneralAnother Good Article on the tab control PinmemberDavid MacDermot1-Jul-09 5:26 
GeneralVersion Win32 SDK C Tab Control - with 1 Tab Control only Pinmembersklimkin21-Jun-09 22:47 
GeneralRe: Version Win32 SDK C Tab Control - with 1 Tab Control only PinmemberDavid MacDermot22-Jun-09 6:15