特别注:由于本页内容栏宽度不够,会导致部分内容看不见,请点击这里以获得最佳浏览效果。
内容
- 简介
- WTL 属性表类
- CPropertySheetImpl 的方法
- WTL 属性页类
- CPropertyPageWindow 的方法
- CPropertyPageImpl 的方法
- 处理通知消息
- 创建一个属性表
- 永远最简单的属性表
- 创建一个有用的属性页
- 创建一个更好的属性表类
- 创建一个向导
- 添加更多的页,处理 DDV
- 其他的 UI 考虑
- 居中属性表
- 为属性页添加图标
- 下一步
- 修订历史
简介
甚至于在 Windows 95 把属性表引入为公用控件之前,它就已经成为了呈现选项的一种颇为流行的方法了。向导通常用于指导用户通历软件的安装过程或者其他的复杂工作。WTL 对创建这两种类型的属性表都提供了良好的支持,并允许你使用前 面介绍过的所有的那些对话框相关的特性,比如说 DDX 和 DDV。在本章里,我会演示创建一个基本的属性表和向导,以及如何处理由表发送出的事件和通知消息。
WTL 属性表类
有两个类, CPropertySheetWindow 和 CPropertySheetImpl,它们组合起来实现了属性表。它们都定义在 atldlgs.h 头文件中。 CPropertySheetWindow 是一个窗口接口类,也就是说,它派生于 CWindow,而 CPropertySheetImpl 具有消息映射并实际上实现了窗口功能。这与基本的 ATL 窗口类是一致的,在其中 CWindow 和 CWindowImpl 也被一起使用。
CPropertySheetWindow 包括了对多个 PSM_* 消息的封装,比如 SetActivePageByID() 就封装了 PSM_SETCURSELID。 CPropertySheetImpl 管理着一个 PROPSHEETHEADER 结构以及一组 HPROPSHEETPAGE。 CPropertySheetImpl 中还有一些方法,用于设置某些 PROPSHEETHEADER 域,添加以及删除页。你也可以通过访问 m_psh 成员变量来获得对 PROPSHEETHEADER 的直接访问。
最后要说明的是, CPropertySheet 是 CPropertySheetImpl 的一个特化,如果你根本不需要对属性表进行定制的话,那你就可以使用它。
CPropertySheetImpl 的方法
下面是 CPropertySheetImpl 的一些重要的方法。因为许多方法仅仅是对窗口消息的封装,我不会在这儿列一个详尽的清单,但你可以到 atldlgs.h 里查看方法的完整列表。
1 2 |
CPropertySheetImpl(_U_STRINGorID title = (LPCTSTR) NULL, UINT uStartPage = 0, HWND hWndParent = NULL) |
CPropertySheetImpl 的构造函数允许你当下就指定一些常用的属性,这样你就不必在以后调用别的方法来设置它们。 title 指定了在属性表的标题上用到的文字。 _U_STRINGorID 是一个 WTL 辅助类,可以使你传递 LPCTSTR 或者字符串资源 ID。例如,如果 IDS_SHEET_TITLE 是字符串表中某个字符串的 ID 的话,那么下面这两行就都可以工作:
1 2 |
CPropertySheetImpl mySheet ( IDS_SHEET_TITLE ); CPropertySheetImpl mySheet ( _T("My prop sheet") ); |
uStartPage 是从零开始的页面索引值,当属性表第一次显示时,该页就会被激活。 hWndParent 设置了属性表的父窗口。
1 2 |
BOOL AddPage(HPROPSHEETPAGE hPage) BOOL AddPage(LPCPROPSHEETPAGE pPage) |
向属性表中添加一个属性页。如果该页已经创建的话,你可以将其句柄(一个 HPROPSHEETPAGE)传递给第一个重载版本的函数。更常见的方法是使用第二个重载版本。使用这一版本,你可以先设置一个 PROPSHEETPAGE 结构(此事可以由 CPropertyPageImpl 来做,后文会有介绍),而 CPropertySheetImpl 会为你创建并管理该页。
1 2 |
BOOL RemovePage(HPROPSHEETPAGE hPage) BOOL RemovePage(int nPageIndex) |
从属性表中删除一个属性页。你既可以传递属性页的句柄也可以传递基于零的索引值。
1 2 |
BOOL SetActivePage(HPROPSHEETPAGE hPage) BOOL SetActivePage(int nPageIndex) |
设置属性表的活动属性页。你既可以传递要激活的页的句柄,也可以传递基于零的索引。你也可以在创建属性表之前调用此方法,以设置属性表首次显示时要激活哪一页。
1 |
void SetTitle(LPCTSTR lpszText, UINT nStyle = 0) |
设置属性表标题使用的文字。nStyle 可以是 0 或者 PSH_PROPTITLE。如果是 PSH_PROPTITLE,则此风格会被加到属性表上,就会使“Properties for”字样附加到你在 lpszText 参数里传入的文本之前。
1 |
void SetWizardMode() |
设置 PSH_WIZARD 风格,这会使得属性表变成一个向导。你必须在显示属性表之前调用此方法。
1 |
void EnableHelp() |
设置 PSH_HASHELP 风格,它会为属性表加入帮助按钮。注意,你还需要在为此按钮提供帮助的各个页中启用帮助才能有效。
1 |
INT_PTR DoModal(HWND hWndParent = ::GetActiveWindow()) |
创建并显示一个模态属性表。正的返回值表示成功,对于返回值的完整描述可以参看 PropertySheet() API 的文档。如果有错误发生且属性表没有创建成功, DoModal() 会返回 -1。
1 |
HWND Create(HWND hWndParent = NULL) |
创建并显示一个非模态的属性表,并返回其窗口句柄。如果发生了错误,属性表就创建不出来, Create() 会返回 NULL。
WTL 属性页类
和属性表类相似,此 WTL 类封装了属性页的相关工作,也是既有一个窗口接口类, CPropertyPageWindow,又有一个实现类 CPropertyPageImpl。 CPropertyPageWindow 非常小,其包含的大部分辅助函数都调用了父表中的方法。
CPropertyPageImpl 派生于 CDialogImplBaseT,这是因为一个属性页是用一个对话框资源来构建的。这也意味着,我们在对话框里所用到的所有 WTL 特性在属性表中都是可用的,比如说 DDX 和 DDV。 CPropertyPageImpl 有两个主要的目的:管理保存在成员变量 m_psp 中的 PROPSHEETPAGE 结构,并处理 PSN_* 通知消息。对于非常简单的属性页,你可以使用 CPropertyPage 类。这仅仅适用于那些根本不与用户交互的页,比方说一个 About 页,或者是向导里的简介页面。
你还可以创建掌控 ActiveX 控件的页。首先,你要把 atlhost.h 包含到 stdafx.h 中去,而对于该页,你要使用 CAxPropertyPageImpl 代替 CPropertyPageImpl。对于要掌控 ActiveX 控件的简单页,你可以使用 CAxPropertyPage 来代替 CPropertyPage。
CPropertyPageWindow 的方法
CPropertyPageWindow 最重要的方法是 GetPropertySheet():
1 |
CPropertySheetWindow GetPropertySheet() |
此方法获取属性页的父窗口(也即属性表)的 HWND 并将之关联到一个 CPropertySheetWindow 上。再把新的 CPropertySheetWindow 返回给调用者。请注意,此处只是创建一个临时对象,它并不是返回用来创建属性表的实际的 CPropertySheet 或者 CPropertySheetImpl 对象的指针或者是引用。如果你是使用自己的 CPropertySheetImpl 派生类,而又要在属性表对象中访问数据成员的话,这一点就很重要。
剩下的成员仅仅是调用封装了 PSM_* 消息的 CPropertySheetWindow 函数:
1 2 3 4 5 6 7 |
BOOL Apply() void CancelToClose() void SetModified(BOOL bChanged = TRUE) LRESULT QuerySiblings(WPARAM wParam, LPARAM lParam) void RebootSystem() void RestartWindows() void SetWizardButtons(DWORD dwFlags) |
例如,在 CPropertyPageImpl 派生类里,你可以调用:
1 |
SetWizardButtons ( PSWIZB_BACK | PSWIZB_FINISH ); |
用以代替:
1 2 3 4 |
CPropertySheetWindow wndSheet; wndSheet = GetPropertySheet(); wndSheet.SetWizardButtons ( PSWIZB_BACK | PSWIZB_FINISH ); |
CPropertyPageImpl 的方法
CPropertyPageImpl 管理着一个 PROPSHEETPAGE 结构,即其公用成员 m_psp。 CPropertyPageImpl 还有一个 operator PROPSHEETPAGE* 转换器,因此你可以把一个 CPropertyPageImpl 传递到接受 LPPROPSHEETPAGE 或者 LPCPROPSHEETPAGE 参数的方法里,比如 CPropertySheetImpl::AddPage()。
CPropertyPageImpl 的构造函数允许你设置页的标题,也即出现在页的标签上的文字:
1 |
CPropertyPageImpl(_U_STRINGorID title = (LPCTSTR) NULL) |
如果你需要手动创建一个页,而不是让属性表来干这件事的话,你可以调用 Create():
1 |
HPROPSHEETPAGE Create() |
Create() 只不过是使用 m_psp 作为参数调用了 CreatePropertySheetPage()。你只有在以下情况下才需要调用 Create(),或者是在属性表创建之后需要向其上添加一个页,或者是要把创建的页传递给其他不受你控制的属性表,例如,一个属性表处理器的外壳扩展。
还有三个方法,可以设置页面内的几处标题文字:
1 2 3 |
void SetTitle(_U_STRINGorID title) void SetHeaderTitle(LPCTSTR lpstrHeaderTitle) void SetHeaderSubTitle(LPCTSTR lpstrHeaderSubTitle) |
第一个函数用于改变该页的标签上的文字。其他的两个用在 Wizard97 风格的向导中,用于设置属性页上方的题头区域内的文字。
1 |
void EnableHelp() |
在 m_psp 中设置 PSP_HASHELP 标志,当页面激活时即可以启用 Help 按钮。
处理通知消息
CPropertyPageImpl 中有一个消息映射,它处理了 WM_NOTIFY。如果通知代码是一个 PSN_* 值, OnNotify() 会调用用于此通知的特定的处理器。这由编译时虚函数技术完成,因而在派生类里可以很轻易地覆盖该处理器。
共有两组通知处理器,这是由于 WTL 3 和 7 之间出现了设计上的变化。在 WTL 3 里,通知处理器会返回一个不同于 PSN_* 消息的返回值的值。比如,WTL 3 中 PSN_WIZFINISH 的处理器:
1 2 3 |
case PSN_WIZFINISH: lResult = !pT->OnWizardFinish(); break; |
OnWizardFinish() 希望返回 TRUE 以允许向导结束,或者返回 FALSE 来阻止向导的关闭;但是,由于 IE 5 的公用控件添入了可以从 PSN_WIZFINISH 的处理器返回一个窗口句柄的能力用以设置焦点,所以这就行不通了。WTL 3 的应用不能使用此特性,因为所有的非零值都被认为是一样的。
在 WTL 7 里, OnNotify() 不会改变从 PSN_* 处理器返回的任何值。处理器可以返回任何文档化的合法值,因而其行为也就完全正常了。但是,出于后向兼容的考虑,WTL 3 的处理器仍然存在并且被缺省使用。要使用 WTL 7 的处理器,你必须把下列行添加到 stdafx.h 中,而且位于 atldlgs.h 的包含语句之前:
1 |
#define _WTL_NEW_PAGE_NOTIFY_HANDLERS |
在写新代码的时候,显然没有什么理由不使用 WTL 7 的处理器,所以在这就不介绍 WTL 3 的处理器了。
CPropertyPageImpl 对所有的通知都有缺省的处理器,所以你可以只覆盖和你的程序相关的那些处理器。缺省处理器及其行为如下:
int OnSetActive() – 允许属性页成为活动的
BOOL OnKillActive() – 允许属性页成为非活动的
int OnApply() – 返回表示应用操作已成功的 PSNRET_NOERROR
void OnReset() – 无操作
BOOL OnQueryCancel() – 允许取消操作
int OnWizardBack() – 到上一页
int OnWizardNext() – 到下一页
INT_PTR OnWizardFinish() – 允许向导结束
void OnHelp() – 无操作
BOOL OnGetObject(LPNMOBJECTNOTIFY lpObjectNotify) – 无操作
int OnTranslateAccelerator(LPMSG lpMsg) – 返回表示消息没有被处理的 PSNRET_NOERROR
HWND OnQueryInitialFocus(HWND hWndFocus) – 返回 NULL 以把焦点设置到 Tab 顺序里的第一个控件上
创建一个属性表
现在,我们有关类的介绍就结束了,我们需要有一个程序来演示如何使用它们。本章的示例工程是一个简单的 SDI 应用,它在其客户区要显示一个图片,并使用颜色填充背景。图片和颜色可以通过选项对话框(一个属性表)以及一个向导(后文叙述)来更改。
永远最简单的属性表
使用 WTL AppWizard 生成一个 SDI 工程后,我们就可以开始创建用于 About 框的属性表了。我们首先从向导为我们生成的 about 对话框开始,要改变其风格才能使它像一个属性页一样工作。
第一步是移除 OK 按钮,在属性表里它没有任何意义。在对话框的属性里,将 Style 改为 Child,将 Border 改为 Thin,并选中 Disabled。
第二步,也是最后一步,是在 OnAppAbout() 处理器中创建属性表。我们可以使用不可定制的 CPropertySheet 和 CPropertyPage 来做这件事:
1 2 3 4 5 6 7 8 |
void CMainFrame::OnAppAbout(...) { CPropertySheet sheet ( _T("About PSheets") ); CPropertyPage<IDD_ABOUTBOX> pgAbout; sheet.AddPage ( pgAbout ); sheet.DoModal ( *this ); } |
结果看起来是这样的:
创建一个有用的属性页
因为并不是每个属性表的属性页都和 About 框一样简单,所以大部分的页都会需要是一个 CPropertyPageImpl 的派生类,所以我们现在就来看一下这样的一个类。我们要创建一个新的属性页,其中包含了显示在客户区背景中的图象的设置。对话框如下:
此对话框和 About 页的风格一样。对于此页,我们需要一个新类,将其命名为 CBackgroundOptsPage。此类派生于 CPropertyPageImpl,因为它毕竟是一个属性页,同时也派生于 CWinDataExchange,这样可以启用 DDX。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 |
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; }; |
此类中需要注意的有:
- 其中有一个名为 IDD 的公用成员,里面存放着相关联的对话框资源 ID。
- 其消息映射与 CDialogImpl 类相似。
- 消息映射会把消息串联到 CPropertyPageImpl,从而可以处理属性表相关的消息。
- 其中有一个 OnApply() 处理器,当用户点击属性表上的 OK 时可以保存用户的选择。
OnApply() 相当简单,它调用 DoDataExchange() 来更新 DDX 变量,然后再返回一个代码,指示属性表是否可以关闭:
1 2 3 4 |
int CBackgroundOptsPage::OnApply() { return DoDataExchange(true) ? PSNRET_NOERROR : PSNRET_INVALID; } |
添加一个 Tools|Options 菜单项,让它来把属性表搬出来,我们把此命令的处理器放到视图类中。此处理器像前面一样创建属性表,不过要把新的 CBackgroundOptsPage 添加到属性表里。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 |
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 ); } |
sheet 的构造函数的第二个参数现在是 0,这表示在开始的时候应该看到索引为 0 的页。你可以把此值改为 1 使得属性表出现时首先看到的是 About 页。因为这仅仅是演示代码,我计划偷个懒,让 CBackgroundOptsPage 中连接到单选按钮的变量成为共有的。视图会把当前的选项存放在这些变量里,如果用户点击了属性表的 OK,那就把这些新的值保存起来。
如果用户点击了 OK, DoModal() 会返回 IDOK,于是视图就会使用的新的图片和颜色重绘自己。下面是不同视图的屏幕截图:
创建一个更好的属性表类
OnOptions() 处理器创建的属性表确实不错,但是那一大堆设置和初始化代码,却不应该是 CMainFrame 的职责。更好的方法是从 CPropertySheetImpl 派生一个类,由它来处理这些工作。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 |
#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; }; |
有了这个类,我们就把诸如表中有哪些页之类的细节移到了属性表自身里。构造函数处理以下事宜:把属性页添加到属性表里,并设置其它必要的标志:
1 2 3 4 5 6 7 8 9 |
COptionsSheet::CAppPropertySheet ( _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 ); } |
这样就使 OnOptions() 处理器变得简单了一点:
1 2 3 4 5 6 7 8 9 10 11 |
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 ); } |
创建一个向导
创建一个向导,没什么可惊讶的,和创建属性表很相仿。有一点要多做的工作是启用 Back 和 Next 按钮;像在 MFC 的属性页里那样,你要覆盖 OnSetActive() 并调用 SetWizardButtons() 来启用适当的按钮。我们由一个简单的简介页开始,其 ID 设置为 IDD_WIZARD_INTRO:
留意一下,此页没有标题文字。由于向导中的每个页通常都具有相同的标题,我建议在 CPropertySheetImpl 的构造函数里设置标题,并让每一页都使用相同的字符串资源。这样,就可以只改动一个字符串而使每一页都能反映出来。
此页的实现都在 CWizIntroPage 类里:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 |
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(); }; |
其构造函数通过引用一个字符串资源 ID 来设置页的标题:
1 2 3 4 |
CWizIntroPage::CWizIntroPage() : CPropertyPageImpl<CWizIntroPage>( IDS_WIZARD_TITLE ) { } |
在本页成为当前页的时候,字符串 IDS_WIZARD_TITLE(“PSheets Options Wizard”)就会出现在向导的标题栏内。 OnSetActive() 仅仅启用 Next 按钮:
1 2 3 4 5 |
int CWizIntroPage::OnSetActive() { SetWizardButtons ( PSWIZB_NEXT ); return 0; } |
为了实现此向导,我们需要创建一个 COptionsWizard 类,并在应用的菜单里添加一个 Tools|Wizard 菜单选项。 COptionsWizard 的构造函数与 COptionsSheet 的很相似,在其中,它设置了所有必须的风格位或者标志,并向表中添加了页。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 |
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 ); } |
接下来,为 Tools|Wizard 菜单写就的处理器如下:
1 2 3 4 5 6 |
void CPSheetsView::OnOptionsWizard ( UINT uCode, int nID, HWND hwndCtrl ) { COptionsWizard wizard; wizard.DoModal( GetTopLevelParent() ); } |
下面就是运行着的向导:
添加更多的页,处理 DDV
为了让它成为一个有点用处的向导,我们将给它添加一个新的页,可以设置视图的背景颜色。这一页上还有一个复选框,用于演示处理 DDV 失败并阻止用户继续下去的情况。下面就是这个新页,其 ID 为 IDD_WIZARD_BKCOLOR:
此页的实现是在 CWizBkColorPage 类里。下面是相关的部分代码:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 |
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; }; |
OnSetActive() 的工作方式与简介页一样,不过它会把 Back 和 Next 按钮全部启用。 OnKillActive() 是一个新的处理器,它首先执行 DDX,然后再检查 m_bFailDDV 的值,如果为 true,也即复选框是被选中的, OnKillActive() 会阻止向导运行到下一页。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 |
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 } |
注意,在 OnKillActive() 里的逻辑当然也可以放到 OnWizardNext() 里,这两个处理器都可以将向导停留在当前页上。其不同之处在于 OnKillActive() 是在用户点击了 Back 或者 Next 之时被调用,而 OnWizardNext(),正如其名字所昭示的,仅在用户点击 Next 时被调用。 OnWizardNext() 还可以用作其它用途,如果某些页是可以跳过的,则它可以将向导直接导向另一个不同的页而不是顺序里的下一页。
示例工程里的向导还有另外的两个页 CWizBkPicturePage 和 CWizFinishPage。因为它们和上述的两页类似,故此处不再赘述,要想得到全部的细节可以参看示例代码。
其他的 UI 考虑
居中属性表
属性页和向导的缺省行为会显示到接近其父窗口的左上角的位置:
这看起来很随意,幸好我们还有补救的办法。感谢那些在论坛里提供做这件事情的代码的人们,本文的前一个版本使用了一种更为复杂的方法。
属性表类或者向导类可以处理 WM_SHOWWINDOW 消息。 WM_SHOWWINDOW 的 wParam 参数是一个布尔值,表示窗口是否要被显示出来。如果 wParam 为 true,而且窗口又是第一次显示,那就调用 CenterWindow()。
下面就是我们可以加入到 COptionsSheet 类里来居中属性表的代码。 m_bCentered 成员用于跟踪属性表是否已经居中过了。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 |
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 ); } } |
为属性页添加图标
要使用尚未被成员函数封装起来的属性表和属性页的特性,你就需要直接访问相关的结构:CPropertySheetImpl 里的 PROPSHEETHEADER 类型的成员 m_psh,或者 CPropertyPageImpl 里的 PROPSHEETPAGE 类型的成员 m_psp。
例如,要为选项属性表的 Background 页添加图标的话,我们就需要添加一个标志,并在该页的 PROPSHEETPAGE 结构里设置几个其他的成员:
1 2 3 4 5 6 |
CBackgroundOptsPage::CBackgroundOptsPage() { m_psp.dwFlags |= PSP_USEICONID; m_psp.pszIcon = MAKEINTRESOURCE(IDI_TABICON); m_psp.hInstance = _Module.GetResourceInstance(); } |
下面是结果:
下一步
在第九部分里,我会介绍 WTL 的辅助类,以及 GDI 对象和公用对话框的封装类。
修订历史
2003 年 9 月 13 日:首次发布
2006 年 1 月 13 日:把选项属性表/向导的代码移到了视图类中。更新了居中属性表一节。