Nancy Winnick Cluts
Microsoft Developer Network Technology Group
Created: June 24, 1994
Revised: November 3, 1994
New information regarding wizards under "Creating a Property Sheet" section.
New information in PSN_SETACTIVE under "Property Sheet Notifications" section; addition of new notifications: PSN_QUERYCANCEL, PSN_WIZBACK, PSN_WIZFINISH, and PSN_WIZNEXT.
Change to PROPSHEETPAGE code: LPFNRELEASEPROPSHEETPAGE pfnRelease changed to CALLBACK * pfnCallback.
Change to PROPSHEETHEADER code; addition to DWORD dwFlags of PSH_MULTILINETABS, PSH_WIZARD, PSH_USEPSTARTPAGE, PSH_NOAPPLYNOW, and PSH_USECALLBACK; and addition of LPSCTR pStartPage member.
New messages added under "Property Sheet Messages" section: PSM_GETTABCONTROL, PSM_PRESSBUTTON, PSM_SETCURSELID, PSM_SETFINISHTEXT, and PSM_SETWIZBUTTONS.
Revised: February 1995
Removal of PSN_HASHELP notification.
PSM_ADDPAGE and PSM_REMOVEPAGE now implemented.
Click to open or copy the files in the PROPS sample application for this technical article.
The next release of the Microsoft® Windows® operating system (called Windows 95) will present a new set of common controls to developers of Windows-based applications. These controls are provided in a new dynamic-link library (DLL) called COMCTL32.DLL. The controls allow developers to integrate existing applications into the new Windows 95 shell more thoroughly and seamlessly. COMCTL32.DLL is included with Windows 95 and will also be supported in Win32s® (running on Windows version 3.1) and in Windows NT™. Note that these controls are 32-bit only—they will not be supported in 16-bit Windows environments.
This article describes property sheets and one new common control (the tab control). It is the sixth (and final) article in a series of articles introducing the new common controls. The other articles in the series cover the following topics:
Parts 2–6 of the series have associated code samples that demonstrate the use of the Win32® common controls.
Warning The PROPS executable file associated with this article was built and tested using the Windows 95 Preliminary Development Kit. The executable will run only on Windows 95; it will not run under Windows 3.1 or Windows NT. If you have Windows 95 installed on your machine, but you have problems running this sample, copy the project files to your system using the button above, rebuild the project, and run the executable.
Please note that this article is based on preliminary information that is subject to change before the final version of Windows 95.
A tab control is similar to a notebook divider, which separates topics or sections of information and helps you access a particular topic or section easily. In the next release of the Microsoft® Windows® operating system (called Windows 95), the tab control and property sheet combination is based on the same design principle—the tab control allows the user to switch between property sheets containing information that the user can view and/or set. A property sheet contains one or more pages. An application typically defines a group of dialog boxes as property-sheet pages, and uses tab controls to provide access to a certain page of information. Tab controls can also be used to carry out a specific command instead of providing access to a page; this is similar to the way some menu names (usually followed by an exclamation point) provide immediate access to actions.
The dialog box in Figure 1 has tabs that switch between the Font, Alignment, and Number settings for formatting cells. Previous versions of Windows used individual dialog boxes to display each group of settings. In Windows 95, all the attributes are in one dialog box, and the user clicks the tab controls to switch from one set to the next. The Windows 95 shell makes extensive use of tabs in the Control Panel.
Figure 1. Tab controls in a property sheet
The table below lists the window styles that you can specify when creating tab controls. The default styles are TCS_TABS and TCS_SINGLELINE. This combination produces a tab control that looks like a notebook divider and includes a single line of left-justified text.
Table 1. Tab Control Styles
Style | Use |
TCS_BUTTONS | Creates a tab control that looks and works like a button. |
TCS_FIXEDWIDTH | Creates a tab control with a fixed width. |
TCS_FOCUSONBUTTONDOWN | Specifies that the tab will receive the input focus when the user clicks it. This style is used in conjunction with the TCS_BUTTONS style. |
TCS_MULTILINE | Allows multiple rows of tabs in the tab control. |
TCS_OWNERDRAWFIXED | Specifies an owner-drawn tab control. The parent window of the tab control is responsible for drawing the tab control. |
TCS_RAGGEDRIGHT | Specifies that tabs will not be stretched to fill the row. By default, the tab control will stretch each tab item (text, icon, or combination of text and icon displayed in the tab) equally to fill the tab control. |
TCS_RIGHTJUSTIFY | Right-justifies the text in the tab control. The text is left-justified by default. |
TCS_SHAREIMAGELISTS | Indicates that the tab control shares an image list. Because the image list is shared, it is not created when the tab control is created, or destroyed when the tab control is destroyed. |
TCS_SINGLELINE | Allows only a single row of tabs in the tab control. |
TCS_SORTASCENDING | Indicates that tabs are sorted in ascending order. |
TCS_SORTDESCENDING | Indicates that tabs are sorted in descending order. |
TCS_SORTNONE | Indicates that tabs are not sorted at all. |
TCS_TABS | Creates tabs that look like notebook dividers, and draws a border around the display area. |
TCS_TOOLTIPS | Supports ToolTips for the tab control window. |
You can create a tab control by calling the CreateWindow or CreateWindowEx function, specifying the WC_TABCONTROL window class and a combination of tab styles listed in Table 1 above. As with the other common controls, the WC_TABCONTROL window class is registered when the dynamic-link library for Win32® common controls (COMCTL32.DLL) is loaded. You can call the InitCommonControls function to ensure that the DLL is loaded.
However, to include tabs in the window, the application must also fill out the TC_ITEM or TC_ITEMHEADER structure. These two structures specify the attributes of the tabs. TC_ITEM and TC_ITEMHEADER are nearly identical—the only difference between the two structures is the lack of the lParam member in TC_ITEMHEADER. The lParam member is used for application-defined data. If the application wishes to store more than four extra bytes of data per tab within the tab item structure, lParam will not be sufficient. Instead, the application should define its own structure consisting of the TC_ITEMHEADER structure followed by application-defined data, then set the number of extra bytes per tab using the TCM_SETITEMEXTRA message. For example, if my application stored information about a baseball player for each tab, I would define a structure that looks something like this:
typedef struct _PLAYER_TAB {
TC_ITEMHEADER tci; // tab item information
LPSTR lpstrName; // player's name
LPSTR lpstrTeam; // player's team
LONG lERA; // player's ERA
LONG lSalary; // player's salary--should this be a float?
} PLAYER_TAB
After adding the tab, the application would send the TCM_SETITEMEXTRA message to set the amount of extra data to sizeof(PLAYER_TAB). If the application wanted to store a pointer to the structure above without including TC_ITEMHEADER in the structure, it could use the TC_ITEM structure instead and store the pointer to the structure in the lParam field.
The TC_ITEM structure contains information about a tab item. The structure is listed below.
typedef struct _TC_ITEM {
UINT mask;
UINT state;
UINT stateMask;
LPSTR pszText;
int cchTextMax;
int iImage;
LPARAM lParam;
} TC_ITEM;
The TC_ITEM structure contains the following members:
Table 2. Tab Masks
Mask | Meaning |
TCIF_TEXT | The pszText and cchTextMax members are valid. |
TCIF_IMAGE | The iImage member is valid. |
TCIF_PARAM | The lParam member is valid. |
TCIF_STATE | The state member is valid. |
Table 3. Tab State Flags
Flag | Meaning |
TCIS_FOCUSED | The tab has the input focus. |
TCIS_SELECTED | The tab is selected. |
TCIS_DROPHILITED | The tab is the drop highlight target. |
TCIS_DISABLED | The tab is disabled. |
TCIS_HIDDEN | The tab is hidden. |
Now that you've read the details and perused the structure, it's time to take a look at some simple code that fills out the TC_ITEM structure and create a tab within a tab control by calling the TabCtrl_InsertItem macro. The following code snippet creates a tab control that contains text and has no image list associated with it:
TC_ITEM tie;
tie.mask = TCIF_TEXT | TCIF_STATE | TCIF_IMAGE;
tie.state = 0;
tie.iImage = -1;
tie.pszText = "Tab 1";
if (TabCtrl_InsertItem(hwndTab, i, &tie) == -1)
{
// The insert failed--display an error box.
MessageBox(NULL, "TabCtrl_InsertItem failed!", NULL, MB_OK);
return NULL;
}
So far, you've created the tab control and inserted tab items, but the tab control still doesn't have much functionality. The application must now manage the window associated with the tabs. There are two ways to do this: the easy way and the hard way. The easy way is to use property sheets in conjunction with tabs, as described in the section on property sheets later in this article.
Here's how you add functionality the hard way: When the user selects a tab, the tab control sends a WM_COMMAND message, specifying the TCN_SELCHANGE notification, to manage the switch between logical pages of information. The application processes this notification and makes the appropriate changes to the focus window. For example, to create an edit control to use within each tab, the application would assign the memory handle (send an EM_SETHANDLE message to the edit control) for the incoming page. Although this method certainly works, a better way to handle paging between tabs is to let the system do the grunt work for you and to use property sheets instead.
Two notifications are sent to tab controls in the form of WM_COMMAND messages: TCN_SELCHANGING and TCN_SELCHANGE. TCN_SELCHANGING is sent to notify a tab control that the current selection is changing; TCN_SELCHANGE is sent to a tab control when the selection has changed. The tab control can disallow the selection from changing by returning NULL in response to the TCN_SELCHANGING notification.
You send messages to tab controls to do things such as add tabs, remove tabs, or change the tab control's appearance or behavior. As with the other common controls, each message has a corresponding macro that you can use instead of sending the message explicitly. This section lists the new messages that manipulate tab controls. In the macros listed below, HWND hwnd refers to the handle to the tab control, unless noted otherwise.
wParam = fLarger; \\ type of adjustment to perform
lParam = prc; \\ coordinates of the rectangle
Description: Given a rectangle, the TCM_ADJUSTRECT message calculates the display area of a tab control.
Parameters: If wParam (BOOL fLarger) is TRUE, prc is a display rectangle and will be filled in with the appropriate window rectangle. If it is FALSE, prc is a window rectangle and will be filled in with the appropriate display rectangle. lParam (RECT FAR * prc) is the address of a structure containing the original coordinates. This structure will also receive the new coordinates based on the adjusted calculation.
Return value: None.
Macro: (void)TabCtrl_AdjustRect(hwnd, fLarger, prc);
wParam = 0; \\ not used
lParam = 0; \\ not used
Description: The TCM_DELETEALLITEMS message removes all tabs from a tab control.
Parameters: wParam and lParam are not used.
Return value: TRUE if successful; FALSE otherwise.
Macro: (BOOL)TabCtrl_DeleteAllItems(hwnd, i);
wParam = i; \\ index of the tab to delete
lParam = 0; \\ not used
Description: The TCM_DELETEITEM message removes a tab from a tab control.
Parameters: wParam (int i) is the index of the tab to delete. lParam is not used.
Return value: TRUE if successful; FALSE otherwise.
Macro: (BOOL)TabCtrl_DeleteItem(hwnd, i);
wParam = 0; \\ not used
lParam = 0; \\ not used
Description: The TCM_GETBKCOLOR message retrieves the background color of the tab control.
Parameters: wParam and lParam are not used.
Return value: The background color of the window (COLORREF).
Macro: COLORREF TabCtrl_GetBkColor(hwnd);
wParam = 0; \\ not used
lParam = 0; \\ not used
Description: The TCM_GETCURSEL message gets the tab that is currently selected in the tab control.
Parameters: wParam and lParam are not used.
Return value: The index of the selected tab if successful; –1 otherwise.
Macro: int TabCtrlView_GetCurSel(hwnd);
wParam = 0; \\ not used
lParam = 0; \\ not used
Description: The TCM_GETIMAGELIST message gets the handle of an image list used to draw the tab control.
Parameters: wParam and lParam are not used.
Return value: The handle of the specified image list if successful; NULL otherwise.
Macro: HIMAGELIST TabCtrlView_GetImageList(hwnd);
wParam = iItem; \\ index of the tab
lParam = (TC_ITEM FAR *)pitem; \\ structure receiving tab information
Description: The TCM_GETITEM message gets some or all of a tab's attributes.
Parameters: wParam is the index of the tab. lParam (TC_ITEM FAR * pitem) is the tab structure to fill in. The mask member specifies the attributes to get. If the mask member specifies the TCIF_TEXT flag, the pszText member must contain the address of the buffer that receives the tab text, and the cchTextMax member must specify the size of the buffer. If the mask member specifies the TCIF_STATE flag, the stateMask member specifies which tab states are to be returned.
Return value: TRUE if successful; FALSE otherwise.
Macro: (BOOL)TabCtrl_GetItem(hwnd, iItem, pitem);
wParam = 0; \\ not used
lParam = 0; \\ not used
Description: The TCM_GETITEMCOUNT message gets the number of tabs in a tab control.
Parameters: wParam and lParam are not used.
Return value: The number of tabs.
Macro: (int)TabCtrl_GetItemCount(hwnd);
wParam = i; \\ index of the tab
lParam = (RECT FAR *)prc; \\ bounding rectangle
Description: The TCM_GETITEMRECT message gets the bounding rectangle for a tab in the tab control.
Parameters: wParam (int i) is the index of the tab. lParam (RECT FAR * prc) is the bounding rectangle.
Return value: TRUE if successful; FALSE otherwise.
Macro: (BOOL)TabCtrl_GetItemRect(hwnd, i, prc);
wParam = 0; \\ not used
lParam = 0; \\ not used
Description: The TCM_GETROWCOUNT message gets the number of rows in a tab control. This message is valid only for tab controls that have the TCS_MULTILINE style.
Parameters: wParam and lParam are not used.
Return value: The number of rows.
Macro: (int)TabCtrl_GetRowCount(hwnd);
wParam = 0; \\ not used
lParam = 0; \\ not used
Description: The TCM_GETTEXTBKCOLOR message gets the background text color in a tab control.
Parameters: wParam and lParam are not used.
Return value: The background color of the text (COLORREF).
Macro: (COLORREF)TabCtrl_GetTextBkColor(hwnd);
wParam = 0; \\ not used
lParam = 0; \\ not used
Description: The TCM_GETTEXTCOLOR message gets the color of the text in a tab control.
Parameters: wParam and lParam are not used.
Return value: The text color (COLORREF).
Macro: (COLORREF)TabCtrl_GetTextColor(hwnd);
wParam = 0; \\ not used
lParam = 0; \\ not used
Description: The TCM_GETTOOLTIPS message gets the handle for the ToolTip control.
Parameters: wParam and lParam are not used.
Return value: The handle of the ToolTip control.
Macro: (HWND)TabCtrl_GetToolTips(hwnd);
wParam = 0; \\ not used
lParam = (TC_HITTESTINFO FAR *)pinfo; \\ position to hit test
Description: The TCM_HITTEST message determines which tab is at a specified position in the tab control.
Parameters: wParam is not used. lParam (TC_HITTESTINFO FAR * pinfo) specifies the structure containing the position to hit test. The pt member is the position to hit test in client coordinates. The flags member specifies the results of the hit test. The possible flags are listed in Table 4, below.
Table 4. Tab Hit Test Flags
Flag | Meaning |
TCHT_NOWHERE | The position is inside the tab control's client area but not over the tab. |
TCHT_ONITEMICON | The position is over the tab's icon. |
TCHT_ONITEMLABEL | The position is over the tab's text. |
TCHT_ONITEM | The position is over the tab's icon or text. This is a combination of the TCHT_ONITEMICON and TCHT_ONITEMLABEL flags. |
Return value: The index of the tab at the specified position; –1 otherwise.
Macro: (int)TabCtrl_HitTest(hwnd, pinfo);
wParam = iItem; \\ index of the tab
lParam = (const TC_ITEM FAR *)pitem; \\ new tab information
Description: The TCM_INSERTITEM message inserts a new tab in a tab control.
Parameters: wParam is the index of the new tab. lParam (const TC_ITEM FAR * pitem) specifies the attributes of the new tab.
Return value: The index of the new tab if successful; –1 otherwise.
Macro: (int)TabCtrl_InsertItem(hwnd, iItem, pitem);
wParam = iIndex; \\ the index of the image
lParam = 0; \\ not used
Description: The TCM_REMOVEIMAGE message removes an image from a tab control's image list. The tab control will update the image index for each tab so that each tab remains associated with its correct image.
Parameters: wParam (int i) is the index of the image. lParam is not used.
Return value: TRUE if successful; FALSE otherwise.
Macro: (BOOL)TabCtrl_RemoveImage(hwnd, i);
wParam = 0; \\ not used
lParam = (COLORREF)clrBk; \\ background color to set
Description: The TCM_SETBKCOLOR message sets the background color of the tab control.
Parameters: wParam is not used. lParam (COLORREF clrBk) specifies the background color to set (CLR_NONE for no background color).
Return value: TRUE if successful; FALSE otherwise.
Macro: (BOOL)TabCtrl_SetBkColor(hwnd, clrBk);
wParam = i; \\ index of the tab
lParam = 0; \\ not used
Description: The TCM_SETCURSEL message selects the specified tab.
Parameters: wParam (int i) is the index of the tab to select. lParam is not used.
Return value: The index of the selected tab if successful; NULL otherwise.
Macro: int TabCtrlView_SetCurSel(hwnd, i);
wParam = 0; \\ not used
lParam = (HIMAGELIST) himl; \\ handle of the image list
Description: The TCM_SETIMAGELIST message sets the image list used to draw the tab control.
Parameters: wParam is not used. lParam (HIMAGELIST himl) is the handle to the image list.
Return value: TRUE if successful; FALSE otherwise.
Macro: (BOOL)TabCtrl_SetImageList(hwnd, himl);
wParam = iItem; \\ index of the tab
lParam = (const TC_ITEM FAR *)pitem; \\ new tab attributes
Description: The TCM_SETITEM message sets a tab's attributes.
Parameters: wParam is the index of the tab in the tab control. lParam (const TC_ITEM FAR * pitem) specifies the structure that contains the new tab attributes. The mask member specifies the attributes to get. If the mask member specifies the TCIF_TEXT flag, the pszText member specifies the address of a null-terminated string, and the cchTextMax member is ignored. If the mask member specifies the TCIF_STATE flag, the stateMask member specifies which tab states are to be changed, and the state member contains the new values for those states.
Return value: TRUE if successful; FALSE otherwise.
Macro: (BOOL)TabCtrl_SetItem(hwnd, iItem, pitem);
wParam = cb; \\ the number of extra bytes needed
lParam = 0; \\ not used
Description: The TCM_SETITEMEXTRA message sets the number of extra bytes for a tab.
Parameters: wParam is the number of extra bytes for each tab. lParam is not used.
Return value: TRUE if successful; FALSE otherwise.
Macro: (BOOL)TabCtrl_SetItemExtra(hwnd, cb);
wParam = 0; \\ not used
lParam = MAKELPARAM(x,y); \\ the new size for the tab
Description: The TCM_SETITEMCOUNT message sets the size of a tab control.
Parameters: wParam is not used. lParam (MAKELPARAM (x, y)) is the new width and height for the tab control.
Return value: The old width and height of the tab control.
Macro: (DWORD)TabCtrl_SetItemSize(hwnd, x, y);
wParam = 0; \\ not used
lParam = MAKELPARAM(cx, cy); \\ not used
Description: The TCM_SETPADDING message sets the amount of space between the edges of the tab control and its associated text or image.
Parameters: wParam is not used. lParam (MAKELPARAM( cx, cy)) is the amount of padding between the tab control and its associated text or image.
Return value: None.
Macro: (void)TabCtrl_SetPadding(hwnd, cx, cy);
wParam = 0; \\ not used
lParam = (COLORREF)clrText; \\ new background text color
Description: The TCM_SETTEXTBKCOLOR message sets the background text color of a tab control.
Parameters: wParam is not used. lParam (COLORREF clrText) is the new background text color.
Return value: TRUE if successful; FALSE otherwise.
Macro: (BOOL)TabCtrl_SetTextBkColor(hwnd, clrText);
wParam = 0; \\ not used
lParam = (COLORREF)clrText; \\ new text color
Description: The TCM_SETTEXTCOLOR message sets the text color in a tab control.
Parameters: wParam is not used. lParam (COLORREF clrText) is the new text color.
Return value: TRUE if successful; FALSE otherwise.
Macro: (BOOL)TabCtrl_SetTextColor(hwnd, clrText);
wParam = hwndTT; \\ handle of a ToolTip control
lParam = 0; \\ not used
Description: The TCM_SETTOOLTIPS message sets the handle for the ToolTip control.
Parameters: wParam (HWND hwndTT) is the handle to the ToolTip control. lParam is not used.
Return value: None.
Macro: (void)TabCtrl_SetToolTips(hwnd, hwndTT);
A property sheet, also known as a tabbed dialog, is a window that allows the user to view and edit the properties of an item or an object. For example, a spreadsheet application can use a property sheet to allow the user to set the font and border properties of a cell. Property sheets are used extensively within the new Windows 95 shell to display and change the properties of shell items and the desktop. Application developers who want to give their software a consistent look and feel with the new shell are encouraged to use property sheets.
Figure 2 illustrates a property sheet that allows the user to view and change the properties of a slider. Each group of properties is marked by a tab. Each tab has a separate sheet (called a page) that the user can access to view and change the associated properties. The property sheet in Figure 2 has two tabs: Slider Range and Slider Page and Line Size. The user selects a tab to bring the associated page to the foreground of the property sheet and to change the values.
Figure 2. An example of a property sheet
A property sheet and the pages it contains are actually dialog boxes. The property sheet is a system-defined modeless dialog box, and each page is an application-defined modeless dialog box. The property sheet is a container for the pages and is responsible for managing the pages. It includes a frame, a title bar, a system menu, and four buttons: OK, Cancel, Apply Now, and (optionally) Help. A property sheet must have at least one page and may have up to 24 pages.
Each page manages its own control windows (that is, edit controls and list box controls) much like a typical dialog box. The application provides a dialog box template and dialog procedure for each page. Each page in a property sheet has a label and (optionally) an icon. When the property sheet creates the tab for the page, it displays the label and icon in the tab. If the property sheet has only one page, the tab for the page is not displayed. All property sheet pages should use a non-bold font (the dialog box template should specify the DS_NOBOLD style). If you want to give your property sheet a three-dimensional look, add the DS_3DLOOK style to the dialog box template.
Note At the time that this article was written, the DS_NOBOLD and DS_3DLOOK styles were not supported in the tools provided with Microsoft Visual C++™.
Wizard controls are based on property sheets and are designed to make it easier for the developer to create wizards. For detailed information about wizard controls, see "We're Off to See the Wizard" in the MSDN Library.
You use the PropertySheet function to create a property sheet. If this function is successful, it returns a handle to the property sheet and automatically sets the size and initial position of the property sheet. The function bases the initial position of the property sheet on the largest page specified in the array of pages and the owner window's size and position. You can force the pages to match the width of the four buttons at the bottom of the property sheet by setting the width of the widest page to 190 dialog units.
To create a property sheet, you should define an array of PROPSHEETPAGE structures for each page, then fill out a PROPSHEETHEADER structure and call the PropertySheet function. This function will create handles for the pages before adding the pages to the property sheet. The order of the array determines the order of the pages in the property sheet, so be sure to define the pages in the array in the order that you want them shown in the tabs.
Once a property sheet has been created, an application can add and remove pages dynamically by sending the PSM_ADDPAGE and PSM_REMOVEPAGE messages or their corresponding macros. By default, when a property sheet is destroyed, all of its pages are destroyed in first in, last out (FILO) order. That is, the last page that the application specified in the array of pages will be the first page destroyed.
When creating a page for a wizard, developers are encouraged to use the built-in constants specifying the size of the page:
The first thing I did when I decided to use property sheets was to take an existing sample and use its dialog boxes for the pages in my property sheet. I made three major changes to my dialog templates:
I also took this opportunity to review some of my dialog boxes and decided that I could use two pages in place of my original four dialog boxes. This added a little extra work to the conversion, but it improved the organization of my pages and gave my sample a more polished look.
Next, I created the pages in my property sheet and the property sheet itself. I created a function that fills out a PROPSHEETPAGE structure for the two pages, fills out the PROPSHEETHEADER structure, and calls the PropertySheet function. I replaced the DialogBox function calls in my code with a call to my CreatePropertySheet function, which creates the property sheet. I used the code below to create the property sheet and its pages.
int CreatePropertySheet(HWND hwndOwner)
{
PROPSHEETPAGE psp[2];
PROPSHEETHEADER psh;
psp[0].dwSize = sizeof(PROPSHEETPAGE);
psp[0].dwFlags = PSP_USETITLE;
psp[0].hInstance = hInst;
psp[0].pszTemplate = MAKEINTRESOURCE(IDD_RANGE);
psp[0].pszIcon = NULL;
psp[0].pfnDlgProc = Range;
psp[0].pszTitle = "Slider Range";
psp[0].lParam = 0;
psp[1].dwSize = sizeof(PROPSHEETPAGE);
psp[1].dwFlags = PSP_USETITLE;
psp[1].hInstance = hInst;
psp[1].pszTemplate = MAKEINTRESOURCE(IDD_PROPS);
psp[1].pszIcon = NULL;
psp[1].pfnDlgProc = PageSize;
psp[1].pszTitle = "Slider Page and Line Size";
psp[1].lParam = 0;
psh.dwSize = sizeof(PROPSHEETHEADER);
psh.dwFlags = PSH_PROPSHEETPAGE;
psh.hwndParent = hwndOwner;
psh.hInstance = hInst;
psh.pszIcon = NULL;
psh.pszCaption = (LPSTR) "Slider Properties";
psh.nPages = sizeof(psp) / sizeof(PROPSHEETPAGE);
psh.ppsp = (LPCPROPSHEETPAGE) &psp;
return (PropertySheet(&psh));
}
When you convert a dialog procedure from managing a dialog box to managing a property sheet page, the major changes that you need to make involve the handling of the OK and Cancel buttons. Typically, a dialog box procedure is notified that the OK or Cancel button has been pressed via a WM_COMMAND message. When the dialog procedure gets this message, it generally verifies the information entered into the dialog box controls and calls the EndDialog function to destroy the dialog box. The following code demonstrates how a typical dialog box procedure manages the OK button:
case WM_COMMAND:
if (LOWORD(wParam) == IDOK)
{
uMin = GetDlgItemInt(hDlg, IDE_MIN, &bErr, TRUE);
uMax = GetDlgItemInt(hDlg, IDE_MAX, &bErr, TRUE);
SendMessage( hWndCurrent, TBM_SETRANGE, TRUE,
MAKELONG(uMax,uMin));
EndDialog(hDlg, TRUE);
return (TRUE);
}
break;
In a property sheet, the OK and Cancel notifications are no longer sent to the dialog box procedure. Instead, the dialog procedure must handle a group of page notifications. Some of these notifications require that the dialog procedure set the DWL_MSGRESULT window byte to either TRUE or FALSE. See the "Property Sheet Notifications" section later in this article for details on each of the notifications.
My application needed to handle the following notifications:
Initially, I found it difficult to differentiate between the OK and Apply Now buttons. They both require that the page validate and apply the changes the user has made. The only difference is that pressing OK causes the property sheet to be destroyed after the changes are applied—pressing Apply Now does not. As a result, if the user decides to apply some changes and later chooses to cancel out of the property sheet, the application should reset the properties to their initial values rather than saving the applied values.
Another change you must make when you convert a dialog procedure from handling a dialog box to handling a property sheet page is to remove the EndDialog call. The EndDialog function is not called for a property sheet page because it destroys the entire property sheet instead of destroying only the page.
When a page is created, the dialog procedure for the page receives a WM_INITDIALOG message (as it does when a dialog box is created); however, the lParam parameter points to the PROPSHEETPAGE structure used to create the page. The dialog procedure may save the pointer to this structure and use it later to modify the page. Although I did not modify any pages in my sample, I went ahead and saved this pointer in case I needed it later. (Obviously, I would have removed the pointer information from production code.)
I'm glad you asked. I used the following code to manage a property sheet page that sets the range in a slider:
BOOL APIENTRY Range(
HWND hDlg,
UINT message,
UINT wParam,
LONG lParam)
{
static PROPSHEETPAGE * ps;
BOOL bErr;
static UINT uMin, uMax, uMinSave, uMaxSave;
switch (message)
{
case WM_INITDIALOG:
// Save the PROPSHEETPAGE information.
ps = (PROPSHEETPAGE *)lParam;
return (TRUE);
case WM_NOTIFY:
switch (((NMHDR FAR *) lParam)->code)
{
case PSN_SETACTIVE:
// Initialize the controls.
uMinSave = SendMessage( hWndSlider, TBM_GETRANGEMIN, 0L,
0L);
uMaxSave = SendMessage( hWndSlider, TBM_GETRANGEMAX, 0L,
0L);
SetDlgItemInt(hDlg, IDE_MIN, uMinSave, TRUE);
SetDlgItemInt(hDlg, IDE_MAX, uMaxSave, TRUE);
break;
case PSN_APPLY:
uMin = GetDlgItemInt(hDlg, IDE_MIN, &bErr, TRUE);
uMax = GetDlgItemInt(hDlg, IDE_MAX, &bErr, TRUE);
SendMessage( hWndSlider, TBM_SETRANGE, TRUE,
MAKELONG(uMin, uMax));
SetWindowLong(hDlg, DWL_MSGRESULT, TRUE);
break;
case PSN_KILLACTIVE:
SetWindowLong(hDlg, DWL_MSGRESULT, FALSE);
return 1;
break;
case PSN_RESET:
// Reset to the original values.
SendMessage( hWndSlider, TBM_SETRANGE, TRUE,
MAKELONG(uMinSave, uMaxSave));
SetWindowLong(hDlg, DWL_MSGRESULT, FALSE);
break;
}
}
return (FALSE);
}
You can write a single piece of code that works both in a property sheet and in a dialog box, but this is not as easy as having dedicated code for each. If you are using shared code, follow these guidelines:
If you happen to use a single template for all of the pages in your property sheet, the user may encounter annoying screen flashes when switching between pages. Your application can minimize or eliminate these flashes by responding to the WM_SHOWWINDOW message. The following code snippet demonstrates one method of eliminating the screen flash:
case WM_SHOWWINDOW:
// Check if the window is being shown via a ShowWindow.
if (wParam && !LOWORD(lParam))
// It is, so post a message to myself.
PostMessage(hDlg, WM_APP, 0, 0L);
break;
case WM_APP:
// Remove the rectangle for the page from the invalid list.
ValidateRect(hDlg, NULL);
// Invalidate any and all controls within the page.
InvalidateRect(GetDlgItem(hDlg, ID_CONTROL1), NULL, FALSE);
InvalidateRect(GetDlgItem(hDlg, ID_CONTROL2), NULL, FALSE);
.
.
.
InvalidateRect(GetDlgItem(hDlg, ID_CONTROLn), NULL, FALSE);
break;
An application that uses this method repaints only the controls that need repainting inside the page, instead of repainting the whole window when the WM_SHOWWINDOW message is sent. A page will also need to call InvalidateRect with the fErase parameter set to TRUE for controls that do not completely paint their client area during a WM_PAINT message (for example, for a list box that is not full).
You can use pages from one property sheet to extend the functionality of another property sheet. That is, a property sheet can use pages created by another property sheet. Pretty cool, eh? This extension resides in a dynamic-link library (DLL). For the extension to work, the original module that created the property sheet must include the AddPropSheetPageProc callback function that the extension DLL calls to add its pages to the property sheet. The extension DLL must include the ExtensionPropSheetPageProc callback function to get the address of the AddPropSheetPageProc function from the module that created the original property sheet. The extension DLL must also export the ExtensionPropSheetPageProc callback function.
The property sheet extensions topic really deserves its own article—adding the information to this article would likely cause information overload, so I will not go into details here. If you would like to see another article covering property sheet extensions, let me know.
A property sheet sends notification messages to the dialog procedure for a page when the page gains or loses the activation, and when the user selects the OK, Cancel, Apply Now, or Help button. The notifications are sent in the form of WM_NOTIFY messages. The lParam member is a pointer to an NMHDR structure describing the notification. The hwndFrom member contains the window handle of the property sheet, and the hwndTo member contains the window handle of the page.
Some notifications require the dialog procedure to return either TRUE or FALSE in response to the WM_NOTIFY message. For example, the PSN_HASHELP notification requires a response of TRUE if it supports the Help button; otherwise, the button is disabled. The return value from the dialog procedure must be set using the SetWindowLong function rather than simply returning TRUE or FALSE. The value that is to be returned is set in the DWL_MSGRESULT window attribute as follows:
SetWindowLong(hDlg, DWL_MSGRESULT, value);
This notification is sent to the dialog procedure for the page when the user clicks the OK or Apply Now button. PSN_APPLY indicates that the user wants the current changes to take effect. When the dialog procedure for the page receives this notification, it should set the DWL_MSGRESULT window attribute for the page to TRUE if it could not process this message and wishes focus to change to the page. When you convert an existing dialog box into a property sheet page, handle the OK button when you process the PSN_APPLY notification, but remember: Don't call the EndDialog function. Note that the current page will receive the PSN_KILLACTIVE notification before it receives PSN_APPLY, and set the DWL_MSGRESULT window attribute for the page to FALSE for the PSN_APPLY notification to be sent.
This notification is sent to the dialog procedure for the page when the user clicks the Help button. The page should call WinHelp or display help text in whatever way is appropriate when it receives this message.
This notification is sent to the dialog procedure for the page when the page is about to lose activation because the user pressed the OK or Apply Now button. The page should do validation at this point and set the DWL_MSGRESULT window attribute for the page to TRUE if it should not lose activation at this time (this should be accompanied by a message box to the user describing the problem). The page should set DWL_MSGRESULT to FALSE when it is okay to lose the activation. In this case, the property sheet sends the PSN_APPLY notification to each page, directing the page to apply the new properties to the corresponding item. If the page determines that the user's changes are not valid, it should set DWL_MSGRESULT to TRUE and display a dialog box informing the user of the problem. The page remains active until it sets DWL_MSGRESULT to FALSE in response to a PSN_KILLACTIVE message. An application can also use the PSM_APPLY message to simulate clicking the Apply Now button.
This notification is sent to the dialog procedure for a page when the Cancel button has been clicked. The page can reject a cancel by setting the notification result to a nonzero value.
This notification is sent to the dialog procedure for a page when the user clicks the Cancel button and wishes to cancel all changes since the last Apply Now command. When you convert an existing dialog into a property sheet page, handle the Cancel button when you process the PSN_RESET notification, with one major exception: Do not call the EndDialog function. A page should take this opportunity to perform clean-up operations.
This notification is sent to the dialog procedure for a page when the page is about to become the active page. The page should take this opportunity to perform any initializing it needs at this time. Note that the PSN_SETACTIVE message is sent before the page is visible to reduce screen flashing. A property sheet can have only one active page at a time, and the page that has the activation is at the foreground of the overlapping stack of pages. The user activates a page by selecting its tab; an application activates a page by using the PSM_SETCURSEL message. The page receiving the activation notification can prevent itself from being activated and set the activation to another page. If the notification result is set to –1, the next or previous page (depending upon the button that was clicked) will be activated. To set the activation to a specific page, the notification result is set to the ID of the dialog box to be activated.
This notification is sent to the dialog procedure for a wizard page when the Back button is clicked. The page can prevent the action by setting the notification result to –1. The page can branch activation to another page that is out of order by setting the notification result to the ID of the dialog box to be activated.
This notification is sent to the dialog procedure for a wizard page when the Finish button is clicked. The page can prevent the action by setting the notification result to a nonzero value.
This notification is sent to the dialog procedure for a wizard page when the Next button is clicked. The page can prevent the action by setting the notification result to –1. The page can branch activation to another page that is out of order by setting the notification result to the ID of the dialog box to be activated.
Two new structures that support property sheets have been defined in Windows: PROPSHEETPAGE and PROPSHEETHEADER. These structures are used in conjunction with the new property sheet notifications and messages. These two new structures are described below.
typedef struct _PROPSHEETPAGE {
DWORD dwSize;
DWORD dwFlags;
HINSTANCE hInstance;
union {
LPCSTR pszTemplate;
LPCDLGTEMPLATE pResource;
};
union {
HICON hIcon;
LPCSTR pszIcon;
};
LPCSTR pszTitle;
DLGPROC pfnDlgProc;
LPARAM lParam;
CALLBACK * pfnCallback;
UINT FAR * pcRefParent;
} PROPSHEETPAGE, FAR *LPPROPSHEETPAGE;
The PROPSHEETPAGE structure describes a page in a property sheet. When the page is first created, the lParam of the WM_INITDIALOG message points to this structure. By default, a property sheet uses the name string specified in the dialog box template as the label for a page. You can override the name string by including the PSP_USETITLE value in the dwFlags member of the PROPSHEETPAGE structure that defines the page. When PSP_USETITLE is specified, the pszTitle member must contain the address of the label string for the page. The PROPSHEETPAGE structure has the following members:
PSP_DEFAULT: Use the default behavior and meaning of the fields.
PSP_DLGINDIRECT: Use pResource instead of pszTemplate in the first union, which means that the template is supplied in memory, not loaded from a resource.
PSP_USEHICON: Use the hIcon field for the small icon on the tab.
PSP_USEICONID: Use the pszIcon field as the name of the icon resource to load for use on the tab.
PSP_USETITLE: Use the spszTitle field as a pointer to a string or a resource identifier rather than using the title stored in the dialog template.
PSP_USERELEASEFUNC: Call the release function that is specified by pfnCallback when the HPROPSHEETPAGE is created. An application uses the release function to do any cleanup work associated with the property sheet, such as releasing resources and freeing up memory.
PSP_USEREFPARENT: Maintain a reference count for the life of the HPROPSHEETPAGE created using this structure.
typedef struct _PROPSHEETHEADER {
DWORD dwSize;
DWORD dwFlags;
HWND hwndParent;
HINSTANCE hInstance;
union {
HICON hIcon;
LPCSTR pszIcon;
};
LPCSTR pszCaption;
UINT nPages;
union {
UINT nStartPage;
LPCSTR pStartPage; // PSH_USEPSTARTPAGE: name of page or string id
};
union {
LPCPROPSHEETPAGE ppsp;
HPROPSHEETPAGE FAR *phpage;
};
PFNPROPSHEETCALLBACK pfnCallback;
} PROPSHEETHEADER, FAR *LPPROPSHEETHEADER;
The PROPSHEETHEADER structure describes a property sheet. The structure has the following members:
PSH_DEFAULT: Use the default meaning for all structure members.
PSH_MULTILINETABS: Support multiline tabs.
PSH_NOAPPLYNOW: Hide the Apply Now button.
PSH_PROPSHEETPAGE: Use property sheet pages rather than an array or page handles.
PSH_PROPTITLE: Use the string referred to by pszTitle as the title string.
PSH_USECALLBACK: Use the callback procedure specified in the pfnCallback member.
PSH_USEHICON: Use the handle to the icon specified by the hIcon field for the caption of the property sheet dialog.
PSH_USEICONID: Use the pszIcon field for the string or resource identifier for the icon.
PSH_USEPSTARTPAGE: Use the pStartPage member as the initial page.
PSH_WIZARD: This property sheet is a wizard.
Windows provides three new functions to manage property sheets and their pages. This section lists these new functions, their parameters, and return values.
HPROPSHEETPAGE CreatePropertySheetPage(LPCPROPSHEETPAGE lppsp);
Description: The CreatePropertySheetPage function creates a new page for a property sheet. An application uses this function to create a property sheet that includes the new page, or it uses the PSM_ADDPAGE message to add the new page to an existing property sheet.
Parameter:
Return value: The handle of the new property sheet if successful; NULL otherwise.
BOOL DestroyPropertySheetPage(HPROPSHEETPAGE hPSPage);
Description: The DestroyPropertySheetPage function destroys a property sheet page. An application must call this function for pages that have been created with the CreatePropertySheetPage function.
Parameter:
Return value: TRUE if successful; FALSE otherwise.
int PropertySheet(LPCPROPSHEETHEADER lppsph);
Description: The PropertySheet function creates a property sheet and adds the pages defined in the PROPSHEETHEADER structure.
Parameter:
Return value: A positive value if successful; –1 otherwise. The supported return values are:
Windows provides 16 new messages to handle property sheets and their pages. This section details these new messages, their parameters, return values, and associated macros. Remember that property sheets are part of the new common control library—it is a good idea to call the InitCommonControls function to ensure that the common-control DLL has been loaded before sending any of these messages.
wParam = 0; \\ not used
lParam = (HPROPSHEETPAGE)hPage; \\ handle of the page to add
Description: The PSM_ADDPAGE message adds a page to the end of an existing property sheet. Note that the property sheet will not resize dynamically added pages. If you must add a page, make sure that its size is no larger than the maximum size already in use. Since the size of the property sheet cannot change after it has been created, the new page must be no larger than the largest page currently in the property sheet.
Parameters: wParam is not used. lParam is the handle of the page to add.
Return value: None.
Macro: (VOID)PropSheet_AddPage(hPropSheetDlg, hPage);
wParam = 0; \\ not used
lParam = 0; \\ not used
Description: The PSM_APPLY message is sent to the property sheet to simulate clicking the Apply Now button. This message returns TRUE if and only if every page successfully saved its information.
Parameters: wParam and lParam are not used.
Return value: TRUE if every page successfully saved its data; FALSE otherwise.
Macro: (BOOL)PropSheet_Apply(hPropSheetDlg);
wParam = 0; \\ not used
lParam = 0; \\ not used
Description: The PSM_CANCELTOCLOSE message is sent when a change has been made that cannot be canceled in the property sheet (for example, a change to the registry). The Cancel button will be disabled, and the OK button label will be changed to Close.
Parameters: wParam and lParam are not used.
Return value: None.
Macro: (VOID)PropSheet_CancelToClose(hPropSheetDlg);
wParam = hwnd; \\ handle of the page window
lParam = 0; \\ not used
Description: The PSM_CHANGED message is sent to the property sheet when information in the page has changed. The property sheet may change the name of the page in the list of pages to italic text. The Apply Now button will also be enabled. (This button is initially disabled when a page becomes active, indicating that there are no property changes to apply yet.) When the page receives user input through one of its controls, indicating that the user has edited a property, the page should send the PSM_CHANGED message to the property sheet. If the user subsequently clicks the Apply Now or Cancel button, the page should reinitialize its controls and then send the PSM_UNCHANGED message to re-disable the Apply Now button. Sometimes, the Apply Now button causes a page to change a property sheet, and the change cannot be undone. When this happens, the page should send the PSM_CANCELTOCLOSE message to the property sheet. The message causes the property sheet to change the label of the Cancel button to Close, indicating to the user that the applied changes cannot be canceled.
Parameters: wParam is the handle of the page window. lParam is not used.
Return value: None.
Macro: (VOID)PropSheet_Changed (hPropSheetDlg, hwndPage);
wParam = 0; \\ not used
lParam = 0; \\ not used
Description: The PSM_GETTABCONTROL message gets the handle to the tab control.
Parameters: wParam and lParam are not used.
Return value: None.
Macro: (VOID)PropSheet_GetTabControl(hPropSheetDlg);
wParam = index; \\ index of the page
lParam = 0; \\ not used
Description: If the user has visited (tabbed to) a page, the PSM_GETPAGE message retrieves the window handle of the dialog box for the page at the specified index, and returns NULL otherwise.
Parameters: wParam is the index of the page to retrieve. lParam is not used.
Return value: The handle of the property page.
Macro: (HWND)PropSheet_GetPage(hPropSheetDlg, index);
wParam = iButton; \\ the id of the Button
lParam = 0; \\ not used
Description: The PSM_PRESSBUTTON message causes the specified button to be “pressed.”
Parameters: wParam is the ID of the button. It can be one of the values listed below. lParam is not used.
Return value: None.
Macro: (VOID)PropSheet_PressButton(hPropSheetDlg, iButton);
wParam = 0; \\ not used
lParam = MAKELPARAM(x,y); \\ the new size for the tab
Description: The PSM_QUERYSIBLINGS message is forwarded to each property sheet page until a property page returns a nonzero value, which becomes the return value of this message. This is a convenient message for passing information between property pages when the property pages don't know about one another. For example, the printer property page extensions use this message to communicate with the property sheet pages provided by the system.
Parameters: wParam and lParam are the parameters for the forwarded message.
Return value: The return value of the forwarded message.
Macro: (VOID)PropSheet_QuerySiblings(hPropSheetDlg, wParam, lParam);
wParam = 0; \\ not used
lParam = 0; \\ not used
Description: The PSM_REBOOTSYSTEM message is sent when MS-DOS® needs to be restarted for the changes specified in the property sheet to take effect. The page should only send this notification in response to a PSN_APPLY or PSN_KILLACTIVE notification. Note that this notification supersedes all PSN_RESTARTWINDOWS notifications that precede or follow. This message will cause the property sheet return value to be ID_REBOOTSYSTEM if the user selects OK to close the property sheet.
Parameters: wParam and lParam are not used.
Return value: None.
Macro: (VOID)PropSheet_RebootSystem(hPropSheetDlg);
wParam = index; \\ index of the page
lParam = (HPROPSHEETPAGE)hPage; \\ the page to remove
Description: The PSM_REMOVEPAGE message removes a page from an existing property sheet. If hPage is NULL or does not exist, the property sheet will remove the page at the location specified by the index parameter. When a page is defined, an application may specify the address of a ReleasePropSheetPageProc callback function that the property sheet calls when it is removing the page. Using a ReleasePropSheetPageProc function gives an application the opportunity to perform clean-up operations for individual pages.
Parameters: wParam is the index of the page. lParam is the handle of the page to remove.
Return value: None.
Macro: (VOID)PropSheet_RemovePage(hPropSheetDlg, index, hPage);
wParam = 0; \\ not used
lParam = 0; \\ not used
Description: The PSM_RESTARTWINDOWS message is sent when Windows needs to be restarted for changes specified by the property sheet to take effect. The page should only send this notification in response to a PSN_APPLY or PSN_KILLACTIVE notification. This will cause the property sheet to return ID_PSRESTARTWINDOWS if the user selects OK to close the property sheet.
Parameters: wParam and lParam are not used.
Return value: None.
Macro: (VOID)PropSheet_RestartWindows(hPropSheetDlg);
wParam = index; \\ index to the tab
lParam = (HPROPSHEETPAGE)hPage; \\ the page to select
Description: The PSM_SETCURSEL message is sent to the property sheet to change focus to a different page. If hPage is NULL or cannot be found, the property sheet will set the active page at location index.
Parameters: wParam is the index to the tab. lParam is the page to select.
Return value: None.
Macro: (VOID)PropSheet_SetCurSel(hPropSheetDlg, index, hPage);
wParam = 0; \\ not used
lParam = (int)id; \\ the index to the tab or handle of the page
Description: The PSM_SETCURSELID message sets the active page by the ID of the tab or the hPage as specified in lParam.
Parameters: wParam is not used. lParam is the index of the tab or the handle of the page to activate.
Return value: None.
Macro: (VOID)PropSheet_SetCurSelById(hPropSheetDlg, id);
wParam = 0; \\ not used
lParam = (LPSTR)lpszText; \\ the text of the button
Description: The PSM_SETFINISHTEXT message enables the Finish button, hides the Back button, and sets the text on the Finish button to the text specified in lParam.
Parameters: wParam is not used. lParam is the text of the button.
Return value: None.
Macro: (VOID)PropSheet_SetFinishText(hPropSheetDlg, lpszText);
wParam = 0; \\ not used
lParam = (DWORD)dwFlags; \\ the buttons to enable
Description: The PSM_SETWIZBUTTONS message specifies which buttons should be enabled within the wizard. It is only supported in wizard-style property sheets.
Parameters: wParam is not used. lParam specifies which buttons are enabled. This parameter can be a combination of the following values:
Return value: None.
Macro: (VOID)PropSheet_SetWizButtons (hPropSheetDlg, dwFlags);
wParam = hwndPage; \\ handle of the page window
lParam = 0; \\ not used
Description: The PSM_UNCHANGED message is sent to the property sheet when the information in the page has reverted to its previously saved state. The property sheet cancels any changes caused by PSM_CHANGED. The Apply Now button may be disabled if no pages with registered changes remain.
Parameters: wParam is the handle of the property sheet page that has been saved. lParam is not used.
Return value: None.
Macro: (VOID)PropSheet_UnChanged(hPropSheetDlg, hwndPage);
This article marks the end of my series on the new Win32 common controls. I must say that at this point I am tempted to simply say, "Ta-da!" However, as a parting gesture, I'll attempt a summary of what you've learned in this article.
Tabs, used in conjunction with property sheets, provide an efficient means of displaying the properties of objects and allowing the user to change these properties. Property sheets are best used in areas where you previously used a stack of cascading modal dialog boxes. Property sheets give the user the ability to go back and forth among logical groupings of properties instead of having to save changes individually. Because property sheets are so simple to implement, they are a good candidate to add to the list of changes that you, the application developer, are undoubtedly planning to make to take advantage of the new built-in features of Windows 95.
So there you have it. TADA.WAV!