Creating a Property Sheet

My PROPS sample demonstrates how to create and manipulate property sheets. It produces a simple property sheet that supports two pages. I had already written the TRACKBAR sample (discussed in Chapter 2), so I put a trackbar on the client area of the screen and used a property sheet to set values for this control.

Converting Dialog Boxes to Property Sheets

First I took an existing sample and used its dialog boxes for the pages in my property sheet. I made two major changes to the dialog templates:

In the Microsoft Visual C++ 2.1 resource editor, the DS_3DLOOK style is not supported with the other dialog box styles. To use this style, you need to edit your dialog boxes manually.

I also took this opportunity to review my dialog boxes and concluded that I could rearrange the contents of the original four dialog boxes into two pages. This added a little extra work to the conversion, but it improved the organization of the pages and gave my sample a more polished look. If you aren't converting dialog boxes to property sheet pages, you can simply use the resource editor to create a new dialog box, follow the two steps described in the bulleted list on page 114, and add your controls.

After I converted the dialog boxes, I produced the property sheet by defining an array of PROPSHEETPAGE structures for the pages, filling out a PROPSHEETHEADER structure, and then calling the PropertySheet function. This function creates 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 decide the sequence of the tabs before you define the pages in the array.

Once a property sheet exists, an application can add and remove pages dynamically by sending the PSM_ADDPAGE and PSM_REMOVEPAGE messages or executing their corresponding macros. By default, when a property sheet is destroyed, its pages are destroyed in first-in-last-out (FILO) order—that is, the last page specified in the array of pages is the first page destroyed.

I wrote the CreatePropertySheet function to create the property sheet and its pages. This function fills out a PROPSHEETPAGE structure for the two pages, fills out the PROPSHEETHEADER structure, and then calls the PropertySheet function. I replaced the DialogBox function calls in my code with a call to the CreatePropertySheet function.

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 = "Trackbar 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 = "Trackbar 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)"Trackbar Properties";
psh.nPages = sizeof (psp) / sizeof (PROPSHEETPAGE);
psh.ppsp = (LPCPROPSHEETPAGE) &psp;

return PropertySheet (&psh);
}

Changing the Dialog Procedure

Next I had to convert my dialog procedure from managing a dialog box to managing a property sheet page. The major changes involved the handling of the OK and Cancel buttons. Typically, a WM_COMMAND message notifies a dialog procedure that the OK or Cancel button has been clicked. When the procedure gets this message, it generally verifies the information entered in the dialog box controls and calls the EndDialog function to destroy the dialog box. The following code demonstrates how a typical dialog 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 procedure. Instead, the procedure must handle a group of page notifications. My application needed to handle the following notifications:

PSN_APPLY Sent when the user clicks the OK button or the Apply button. This is also the time to validate any changes the user has made.
PSN_KILLACTIVE Sent when the user clicks a tab on the property sheet and switches pages.
PSN_RESET Sent when the user clicks the Cancel button.
PSN_SETACTIVE Sent when a page is coming into focus. The application should take this opportunity to initialize the controls for that page.

Initially, I found it difficult to differentiate between the OK and Apply buttons. They both require the page to validate and apply the changes the user has made. The only difference is that clicking OK destroys the property sheet after the changes are applied, whereas clicking Apply does not. As a result, if the user applies a change and later cancels out of the property sheet, the application should reset the property to its initial value rather than saving the applied value. In other words, changes are permanent when the user chooses the OK button; the Apply button allows the user to “try out” an action.

Another change I had to make was removing the EndDialog call. I couldn't call the EndDialog function for a property sheet page because it destroys the entire property sheet instead of destroying only the page. The following dialog procedure handles the Trackbar Range page:

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

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 that is used to produce the page. The dialog procedure can save the pointer to this structure and use it later to modify the page.