您的位置:首页 > 其它

Win32 SDK C Tab Control Made Easy

2013-02-14 13:37 645 查看


Win32 SDK C Tab Control Made Easy

By David
MacDermot, 18 Jun 2009




   4.57 (24 votes)
 
Download demo project - 114.79 KB

Download source - 6.56 KB


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.


 Collapse | Copy
Code
/***************************************************************************/
// 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:

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.


 Collapse | Copy
Code
/** 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:

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.


 Collapse | Copy
Code
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.


 Collapse | Copy
Code
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. :) I use a 
Refresh
 macro
one time only, right here in order to reduce flicker in the main dialog.


 Collapse | Copy
Code
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.


 Collapse | Copy
Code
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.


 Collapse | Copy
Code
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.


 Collapse | Copy
Code
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
:


 Collapse | Copy
Code
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.


 Collapse | Copy
Code
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.


 Collapse | Copy
Code
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)

转自:http://www.codeproject.com/Articles/14712/Win32-SDK-C-Tab-Control-Made-Easy
内容来自用户分享和网络整理,不保证内容的准确性,如有侵权内容,可联系管理员处理 点击这里给我发消息
标签: