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:
- Tab pages should be designed as separate dialogs. These could then be hidden and shown during tab selection.
- All events received on controls should be handled in the parent dialog's callback procedure.
- Keyboard navigation of the tab control, as well as tab pages, must work.
- 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.
typedef struct TabControl
{
HWND hTab;
HWND hVisiblePage;
HWND* hTabPages;
LPSTR *tabNames;
int tabPageCount;
BOOL blStretchTabs;
BOOL (CALLBACK *ParentProc)(HWND, UINT, WPARAM, LPARAM);
void (*TabPage_OnSize)(HWND hwnd, UINT state, int cx, int cy);
BOOL (*Notify) (LPNMHDR);
BOOL (*StretchTabPage) (HWND, INT);
BOOL (*CenterTabPage) (HWND, INT);
}TABCTRL, *LPTABCTRL;
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);
#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:
- Placed and sized two tab controls.
- Created a dialog for each desired tab page. Note that dialogs should be borderless and sized to fit the tab control's client area.
- Declared an instance of the
TabControl struct
for each of my tab controls.
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:
- The
tabnames
and dlgnames
arrays must be null terminated. - You supply the optional
TabCtrl1_TabPages_OnSize()
function discussed later in this article or NULL
if not needed. - 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);
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, GetDlgItem(hwnd, TAB_CONTROL_1), tabnames, dlgnames, &FormMain_DlgProc, &TabCtrl1_TabPages_OnSize, TRUE);
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);
}
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;
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)
{
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);
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) {
return;
}
else
{
if (WM_SHOWWINDOW == msg.message && FALSE == msg.wParam)
return;
if (WM_KEYDOWN == msg.message && VK_TAB == msg.wParam)
{
if (!fFirstStop)
{
fFirstStop = TRUE;
hFirstStop = msg.hwnd;
}
else if (hFirstStop == msg.hwnd)
{
HWND hTab = (HWND)GetWindowLong
(GetParent(msg.hwnd), GWL_USERDATA);
if(NULL == hTab) hTab = m_lptc->hTab;
SetFocus(hTab);
return;
}
}
handled = IsDialogMessage(hwnd, &msg);
if (!handled)
{
TranslateMessage(&msg);
DispatchMessage(&msg);
}
}
}
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;
BOOL verticalTabs = GetWindowLong(m_lptc->hTab, GWL_STYLE) & TCS_VERTICAL;
if (verticalTabs)
{
switch (tk->wVKey)
{
case VK_LEFT: case VK_RIGHT:
SetFocus(m_lptc->hTabPages[currentSel]);
FirstTabstop_SetFocus(m_lptc->hTabPages[currentSel]);
TabPageMessageLoop(m_lptc->hTabPages[currentSel]);
break;
default:
return;
}
}
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_WM_COMMAND(hwnd, id, hwndCtl, codeNotify, m_lptc->ParentProc);
if (codeNotify != 0)
return;
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)
{
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.