Contents
Introduction
Property sheets have been a popular way of presenting options, even before Windows 95 introduced the sheet as
a common control. Wizards are often used to guide users through installing software or other complex tasks. WTL
provides good support for creating both of these types of property sheets, and lets you use all of the dialog-related
features that we've covered earlier, like DDX and DDV. In this article, I'll demonstrate creating a basic property
sheet and a wizard, and how to handle events and notification messages sent by the sheet.
WTL Property Sheet Classes
There are two classes that combine to implement a property sheet, CPropertySheetWindow
and CPropertySheetImpl
.
Both are defined in the atldlgs.h header file. CPropertySheetWindow
is a window interface class
(that is, it derives from CWindow
), and CPropertySheetImpl
has a message map and actually
implements the window. This is similar to the basic ATL windowing classes, where CWindow
and CWindowImpl
are used together.
CPropertySheetWindow
contains wrappers for the various PSM_*
messages, such as SetActivePageByID()
which wraps PSM_SETCURSELID
. CPropertySheetImpl
manages a PROPSHEETHEADER
struct and an array of HPROPSHEETPAGE
s. CPropertySheetImpl
also has methods for setting
some PROPSHEETHEADER
fields, and adding and removing pages. You can access the PROPSHEETHEADER
directly by accessing the m_psh
member variable.
Finally, CPropertySheet
is a specialization of CPropertySheetImpl
that you can use
if you don't need to customize the sheet at all.
CPropertySheetImpl methods
Here are some of the important methods of CPropertySheetImpl
. Since many methods are just wrappers
around window messages, I won't present an exhaustive list here, but you can check out atldlgs.h to see
the complete list of methods.
CPropertySheetImpl(_U_STRINGorID title = (LPCTSTR) NULL,
UINT uStartPage = 0, HWND hWndParent = NULL)
The CPropertySheetImpl
constructor lets you specify some common properties right away, so you don't
have to call other methods later to set them. title
specifies the text to use in the property sheet's
caption. _U_STRINGorID
is a WTL utility class that lets you pass either an LPCTSTR
or
string resource ID. For example, both of these lines will work:
CPropertySheet mySheet ( IDS_SHEET_TITLE );
CPropertySheet mySheet ( _T("My prop sheet") );
if IDS_SHEET_TITLE
is the ID of a string in the string table. uStartPage
is the zero-based
index of the page that should be active when the sheet is first made visible. hWndParent
sets the
sheet's parent window.
BOOL AddPage(HPROPSHEETPAGE hPage)
BOOL AddPage(LPCPROPSHEETPAGE pPage)
Adds a property page to the sheet. If the page is already created, you can pass its handle (an HPROPSHEETPAGE
)
to the first overload. The more common way is to use the second overload. With that version, you set up a PROPSHEETPAGE
struct (which you can do with CPropertyPageImpl
, covered later) and CPropertySheetImpl
will create and manage the page for you.
BOOL RemovePage(HPROPSHEETPAGE hPage)
BOOL RemovePage(int nPageIndex)
Removes a page from the sheet. You can pass either the page's handle or its zero-based index.
BOOL SetActivePage(HPROPSHEETPAGE hPage)
BOOL SetActivePage(int nPageIndex)
Sets the active page in the sheet. You can pass either the handle or zero-based index of the page to be made
active. You can call this method before showing the property sheet to set which page will be active when the sheet
is first made visible.
void SetTitle(LPCTSTR lpszText, UINT nStyle = 0)
Sets the text to be used in the caption of the property sheet. nStyle can be either 0 or PSH_PROPTITLE
.
If it is PSH_PROPTITLE
, then that style bit is added to the sheet, which causes the words "Properties
for" to be prepended to the text you pass in lpszText
.
void SetWizardMode()
Sets the PSH_WIZARD
style, which changes the sheet into a wizard. You must call this method before
showing the sheet.
void EnableHelp()
Sets the PSH_HASHELP
style, which adds a Help button to the sheet. Note that you also need to enable
help in each page that provides help for this to take effect.
INT_PTR DoModal(HWND hWndParent = ::GetActiveWindow())
Creates and shows a modal property sheet. The return value is positive to indicate success, see the docs on
the PropertySheet()
API for a full description of the return value. If an error occurs and the sheet
can't be created, DoModal()
returns -1.
HWND Create(HWND hWndParent = NULL)
Creates and shows a modeless property sheet, and returns its window handle. If an error occurs and the sheet
can't be created, Create()
returns NULL.
WTL Property Page Classes
The WTL classes that encapsulate property pages work similarly to the property sheet classes. There is a window
interface class, CPropertyPageWindow
, and an implementation class, CPropertyPageImpl
.
CPropertyPageWindow
is very small, and contains mostly utility functions that call methods in the
parent sheet.
CPropertyPageImpl
derives from the ATL class CDialogImplBaseT
, since a page is built
from a dialog resource. That means that all the WTL features we've used in dialogs are also available in property
sheets, like DDX and DDV. CPropertyPageImpl
has two main purposes: it manages a PROPSHEETPAGE
struct (kept in the member variable m_psp
), and handles PSN_*
notification messages.
For very simple property pages, you can use the CPropertyPage
class. This is only suitable for pages
that do not interact with the user at all, for example an About page, or the introduction page in a wizard.
You can also create pages that host ActiveX controls. You first include atlhost.h in stdafx.h.
For the page, you use CAxPropertyPageImpl
instead of CPropertyPageImpl
. For simple pages
that host ActiveX controls, you can use CAxPropertyPage
instead of CPropertyPage
.
CPropertyPageWindow methods
CPropertyPageWindow
's most important method is GetPropertySheet()
:
CPropertySheetWindow GetPropertySheet()
This method gets the HWND
of the page's parent window (the sheet) and attaches a CPropertySheetWindow
to that HWND
. The new CPropertySheetWindow
is then returned to the caller. Note that
this only creates a temporary object; it does not return a pointer or reference to the actual CPropertySheet
or CPropertySheetImpl
object used to create the sheet. This is important if you are using your own
CPropertySheetImpl
-derived class and need to access data members in the sheet object.
The remaining members just call through to CPropertySheetWindow
functions that wrap PSM_*
messages:
BOOL Apply()
void CancelToClose()
void SetModified(BOOL bChanged = TRUE)
LRESULT QuerySiblings(WPARAM wParam, LPARAM lParam)
void RebootSystem()
void RestartWindows()
void SetWizardButtons(DWORD dwFlags)
For example, in a CPropertyPageImpl
-derived class, you can call:
SetWizardButtons ( PSWIZB_BACK | PSWIZB_FINISH );
instead of:
CPropertySheetWindow wndSheet;
wndSheet = GetPropertySheet();
wndSheet.SetWizardButtons ( PSWIZB_BACK | PSWIZB_FINISH );
CPropertyPageImpl methods
CPropertyPageImpl
manages a PROPSHEETPAGE
struct, the public member m_psp
.
CPropertyPageImpl
also has an operator PROPSHEETPAGE*
converter, so you can pass a CPropertyPageImpl
to a method that takes an LPPROPSHEETPAGE
or LPCPROPSHEETPAGE
, such as CPropertySheetImpl::AddPage()
.
The CPropertyPageImpl
constructor lets you set the page title, which is the text that appears on
the page's tab:
CPropertyPageImpl(_U_STRINGorID title = (LPCTSTR) NULL)
If you ever need to create a page manually, instead of letting the sheet do it, you can call Create()
:
HPROPSHEETPAGE Create()
Create()
just calls the Win32 API CreatePropertySheetPage()
with m_psp
as the parameter. You would only need to call Create()
if you are adding pages to a sheet after the
sheet is created, or if you are creating a page to be passed to some other sheet not under your control (for example,
a property sheet handler shell extension).
There are three methods for setting various title text on the page:
void SetTitle(_U_STRINGorID title)
void SetHeaderTitle(LPCTSTR lpstrHeaderTitle)
void SetHeaderSubTitle(LPCTSTR lpstrHeaderSubTitle)
The first changes the text on the page's tab. The other two are used in Wizard97-style wizards to set the text
in the header above the property page area.
void EnableHelp()
Sets the PSP_HASHELP
flag in m_psp
to enable the Help button when the page is active.
Handling notification messages
CPropertyPageImpl
has a message map that handles WM_NOTIFY
. If the notification code
is a PSN_*
value, OnNotify()
calls a handler for that particular notification. This is
done using the compile-time virtual function technique, so the handlers can be easily overridden in derived classes.
There are two sets of notification handlers, due to a design change between WTL 3 and 7. In WTL 3, the notification
handlers had return values that differ from the return values for the PSN_*
messages. For example,
the WTL 3 handler for PSN_WIZFINISH
is:
case PSN_WIZFINISH:
lResult = !pT->OnWizardFinish();
break;
OnWizardFinish()
was expected to return TRUE
to let the wizard finish, or FALSE
to prevent the wizard from closing. This code broke when the IE 5 common controls added the ability to return a
window handle from the PSN_WIZFINISH
handler to give that window the focus. WTL 3 apps could not use
this feature because all non-zero values were treated the same.
In WTL 7, OnNotify()
does not change any value returned from a PSN_*
handler. The
handlers can return any documented legal value and the behavior will be correct. However, for backwards compatibility,
the WTL 3 handlers are still present and are used by default. To use the WTL 7 handlers, you must add this line
to stdafx.h before including atldlgs.h:
#define _WTL_NEW_PAGE_NOTIFY_HANDLERS
When writing new code, there is no reason not to use the WTL 7 handlers, so the WTL 3 handlers will not be covered
here.
CPropertyPageImpl
has default handlers for all notifications, so you can override only the handlers
that are relevant to your program. The default handlers and their actions are:
int OnSetActive()
- allows the page to become active
BOOL OnKillActive()
- allows the page to become inactive
int OnApply()
- returns PSNRET_NOERROR
to indicate the apply operation succeeded
void OnReset()
- no action
BOOL OnQueryCancel()
- allows the cancel operation
int OnWizardBack()
- goes to the previous page
int OnWizardNext()
- goes to the next page
INT_PTR OnWizardFinish()
- allows the wizard to finish
void OnHelp()
- no action
BOOL OnGetObject(LPNMOBJECTNOTIFY lpObjectNotify)
- no action
int OnTranslateAccelerator(LPMSG lpMsg)
- returns PSNRET_NOERROR
to indicate that
the message was not handled
HWND OnQueryInitialFocus(HWND hWndFocus)
- returns NULL to set the focus to the first control in
the tab order
Creating a Property Sheet
Now that our tour of the classes is complete, we need a program to illustrate how to use them. The sample project
for this article is a simple SDI app that shows a picture in the client area, and fills the background with a color.
The picture and color can be changed via an options dialog (a property sheet), and a wizard (which I'll describe
later).
The simplest property sheet, ever
After making an SDI project with the WTL AppWizard, we can start by creating a sheet to use for our about box.
Let's start with the About dialog the wizard creates for us, and change the styles so it will work as a
property page.
The first step is to remove the OK button, since that doesn't make sense in a sheet. In the dialog's properties,
change the Style to Child, the Border to Thin, and set Disabled to checked.
The second (and final) step is to create a property sheet in the OnAppAbout()
handler. We can do
this with CPropertySheet
and CPropertyPage
:
void CMainFrame::OnAppAbout(...)
{
CPropertySheet sheet ( _T("About PSheets") );
CPropertyPage<IDD_ABOUTBOX> pgAbout;
sheet.AddPage ( pgAbout );
sheet.DoModal ( *this );
}
The result looks like this:
Creating a useful property page
Since not every page in every sheet is as simple as an about box, most pages will require a CPropertyPageImpl
-derived
class, so we'll take a look at such a class now. We'll make a new property page that contains the settings for
the graphics shown in the background of the client area. Here's the dialog:
This dialog has the same styles as the About page. We'll need a new class to go along with the page,
called CBackgroundOptsPage
. This class derives from CPropertyPageImpl
since it's a property
page, and CWinDataExchange
to enable DDX.
class CBackgroundOptsPage :
public CPropertyPageImpl<CBackgroundOptsPage>,
public CWinDataExchange<CBackgroundOptsPage>
{
public:
enum { IDD = IDD_BACKGROUND_OPTS };
CBackgroundOptsPage();
~CBackgroundOptsPage();
BEGIN_MSG_MAP(CBackgroundOptsPage)
MSG_WM_INITDIALOG(OnInitDialog)
CHAIN_MSG_MAP(CPropertyPageImpl<CBackgroundOptsPage>)
END_MSG_MAP()
BEGIN_DDX_MAP(CBackgroundOptsPage)
DDX_RADIO(IDC_BLUE, m_nColor)
DDX_RADIO(IDC_ALYSON, m_nPicture)
END_DDX_MAP()
BOOL OnInitDialog ( HWND hwndFocus, LPARAM lParam );
int OnApply();
int m_nColor, m_nPicture;
};
Things to note in this class:
- There is a public member named
IDD
that holds the associated dialog resource ID.
- The message map resembles that of a
CDialogImpl
class.
- The message map chains messages to
CPropertyPageImpl
so that property sheet-related notification
messages are handled.
- There is an
OnApply()
handler to save the user's choices when he clicks OK in the sheet.
OnApply()
is pretty simple, it calls DoDataExchange()
to update the DDX variables,
then returns a code indicating whether the sheet should close or not:
int CBackgroundOptsPage::OnApply()
{
return DoDataExchange(true) ? PSNRET_NOERROR : PSNRET_INVALID;
}
We'll add a Tools|Options menu item that brings up the property sheet, and put add handler for this command
to the view class. The handler creates the property sheet as before, but with the new CBackgroundOptsPage
added to the sheet.
void CPSheetsView::OnOptions ( UINT uCode, int nID, HWND hwndCtrl )
{
CPropertySheet sheet ( _T("PSheets Options"), 0 );
CBackgroundOptsPage pgBackground;
CPropertyPage<IDD_ABOUTBOX> pgAbout;
pgBackground.m_nColor = m_nColor;
pgBackground.m_nPicture = m_nPicture;
sheet.m_psh.dwFlags |= PSH_NOAPPLYNOW|PSH_NOCONTEXTHELP;
sheet.AddPage ( pgBackground );
sheet.AddPage ( pgAbout );
if ( IDOK == sheet.DoModal() )
SetBackgroundOptions ( pgBackground.m_nColor,
pgBackground.m_nPicture );
}
The sheet
constructor now has a second parameter of 0, meaning that the page at index 0 should
be visible initially. You could change that to 1 to have the About page be visible when the sheet first
appears. Since this is just demo code, I'm going to be lazy and make the CBackgroundOptsPage
variables
connected to the radio buttons public. The view stores the current options in those variables, and saves the new
values if the user clicks OK in the sheet.
If the user clicks OK, DoModal()
returns IDOK
, and the view redraws itself using the
new picture and color. Here are some screen shots to show the different views:
Creating a better property sheet class
The OnOptions()
handler creates the sheet just fine, but there's an awful lot of setup and initialization
code there, which really shouldn't be CMainFrame
's responsibility. A better way is to make a class
derived from CPropertySheetImpl
that handles those tasks.
#include "BackgroundOptsPage.h"
class COptionsSheet : public CPropertySheetImpl<COptionsSheet>
{
public:
COptionsSheet(_U_STRINGorID title = (LPCTSTR) NULL,
UINT uStartPage = 0, HWND hWndParent = NULL);
BEGIN_MSG_MAP(COptionsSheet)
CHAIN_MSG_MAP(CPropertySheetImpl<COptionsSheet>)
END_MSG_MAP()
CBackgroundOptsPage m_pgBackground;
CPropertyPage<IDD_ABOUTBOX> m_pgAbout;
};
With this class, we've encapsulated the details of what pages are in the sheet, and moved them into the sheet
class itself. The constructor handles adding the pages to the sheet, and setting any other necessary flags:
COptionsSheet::COptionsSheet (
_U_STRINGorID title, UINT uStartPage, HWND hWndParent ) :
CPropertySheetImpl<COptionsSheet>(title, uStartPage, hWndParent)
{
m_psh.dwFlags |= PSH_NOAPPLYNOW|PSH_NOCONTEXTHELP;
AddPage ( m_pgBackground );
AddPage ( m_pgAbout );
}
As a result, the OnOptions()
handler becomes a bit simpler:
void CPSheetsView::OnOptions ( UINT uCode, int nID, HWND hwndCtrl )
{
COptionsSheet sheet ( _T("PSheets Options"), 0 );
sheet.m_pgBackground.m_nColor = m_nColor;
sheet.m_pgBackground.m_nPicture = m_nPicture;
if ( IDOK == sheet.DoModal() )
SetBackgroundOptions ( sheet.m_pgBackground.m_nColor,
sheet.m_pgBackground.m_nPicture );
}
Creating a Wizard
Creating a wizard is, not surprisingly, similar to creating a property sheet. A little more work is required
to enable the Back and Next buttons; just as in MFC property pages, you override OnSetActive()
and call SetWizardButtons()
to enable the appropriate buttons. We'll start with a simple introduction
page, with ID IDD_WIZARD_INTRO
:
Notice that the page has no caption text. Since every page in a wizard usually has the same title, I prefer
to set the text in the CPropertySheetImpl constructor, and have every page use the same string resource. That way,
I can just change that one string and every page will reflect the change.
The implementation of this page is done in the CWizIntroPage
class:
class CWizIntroPage : public CPropertyPageImpl<CWizIntroPage>
{
public:
enum { IDD = IDD_WIZARD_INTRO };
CWizIntroPage();
BEGIN_MSG_MAP(COptionsWizard)
CHAIN_MSG_MAP(CPropertyPageImpl<CWizIntroPage>)
END_MSG_MAP()
int OnSetActive();
};
The constructor sets the page's text by referencing a string resource ID:
CWizIntroPage::CWizIntroPage() :
CPropertyPageImpl<CWizIntroPage>(IDS_WIZARD_TITLE)
{
}
The string IDS_WIZARD_TITLE
("PSheets Options Wizard") will appear in the wizard's caption
bar when this page is the current page. OnSetActive()
enables just the Next button:
int CWizIntroPage::OnSetActive()
{
SetWizardButtons ( PSWIZB_NEXT );
return 0;
}
To implement the wizard, we'll create a class COptionsWizard
, and a Tools|Wizard
menu option in the app's menu. The COptionsWizard
constructor is pretty similar to COptionsSheet
's,
in that it sets any necessary style bits or flags, and adds pages to the sheet.
class COptionsWizard : public CPropertySheetImpl<COptionsWizard>
{
public:
COptionsWizard ( HWND hWndParent = NULL );
BEGIN_MSG_MAP(COptionsWizard)
CHAIN_MSG_MAP(CPropertySheetImpl<COptionsWizard>)
END_MSG_MAP()
CWizIntroPage m_pgIntro;
};
COptionsWizard::COptionsWizard ( HWND hWndParent ) :
CPropertySheetImpl<COptionsWizard> ( 0U, 0, hWndParent )
{
SetWizardMode();
AddPage ( m_pgIntro );
}
Then the handler for the Tools|Wizard menu looks like this:
void CPSheetsView::OnOptionsWizard ( UINT uCode, int nID, HWND hwndCtrl )
{
COptionsWizard wizard;
wizard.DoModal ( GetTopLevelParent() );
}
And here's the wizard in action:
Adding More Pages, Handling DDV
To make this a useful wizard, we'll add a new page for setting the view's background color. This page will also
have a checkbox for demonstrating handling a DDV failure and preventing the user from continuing on with the wizard.
Here's the new page, whose ID is IDD_WIZARD_BKCOLOR
:
The implementation for this page is in the class CWizBkColorPage
. Here are the parts relating to
class CWizBkColorPage :
public CPropertyPageImpl<CWizBkColorPage>,
public CWinDataExchange<CWizBkColorPage>
{
public:
BEGIN_DDX_MAP(CWizBkColorPage)
DDX_RADIO(IDC_BLUE, m_nColor)
DDX_CHECK(IDC_FAIL_DDV, m_bFailDDV)
END_DDX_MAP()
int OnSetActive();
BOOL OnKillActive();
int m_nColor;
protected:
bool m_bFailDDV;
};
OnSetActive()
works similarly to the intro page, it enables both the Back and Next
buttons. OnKillActive()
is a new handler, it invokes DDX then checks the value of m_bFailDDV
.
If that variable is true
, meaning the checkbox was checked, OnKillActive()
prevents the
wizard from going to the next page.
int CWizBkColorPage::OnSetActive()
{
SetWizardButtons ( PSWIZB_BACK | PSWIZB_NEXT );
return 0;
}
int CWizBkColorPage::OnKillActive()
{
if ( !DoDataExchange(true) )
return TRUE;
if ( m_bFailDDV )
{
MessageBox (
_T("Error box checked, wizard will stay on this page."),
_T("PSheets"), MB_ICONERROR );
return TRUE;
}
return FALSE;
}
Note that the logic in OnKillActive()
could certainly have been put in OnWizardNext()
instead, since both handlers have the ability to keep the wizard on the current page. The difference is that OnKillActive()
is called when the user clicks Back or Next, while OnWizardNext()
is only called when
the user clicks Next (as the name signifies). OnWizardNext()
is also used for other purposes;
it can direct the wizard to a different page instead of the next one in order, if some pages need to be skipped.
The wizard in the sample project has two more pages, CWizBkPicturePage
and CWizFinishPage
.
Since they are similar to the two pages above, I won't cover them in detail here, but you can check out the sample
code to get all the details.
Other UI Considerations
Centering a sheet
The default behavior of sheets and wizards is to appear near the upper-left corner of their parent window:
This looks rather sloppy, but fortunately we can remedy it. Thanks to the folks on the forum who provided the
code to do this; the previous version of the article did it in a much more complicated way.
The property sheet or wizard class can handle the WM_SHOWWINDOW
message. The wParam
sent with WM_SHOWWINDOW
is a boolean indicating whether the window is being shown. If wParam
is true
, and it's the first time the window is being shown, then it can call CenterWindow()
.
Here is the code that we can add to COptionsSheet
to center the sheet. The m_bCentered
member is how we keep track of whether the sheet has already been centered.
class COptionsSheet : public CPropertySheetImpl<COptionsSheet>
{
BEGIN_MSG_MAP(COptionsSheet)
MSG_WM_SHOWWINDOW(OnShowWindow)
CHAIN_MSG_MAP(CPropertySheetImpl<COptionsSheet>)
END_MSG_MAP()
void OnShowWindow(BOOL bShowing, int nReason);
protected:
bool m_bCentered;
};
void COptionsSheet::OnShowWindow(BOOL bShowing, int nReason)
{
if ( bShowing && !m_bCentered )
{
m_bCentered = true;
CenterWindow ( m_psh.hwndParent );
}
}
Adding icons to pages
To use other features of sheets and pages that are not already wrapped by member functions, you'll need to access
the relevant structures directly: the PROPSHEETHEADER
member m_psh
in CPropertySheetImpl
,
or the PROPSHEETPAGE
member m_psp
in CPropertyPageImpl
.
For example, to add an icon to the Background page of the options property sheet, we need to add a flag
and set a couple other members in the page's PROPSHEETPAGE
struct:
CBackgroundOptsPage::CBackgroundOptsPage()
{
m_psp.dwFlags |= PSP_USEICONID;
m_psp.pszIcon = MAKEINTRESOURCE(IDI_TABICON);
m_psp.hInstance = _Module.GetResourceInstance();
}
And here's the result:
Up Next
In Part 9, I'll cover WTL's utility classes, and its wrappers for GDI object and common dialogs.
Copyright and license
This article is copyrighted material, (c)2003-2006 by Michael Dunn. I realize this isn't going to stop people
from copying it all around the 'net, but I have to say it anyway. If you are interested in doing a translation
of this article, please email me to let me know. I don't foresee denying anyone permission to do a translation,
I would just like to be aware of the translation so I can post a link to it here.
The demo code that accompanies this article is released to the public domain. I release it this way so that
the code can benefit everyone. (I don't make the article itself public domain because having the article available
only on CodeProject helps both my own visibility and the CodeProject site.) If you use the demo code in your own
application, an email letting me know would be appreciated (just to satisfy my curiosity about whether folks are
benefitting from my code) but is not required. Attribution in your own source code is also appreciated but not
required.
Revision History
September 13, 2003: Article first published.
January 13, 2006: Moved options sheet/wizard code into the view class. Updated section on centering the sheet.
Series Navigation: « Part VII (Splitter Windows)