Click here to Skip to main content
15,314,594 members
Articles / Programming Languages / C
Posted 7 Jul 2006

Tagged as


131 bookmarked

Win32 SDK C Tab Control Made Easy

Rate me:
Please Sign up or sign in to vote.
4.70/5 (35 votes)
18 Jun 2009CPOL5 min read
This article describes formatting a non-MFC based tab control
Screenshot - Win32_SDK_C_TabCtrlDemo_1.gif


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.


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

	// 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);


// Exported function prototypes

void New_TabControl(LPTABCTRL,
                    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};

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)
          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. :) 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;

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

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


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

               rc.left+(rc.right-rc.left-w) / 2,
               chRc.right - chRc.left,
               chRc.bottom -,

               chRc.right - chRc.left,chRc.bottom -,

               chRc.right - chRc.left,chRc.bottom -,
     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

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

               //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;
               // Perform default dialog message processing using IsDialogM. . .
               handled = IsDialogMessage(hwnd, &msg);

               // Non dialog message handled in the standard way.
               if (!handled)
     // If we get here window is closing

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:

     }	// 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)

     // Mouse clicks on a control should engage the Message Loop

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), 
          FORWARD_WM_NOTIFY(nm.hdr.hwndFrom,nm.hdr.idFrom, &nm, SendMessage);
          NMTCKEYDOWN nm = { m_lptc->hTab, GetDlgCtrlID(m_lptc->hTab), 
          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().


  • July 5, 2006 version
  • May 5, 2007 version 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.


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


About the Author

David MacDermot
United States United States
No Biography provided

Comments and Discussions

GeneralMy vote of 5 Pin
ikillmeba4-Mar-13 4:38
Memberikillmeba4-Mar-13 4:38 
QuestionSomething wrong... Pin
ggggfjeicfh3-Apr-12 21:48
Memberggggfjeicfh3-Apr-12 21:48 
GeneralCompile error Pin
mcodeproject334313-Feb-11 22:55
Membermcodeproject334313-Feb-11 22:55 
GeneralRe: Compile error Pin
David MacDermot14-Feb-11 4:55
MemberDavid MacDermot14-Feb-11 4:55 
GeneralRe: Compile error Pin
mcodeproject334314-Feb-11 22:16
Membermcodeproject334314-Feb-11 22:16 
GeneralRe: Compile error Pin
David MacDermot15-Feb-11 5:48
MemberDavid MacDermot15-Feb-11 5:48 
GeneralRe: Compile error Pin
RASPeter19-Apr-12 16:34
MemberRASPeter19-Apr-12 16:34 
GeneralUsing this project with Mingw Pin
jlemon10-Feb-11 11:33
Memberjlemon10-Feb-11 11:33 
GeneralRe: Using this project with Mingw - Got it working Pin
mykeapredko23-Nov-17 17:58
Membermykeapredko23-Nov-17 17:58 
Generalvs 2008 compilation errors Pin
dheeraj1218-May-10 1:38
Memberdheeraj1218-May-10 1:38 
GeneralRe: vs 2008 compilation errors Pin
David MacDermot18-May-10 7:20
MemberDavid MacDermot18-May-10 7:20 
GeneralFreezes on rapid button presses Pin
dpefqspkfdu117023-Oct-09 4:41
Memberdpefqspkfdu117023-Oct-09 4:41 
GeneralRe: Freezes on rapid button presses Pin
David MacDermot23-Oct-09 5:52
MemberDavid MacDermot23-Oct-09 5:52 
GeneralRe: Freezes on rapid button presses Pin
dpefqspkfdu117030-Oct-09 5:12
Memberdpefqspkfdu117030-Oct-09 5:12 
GeneralRe: Freezes on rapid button presses Pin
EnochW14-Oct-10 20:10
MemberEnochW14-Oct-10 20:10 
QuestionXP Look and Gray tabs? Pin
tootall10-Oct-09 15:08
Membertootall10-Oct-09 15:08 
AnswerRe: XP Look and Gray tabs? Pin
David MacDermot14-Oct-09 12:14
MemberDavid MacDermot14-Oct-09 12:14 
GeneralRe: XP Look and Gray tabs? Pin
tootall15-Oct-09 16:29
Membertootall15-Oct-09 16:29 
GeneralRe: XP Look and Gray tabs? Pin
lariona1-Dec-16 11:16
Memberlariona1-Dec-16 11:16 
GeneralUseless and beginner code Pin
kilt20-Aug-09 1:17
Memberkilt20-Aug-09 1:17 
GeneralRe: Useless and beginner code Pin
David Crow14-Sep-09 4:07
MemberDavid Crow14-Sep-09 4:07 
GeneralRe: Useless and beginner code Pin
David MacDermot14-Sep-09 7:25
MemberDavid MacDermot14-Sep-09 7:25 
GeneralAnother Good Article on the tab control Pin
David MacDermot1-Jul-09 5:26
MemberDavid MacDermot1-Jul-09 5:26 
GeneralVersion Win32 SDK C Tab Control - with 1 Tab Control only Pin
sklimkin21-Jun-09 22:47
Membersklimkin21-Jun-09 22:47 
GeneralRe: Version Win32 SDK C Tab Control - with 1 Tab Control only Pin
David MacDermot22-Jun-09 6:15
MemberDavid MacDermot22-Jun-09 6:15 

General General    News News    Suggestion Suggestion    Question Question    Bug Bug    Answer Answer    Joke Joke    Praise Praise    Rant Rant    Admin Admin   

Use Ctrl+Left/Right to switch messages, Ctrl+Up/Down to switch threads, Ctrl+Shift+Left/Right to switch pages.