|
|||||||||||||||||||||||
|
|||||||||||||||||||||||
|
Announcements
Chapters
Services
Feature Zones
|
Contents
IntroductionProperty 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 ClassesThere are two classes that combine to implement a property sheet,
Finally, CPropertySheetImpl methodsHere are some of the important methods of CPropertySheetImpl(_U_STRINGorID title = (LPCTSTR) NULL,
UINT uStartPage = 0, HWND hWndParent = NULL)
The CPropertySheet mySheet ( IDS_SHEET_TITLE ); CPropertySheet mySheet ( _T("My prop sheet") ); if 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 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 void SetWizardMode()
Sets the void EnableHelp()
Sets the 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 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, WTL Property Page ClassesThe WTL classes that encapsulate property pages work similarly to the property sheet classes. There is a window
interface class,
You can also create pages that host ActiveX controls. You first include atlhost.h in stdafx.h.
For the page, you use CPropertyPageWindow methods
CPropertySheetWindow GetPropertySheet() This method gets the The remaining members just call through to 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 SetWizardButtons ( PSWIZB_BACK | PSWIZB_FINISH ); instead of: CPropertySheetWindow wndSheet; wndSheet = GetPropertySheet(); wndSheet.SetWizardButtons ( PSWIZB_BACK | PSWIZB_FINISH ); CPropertyPageImpl methods
The 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 HPROPSHEETPAGE Create()
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 Handling notification messages
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 case PSN_WIZFINISH: lResult = !pT->OnWizardFinish(); break;
In WTL 7, #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.
Creating a Property SheetNow 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, everAfter 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 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 pageSince not every page in every sheet is as simple as an about box, most pages will require a
This dialog has the same styles as the About page. We'll need a new class to go along with the page,
called class CBackgroundOptsPage : public CPropertyPageImpl<CBackgroundOptsPage>, public CWinDataExchange<CBackgroundOptsPage> { public: enum { IDD = IDD_BACKGROUND_OPTS }; // Construction CBackgroundOptsPage(); ~CBackgroundOptsPage(); // Maps 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() // Message handlers BOOL OnInitDialog ( HWND hwndFocus, LPARAM lParam ); // Property page notification handlers int OnApply(); // DDX variables int m_nColor, m_nPicture; }; Things to note in this class:
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 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 If the user clicks OK, Creating a better property sheet classThe #include "BackgroundOptsPage.h" class COptionsSheet : public CPropertySheetImpl<COptionsSheet> { public: // Construction COptionsSheet(_U_STRINGorID title = (LPCTSTR) NULL, UINT uStartPage = 0, HWND hWndParent = NULL); // Maps BEGIN_MSG_MAP(COptionsSheet) CHAIN_MSG_MAP(CPropertySheetImpl<COptionsSheet>) END_MSG_MAP() // Property pages 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 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 WizardCreating 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
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 class CWizIntroPage : public CPropertyPageImpl<CWizIntroPage> { public: enum { IDD = IDD_WIZARD_INTRO }; // Construction CWizIntroPage(); // Maps BEGIN_MSG_MAP(COptionsWizard) CHAIN_MSG_MAP(CPropertyPageImpl<CWizIntroPage>) END_MSG_MAP() // Notification handlers int OnSetActive(); }; The constructor sets the page's text by referencing a string resource ID: CWizIntroPage::CWizIntroPage() : CPropertyPageImpl<CWizIntroPage>(IDS_WIZARD_TITLE) { } The string int CWizIntroPage::OnSetActive() { SetWizardButtons ( PSWIZB_NEXT ); return 0; } To implement the wizard, we'll create a class class COptionsWizard : public CPropertySheetImpl<COptionsWizard> { public: // Construction COptionsWizard ( HWND hWndParent = NULL ); // Maps BEGIN_MSG_MAP(COptionsWizard) CHAIN_MSG_MAP(CPropertySheetImpl<COptionsWizard>) END_MSG_MAP() // Property pages 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 DDVTo 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
The implementation for this page is in the class 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() // Notification handlers int OnSetActive(); BOOL OnKillActive(); // DDX vars int m_nColor; protected: bool m_bFailDDV; };
int CWizBkColorPage::OnSetActive() { SetWizardButtons ( PSWIZB_BACK | PSWIZB_NEXT ); return 0; } int CWizBkColorPage::OnKillActive() { if ( !DoDataExchange(true) ) return TRUE; // prevent deactivation if ( m_bFailDDV ) { MessageBox ( _T("Error box checked, wizard will stay on this page."), _T("PSheets"), MB_ICONERROR ); return TRUE; // prevent deactivation } return FALSE; // allow deactivation } Note that the logic in The wizard in the sample project has two more pages, Other UI ConsiderationsCentering a sheetThe 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 Here is the code that we can add to class COptionsSheet : public CPropertySheetImpl<COptionsSheet> { //... BEGIN_MSG_MAP(COptionsSheet) MSG_WM_SHOWWINDOW(OnShowWindow) CHAIN_MSG_MAP(CPropertySheetImpl<COptionsSheet>) END_MSG_MAP() // Message handlers void OnShowWindow(BOOL bShowing, int nReason); protected: bool m_bCentered; // set to false in the ctor }; void COptionsSheet::OnShowWindow(BOOL bShowing, int nReason) { if ( bShowing && !m_bCentered ) { m_bCentered = true; CenterWindow ( m_psh.hwndParent ); } } Adding icons to pagesTo 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 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 CBackgroundOptsPage::CBackgroundOptsPage()
{
m_psp.dwFlags |= PSP_USEICONID;
m_psp.pszIcon = MAKEINTRESOURCE(IDI_TABICON);
m_psp.hInstance = _Module.GetResourceInstance();
}
And here's the result:
Up NextIn Part 9, I'll cover WTL's utility classes, and its wrappers for GDI object and common dialogs. Copyright and licenseThis 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 HistorySeptember 13, 2003: Article first published. Series Navigation: « Part VII (Splitter Windows) | ||||||||||||||||||||||