引言:好几年前就读过这个系列了,也曾经有过翻译的念头,都因种种原因作罢。前些日子在网上看到了一位网友对此系列的翻译,虽然看起来要比看原文省劲,但却发现许多处不忠实原文的地方,而且还有一些翻译上的错误,所以就生出了重新翻译的念头。这是第八章,敬请大家指正。
特别注 1:由于本页内容栏宽度不够,会导致部分内容看不见,请点击这里以获得最佳浏览效果。
特别注 2:本文为第一版,要浏览第二版请点击这里。
链接:上一部分
内容
- 简介
- 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 <code>参数里传入的文本之前。
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 按钮,在属性表里它没有任何意义。在 Styles 标签里,将 Style 改为 Child,将 Border 改为 Thin,同时保留 Title bar 为被选中的状态。在 More Styles 标签,选中 Disabled。
第二步,也是最后一步,是在 OnAppAbout() 处理器中创建属性表。我们使用不可定制的 CPropertySheet 和 CPropertyPage 来做这件事:
1 2 3 4 5 6 7 8 9 |
LRESULT CMainFrame::OnAppAbout(...) { CPropertySheet sheet ( _T("About PSheets") ); CPropertyPage<IDD_ABOUTBOX> pgAbout; sheet.AddPage ( pgAbout ); sheet.DoModal(); return 0; } |
结果看起来是这样的:
创建一个有用的属性页
因为并不是每个属性表的属性页都和 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 CMainFrame::OnOptions ( UINT uCode, int nID, HWND hwndCtrl ) { CPropertySheet sheet ( _T("PSheets Options"), 0 ); CBackgroundOptsPage pgBackground; CPropertyPage<IDD_ABOUTBOX> pgAbout; pgBackground.m_nColor = m_view.m_nColor; pgBackground.m_nPicture = m_view.m_nPicture; sheet.m_psh.dwFlags |= PSH_NOAPPLYNOW; sheet.AddPage ( pgBackground ); sheet.AddPage ( pgAbout ); if ( IDOK == sheet.DoModal() ) m_view.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 CAppPropertySheet : public CPropertySheetImpl<CAppPropertySheet> { public: // Construction CAppPropertySheet ( _U_STRINGorID title = (LPCTSTR) NULL, UINT uStartPage = 0, HWND hWndParent = NULL ); // Maps BEGIN_MSG_MAP(CAppPropertySheet) CHAIN_MSG_MAP(CPropertySheetImpl<CAppPropertySheet>) END_MSG_MAP() // Property pages CBackgroundOptsPage m_pgBackground; CPropertyPage<IDD_ABOUTBOX> m_pgAbout; }; |
有了这个类,我们就把诸如表中有哪些页之类的细节移到了属性表自身里。构造函数处理以下事宜:把属性页添加到属性表里,并设置其它必要的标志:
1 2 3 4 5 6 7 8 9 |
CAppPropertySheet::CAppPropertySheet ( _U_STRINGorID title, UINT uStartPage, HWND hWndParent ) : CPropertySheetImpl<CAppPropertySheet> ( title, uStartPage, hWndParent ) { m_psh.dwFlags |= PSH_NOAPPLYNOW; AddPage ( m_pgBackground ); AddPage ( m_pgAbout ); } |
这样就使 OnOptions() 处理器变得简单了一点:
1 2 3 4 5 6 7 8 9 10 11 |
void CMainFrame::OnOptions ( UINT uCode, int nID, HWND hwndCtrl ) { CAppPropertySheet sheet ( _T("PSheets Options"), 0 ); sheet.m_pgBackground.m_nColor = m_view.m_nColor; sheet.m_pgBackground.m_nPicture = m_view.m_nPicture; if ( IDOK == sheet.DoModal() ) m_view.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 的构造函数与 CAppPropertySheet 的很相似,在其中,它设置了所有必须的风格位或者标志,并向表中添加了页。
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 ); } |
接下来,在 CMainFrame 中为 Tools|Wizard 菜单写就的处理器如下:
1 2 3 4 5 6 |
void CMainFrame::OnOptionsWizard ( UINT uCode, int nID, HWND hwndCtrl ) { COptionsWizard wizard; wizard.DoModal(); } |
下面就是运行着的向导:
添加更多的页,处理 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: // some stuff removed for brevity... 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: int 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 考虑
居中属性表
属性页和向导的缺省行为会显示到接近其父窗口的左上角的位置:
这看起来很随意,幸好我们还有补救的办法。我考虑过的第一个方法是覆盖 CPropertySheetImpl::PropSheetCallback() 并在该函数中将属性表居中。和 PropSheetProc() 一样, PropSheetCallback() 也是 MSDN 里介绍的一个回调函数。OS 在属性表将被创建时调用此函数,而 WTL 则在这个时候子类化属性表窗口。所以,我们首先来做这样的尝试:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 |
class CAppPropertySheet : public CPropertySheetImpl<CAppPropertySheet> { //... static int CALLBACK PropSheetCallback(HWND hWnd, UINT uMsg, LPARAM lParam) { int nRet = CPropertySheetImpl<CAppPropertySheet>::PropSheetCallback ( hWnd, uMsg, lParam ); if ( PSCB_INITIALIZED == uMsg ) { // center sheet... somehow? } return nRet; } }; |
正如你可以看到的,我们陷入了困境。 PropSheetCallback() 是一个静态方法,其中没有我们可以用来访问属性表窗口的 this 指针。OK,那我们把 CPropertySheetImpl::PropSheetCallback() 里的代码复制过来,再把我们自己的加进去怎样?暂时撇开这会把我们的代码和特定版本的 WTL 绑在一起的问题(你应该总是得到警告,告诉你这不是个好办法),则代码应该是这样:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 |
class CAppPropertySheet : public CPropertySheetImpl<CAppPropertySheet> { //... static int CALLBACK PropSheetCallback(HWND hWnd, UINT uMsg, LPARAM) { if(uMsg == PSCB_INITIALIZED) { <b>// Code copied from WTL and tweaked to use CAppPropertySheet // instead of T:</b> ATLASSERT(hWnd != NULL); CAppPropertySheet* pT = (CAppPropertySheet*) _Module.ExtractCreateWndData(); // subclass the sheet window pT->SubclassWindow(hWnd); // remove page handles array pT->_CleanUpPages(); </span><span style="font-family: Courier New;"><b>// Our own code follows: pT->CenterWindow ( pT->m_psh.hwndParent );</b> } return 0; } }; |
在理论上,这样做看起来很不错,不过我试着用它的时候,发现属性表的位置根本不曾动过。显然,在我们调用 CenterWindow() 之后,公用控件的代码再次改变了属性表的位置。
所以,在放弃这一可以完全封装在属性表类中的优秀解决方案之后,只有配合使用属性表和属性页来居中了。我添加了一个用户定义消息,名为 UWM_CENTER_SHEET:
1 |
#define UWM_CENTER_SHEET WM_APP |
CAppPropertySheet 在其消息映射中对此消息进行了处理:
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 |
class CAppPropertySheet : public CPropertySheetImpl<CAppPropertySheet> { //... BEGIN_MSG_MAP(CAppPropertySheet) MESSAGE_HANDLER_EX(UWM_CENTER_SHEET, OnPageInit) CHAIN_MSG_MAP(CPropertySheetImpl<CAppPropertySheet>) END_MSG_MAP() // Message handlers LRESULT OnPageInit ( UINT, WPARAM, LPARAM ); protected: bool m_bCentered; // set to false in the ctor }; LRESULT CAppPropertySheet::OnPageInit ( UINT, WPARAM, LPARAM ) { if ( !m_bCentered ) { m_bCentered = true; CenterWindow ( m_psh.hwndParent ); } return 0; } |
然后,每个页的 OnInitDialog() 方法都会向属性表发送此消息:
1 2 3 4 5 6 7 |
BOOL CBackgroundOptsPage::OnInitDialog ( HWND hwndFocus, LPARAM lParam ) { <b>GetPropertySheet().SendMessage ( UWM_CENTER_SHEET );</b> DoDataExchange(false); return TRUE; } |
属性表里的 m_bCentered 标志可以确保只有第一个 UWM_CENTER_SHEET 消息才起作用。
为属性页添加图标
要使用尚未被成员函数封装起来的属性表和属性页的特性,你就需要直接访问相关的结构: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 日:首次发布
链接:上一部分