// tabcontrol.cpp
#include <windows.h>
#include <commctrl.h>
typedef struct tabpage_s
{
HWND hWnd; // window handle of the controls tab page sub-window
} tabpage_t;
typedef struct tabcontrol_s
{
HWND hWnd; // window handle of the tab control
tabpage_t *pages; // mallocated array of pages in this tab control
int page_count; // number of pages in this tab control
int activepage_index; // index of the active page
WNDPROC ParentProc; // function pointer to Parent Dialog Proc
} tabcontrol_t;
// prototypes of exported functions
void *TabControl_New (HWND hTabControlWnd, WNDPROC ParentProc);
void TabControl_AddPage (void *tab_control, wchar_t *page_name, int dialog_id);
void TabControl_SelectTab (void *tab_control, int page_index);
HWND TabControl_GetItem (void *tab_control, int item_id);
void TabControl_Destroy (void *tab_control);
bool TabControl_Notify (NMHDR *notification);
// prototypes of local functions
static void TabControl_SetFocusOnFirstTabStop (HWND hWnd);
static void TabControl_GetClientRect (HWND hWnd, RECT *client_rect);
static int CALLBACK TabPage_DlgProc (HWND hwndDlg, unsigned int message, WPARAM wParam, LPARAM lParam);
static void TabPage_MessageLoop (HWND hWnd);
static void TabControl_SetFocusOnFirstTabStop (HWND hWnd)
{
// this function ensures focus on first tab stop when entering a tab page.
// WM_NEXTDLGCTL wparam = 1 shifts focus back to the LAST tabstop in this tabpage.
// Posting a VK_TAB returns focus to the first tab stop and causes it to be recognized
// as the first tabstop by the WM_KEYDOWN sniffer in the TabPageMessageLoop().
SendMessage (hWnd, WM_NEXTDLGCTL, 1, FALSE);
PostMessage (GetFocus (), WM_KEYDOWN, VK_TAB, 0);
return;
}
static void TabControl_GetClientRect (HWND hWnd, RECT *client_rect)
{
// returns the client rectangle for the tab control under every possible configuration
// (normal tabs, tabs with buttons, vertical tabs, etc.)
// IMPORTANT: the rectangle structure is populated as follows:
// left = left, top = top, right = WIDTH, bottom = HEIGHT
long tab_style;
RECT tab_rect;
tab_style = GetWindowLong (hWnd, GWL_STYLE);
// calculate the tab control's display area
GetWindowRect (hWnd, client_rect);
ScreenToClient (GetParent (hWnd), (POINT *) &client_rect->left);
ScreenToClient (hWnd, (POINT *) &client_rect->right);
TabCtrl_GetItemRect (hWnd, 0, &tab_rect); // the tab itself
// vertical tabs to the right ?
if ((tab_style & TCS_BOTTOM) && (tab_style & TCS_VERTICAL))
{
client_rect->top += 6; // x coord
client_rect->left += 4; // y coord
client_rect->bottom -= 12; // height
client_rect->right -= (12 + tab_rect.right - tab_rect.left); // width
}
// vertical tabs to the left ?
else if (tab_style & TCS_VERTICAL)
{
client_rect->top += 6; //x coord
client_rect->left += (4 + tab_rect.right - tab_rect.left); // y coord
client_rect->bottom -= 12; // height
client_rect->right -= (12 + tab_rect.right - tab_rect.left); // width
}
// horizontal tabs at the bottom ?
else if (tab_style & TCS_BOTTOM)
{
client_rect->top += 6; // x coord
client_rect->left += 4; // y coord
client_rect->bottom -= (16 + tab_rect.bottom - tab_rect.top); // height
client_rect->right -= 12; // width
}
// else it must be horizontal tabs at the top (default style)
else
{
client_rect->top += (6 + tab_rect.bottom - tab_rect.top); // x coord
client_rect->left += 4; // y coord
client_rect->bottom -= (16 + tab_rect.bottom - tab_rect.top); // height
client_rect->right -= 12; // width
}
return; // finished populating the rectangle
}
static int CALLBACK TabPage_DlgProc (HWND hwndDlg, unsigned int message, WPARAM wParam, LPARAM lParam)
{
// window procedure for tab page dialogs
tabcontrol_t *tab_control;
NMTCKEYDOWN nm;
HWND hWndTab;
// get a pointer to the tab container window
hWndTab = (HWND) GetWindowLong (hwndDlg, GWL_USERDATA);
if (hWndTab == NULL)
return (0); // consistency check
// now get a pointer to the tab container's structure
tab_control = (tabcontrol_t *) GetWindowLong (hWndTab, GWL_USERDATA);
// is this page being initialized ?
if (message == WM_INITDIALOG)
return (DefWindowProc (hwndDlg, WM_INITDIALOG, wParam, lParam)); // if so, don't hook its initialization message
// else is a control being acted upon in this page ?
else if (message == WM_COMMAND)
{
// forward all commands to the parent proc
tab_control->ParentProc (hwndDlg, message, wParam, lParam);
// is this WM_COMMAND message NOT a notification to the parent window ?
if (HIWORD (wParam) == 0)
{
// mouse clicks on a control should engage the Message Loop
SetFocus ((HWND) lParam);
TabControl_SetFocusOnFirstTabStop (hwndDlg);
TabPage_MessageLoop (hwndDlg);
}
SetWindowLongPtr (hwndDlg, DWLP_MSGRESULT, 0);
return (1); // return TRUE so as to notify we handled this message
}
// else is the mouse clicking something ?
else if (message == WM_LBUTTONDOWN)
{
// simulate keypress access so that everything is handled in the same consistent way
nm.hdr.hwndFrom = tab_control->hWnd;
nm.hdr.idFrom = GetDlgCtrlID (tab_control->hWnd);
nm.hdr.code = TCN_KEYDOWN;
nm.wVKey = (GetWindowLong (tab_control->hWnd, GWL_STYLE) & TCS_VERTICAL ? VK_LEFT : VK_DOWN);
nm.flags = 0;
SendMessage (nm.hdr.hwndFrom, WM_NOTIFY, nm.hdr.idFrom, (LPARAM) &nm);
SetWindowLongPtr (hwndDlg, DWLP_MSGRESULT, 0);
return (1); // return TRUE so as to notify we handled this message
}
// forward all other messages to the parent proc
tab_control->ParentProc (hwndDlg, message, wParam, lParam);
return (0);
}
/****************************************************************************
* PURPOSE: Monitor and respond to user keyboard input and system messages.
*
* PARAMS: HWND hwnd - handle to the currently visible tab page
*
* COMMENTS: Send PostQuitMessage(0); from any cancel or exit event.
* Failure to do so will leave the process running even after
* application exit.
\****************************************************************************/
static void TabPage_MessageLoop (HWND hWnd)
{
MSG msg;
int status;
HWND hFirstStop = NULL;
HWND hWndTab;
while ((status = GetMessage (&msg, NULL, 0, 0)))
{
if (status == -1)
return; // Exception
// if this page is being closed, stop the loop
if ((msg.message == WM_SHOWWINDOW) && (msg.wParam == FALSE))
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 ((msg.message == WM_KEYDOWN) && (msg.wParam == VK_TAB))
{
//Tab each tabstop in a tab page once and then return to to
// the tabCtl selected tab
if (hFirstStop == NULL)
hFirstStop = msg.hwnd;
else if (hFirstStop == msg.hwnd)
{
// Tab off the tab page and stop the loop
hWndTab = (HWND) GetWindowLong (GetParent (msg.hwnd), GWL_USERDATA);
if (hWndTab != NULL)
SetFocus (hWndTab); // tab off the tab page
return; // and stop the loop
}
}
// Perform default dialog message processing using IsDialogMessage...
// Non dialog message handled in the standard way.
if (!IsDialogMessage (hWnd, &msg))
{
TranslateMessage(&msg);
DispatchMessage(&msg);
}
}
// If we get here window is closing
PostQuitMessage(0);
return;
}
bool TabControl_Notify (NMHDR *pnm)
{
// handle WM_NOTIFY messages. Returns TRUE if message was handled.
tabcontrol_t *tab_control;
int currentpage_index;
TC_KEYDOWN *tk;
// get the tab control structure
tab_control = (tabcontrol_t *) GetWindowLong (pnm->hwndFrom, GWL_USERDATA);
// is it a "key down" notification ?
if (pnm->code == TCN_KEYDOWN)
{
// handle key presses in the tab control (but not the tab pages)
tk = (TC_KEYDOWN *) pnm;
// is there more than one tab ?
if (TabCtrl_GetItemCount (tk->hdr.hwndFrom) > 1)
{
currentpage_index = TabCtrl_GetCurSel (tk->hdr.hwndFrom);
// are tabs vertical ?
if (GetWindowLong (tab_control->hWnd, GWL_STYLE) & TCS_VERTICAL)
{
if ((tk->wVKey == VK_PRIOR) && (currentpage_index > 0))
{
TabCtrl_SetCurSel (tk->hdr.hwndFrom, currentpage_index - 1); // select the previous page
TabCtrl_SetCurFocus (tk->hdr.hwndFrom, currentpage_index - 1);
}
else if ((tk->wVKey == VK_UP) && (currentpage_index > 0))
{
TabCtrl_SetCurSel (tk->hdr.hwndFrom, currentpage_index - 1); // select the previous page
TabCtrl_SetCurFocus (tk->hdr.hwndFrom, currentpage_index);
}
else if (tk->wVKey == VK_NEXT)
{
TabCtrl_SetCurSel (tk->hdr.hwndFrom, currentpage_index + 1); // select the next page
TabCtrl_SetCurFocus (tk->hdr.hwndFrom, currentpage_index + 1);
}
else if (tk->wVKey == VK_DOWN)
{
TabCtrl_SetCurSel (tk->hdr.hwndFrom, currentpage_index + 1); // select the next page
TabCtrl_SetCurFocus (tk->hdr.hwndFrom, currentpage_index);
}
else if ((tk->wVKey == VK_LEFT) || (tk->wVKey == VK_RIGHT))
{
SetFocus (tab_control->pages[currentpage_index].hWnd); // navigate within selected child tab page
TabControl_SetFocusOnFirstTabStop (tab_control->pages[currentpage_index].hWnd);
TabPage_MessageLoop (tab_control->pages[currentpage_index].hWnd);
}
}
// else tabs must be horizontal (default style)
else
{
if ((tk->wVKey == VK_NEXT) && (currentpage_index > 0))
{
TabCtrl_SetCurSel (tk->hdr.hwndFrom, currentpage_index - 1); // select the previous page
TabCtrl_SetCurFocus (tk->hdr.hwndFrom, currentpage_index - 1);
}
else if ((tk->wVKey == VK_LEFT) && (currentpage_index > 0))
{
TabCtrl_SetCurSel (tk->hdr.hwndFrom, currentpage_index - 1); // select the previous page
TabCtrl_SetCurFocus (tk->hdr.hwndFrom, currentpage_index);
}
else if (tk->wVKey == VK_PRIOR)
{
TabCtrl_SetCurSel (tk->hdr.hwndFrom, currentpage_index + 1); // select the next page
TabCtrl_SetCurFocus (tk->hdr.hwndFrom, currentpage_index + 1);
}
else if (tk->wVKey == VK_RIGHT)
{
TabCtrl_SetCurSel (tk->hdr.hwndFrom, currentpage_index + 1); // select the next page
TabCtrl_SetCurFocus (tk->hdr.hwndFrom, currentpage_index);
}
else if ((tk->wVKey == VK_UP) || (tk->wVKey == VK_DOWN))
{
SetFocus (tab_control->pages[currentpage_index].hWnd); // navigate within selected child tab page
TabControl_SetFocusOnFirstTabStop (tab_control->pages[currentpage_index].hWnd);
TabPage_MessageLoop (tab_control->pages[currentpage_index].hWnd);
}
}
}
return (false); // we didn't handle the message, return FALSE
}
// else is it a tab page change notification ?
else if (pnm->code == TCN_SELCHANGE)
{
// handle TCN_SELCHANGE notification, which is sent when a tab has been pressed.
currentpage_index = TabCtrl_GetCurSel (tab_control->hWnd); // get the new current tab
ShowWindow (tab_control->pages[tab_control->activepage_index].hWnd, SW_HIDE); // hide the current child dialog box, if any
// ShowWindow() does not seem to post the WM_SHOWWINDOW message to the tab page. Since we use
// the hiding of the window as an indication to stop the message loop, let's post it explicitly
PostMessage (tab_control->pages[tab_control->activepage_index].hWnd, WM_SHOWWINDOW, FALSE, 0);
ShowWindow (tab_control->pages[currentpage_index].hWnd, SW_SHOWNORMAL); // show the new child dialog box
tab_control->activepage_index = currentpage_index; // save the current child
return (true); // message was handled, return TRUE
}
return (false); // we didn't handle the message, return FALSE
}
void *TabControl_New (HWND hTabControlWnd, WNDPROC ParentProc)
{
// initialize the hTabControlWnd control handle as a tab control, and return an opaque pointer to it
tabcontrol_t *the_control;
the_control = (tabcontrol_t *) malloc (sizeof (tabcontrol_t));
the_control->hWnd = hTabControlWnd; // save tab control's window handle
the_control->ParentProc = ParentProc; // save tab control parent's window procedure
the_control->pages = NULL;
the_control->page_count = 0;
the_control->activepage_index = -1;
SetWindowLong (the_control->hWnd, GWL_USERDATA, (LONG) the_control); // save tab control structure address to GWL_USERDATA
return (the_control); // tab control is created, return an opaque pointer to it
}
void TabControl_AddPage (void *tab_control, wchar_t *page_name, int dialog_id)
{
// add a page to a tab control from a dialog ID
tabcontrol_t *the_control;
RECT client_rect;
TCITEM tie;
the_control = (tabcontrol_t *) tab_control; // quick access to tab control
// get tab control's client rectangle
TabControl_GetClientRect (the_control->hWnd, &client_rect); // left, top, width, height
// resize pages array based on number of pages
the_control->pages = (tabpage_t *) realloc (the_control->pages, (the_control->page_count + 1) * sizeof (tabpage_t *));
// Add a tab for each name in tabnames (list ends with 0)
tie.mask = TCIF_TEXT | TCIF_IMAGE;
tie.iImage = -1;
tie.pszText = page_name;
TabCtrl_InsertItem (the_control->hWnd, the_control->page_count, &tie);
// create the page dialog with the tab parent's as parent, and save this tab's "real" parent, which is our tab container, in GWL_USERDATA
the_control->pages[the_control->page_count].hWnd = CreateDialog (GetModuleHandle (NULL), MAKEINTRESOURCE (dialog_id), GetParent (the_control->hWnd), (DLGPROC) TabPage_DlgProc);
SetWindowLong (the_control->pages[the_control->page_count].hWnd, GWL_USERDATA, (LONG) the_control->hWnd);
SetWindowPos (the_control->pages[the_control->page_count].hWnd, HWND_TOP, client_rect.left, client_rect.top, client_rect.right, client_rect.bottom, 0);
the_control->page_count++; // tab control has now one page more
// save the current visible page and show it
the_control->activepage_index = 0;
ShowWindow (the_control->pages[the_control->activepage_index].hWnd, SW_SHOW);
return; // finished, page has been added
}
void TabControl_SelectTab (void *tab_control, int page_index)
{
tabcontrol_t *the_control;
NMHDR nmh;
the_control = (tabcontrol_t *) tab_control; // quick access to tab control
TabCtrl_SetCurSel (the_control->hWnd, page_index);
nmh.hwndFrom = the_control->hWnd;
nmh.idFrom = GetDlgCtrlID (the_control->hWnd);
nmh.code = TCN_SELCHANGE;
SendMessage (the_control->hWnd, WM_NOTIFY, (WPARAM) nmh.idFrom, (LPARAM) &nmh);
return;
}
HWND TabControl_GetItem (void *tab_control, int item_id)
{
// helper function to set a tab control item's text
tabcontrol_t *the_control;
int page_index;
the_control = (tabcontrol_t *) tab_control; // quick access to tab control
// cycle through all pages and return the one which has the desired item
for (page_index = 0; page_index < the_control->page_count; page_index++)
if (IsWindow (GetDlgItem (the_control->pages[page_index].hWnd, item_id)))
return (GetDlgItem (the_control->pages[page_index].hWnd, item_id));
return (NULL); // this item was not found in tab control
}
void TabControl_Destroy (void *tab_control)
{
// destroy the tab page dialogs and free the list of pointers to the dialogs
tabcontrol_t *the_control;
int page_index;
the_control = (tabcontrol_t *) tab_control; // quick access to tab control
// destroy each window of this tab successively
for (page_index = 0; page_index < the_control->page_count; page_index++)
DestroyWindow (the_control->pages[page_index].hWnd);
free (the_control->pages);
free (the_control);
return; // finished, the tab control structure's allocated elements are freed
}