译:MFC 程序员的 WTL 教程(八)(第二版)

特别注:由于本页内容栏宽度不够,会导致部分内容看不见,请点击这里以获得最佳浏览效果。

链接:上一部分下一部分

第八部分 – 属性表和向导

内容

  • 简介
  • WTL 属性表类
    • CPropertySheetImpl 的方法
  • WTL 属性页类
    • CPropertyPageWindow 的方法
    • CPropertyPageImpl 的方法
    • 处理通知消息
  • 创建一个属性表
    • 永远最简单的属性表
    • 创建一个有用的属性页
    • 创建一个更好的属性表类
  • 创建一个向导
    • 添加更多的页,处理 DDV
  • 其他的 UI 考虑
    • 居中属性表
    • 为属性页添加图标
  • 下一步
  • 修订历史

简介

甚至于在 Windows 95 把属性表引入为公用控件之前,它就已经成为了呈现选项的一种颇为流行的方法了。向导通常用于指导用户通历软件的安装过程或者其他的复杂工作。WTL 对创建这两种类型的属性表都提供了良好的支持,并允许你使用前 面介绍过的所有的那些对话框相关的特性,比如说 DDX 和 DDV。在本章里,我会演示创建一个基本的属性表和向导,以及如何处理由表发送出的事件和通知消息。

WTL 属性表类

有两个类, CPropertySheetWindowCPropertySheetImpl,它们组合起来实现了属性表。它们都定义在 atldlgs.h 头文件中。 CPropertySheetWindow 是一个窗口接口类,也就是说,它派生于 CWindow,而 CPropertySheetImpl 具有消息映射并实际上实现了窗口功能。这与基本的 ATL 窗口类是一致的,在其中 CWindowCWindowImpl 也被一起使用。

CPropertySheetWindow 包括了对多个 PSM_* 消息的封装,比如 SetActivePageByID() 就封装了 PSM_SETCURSELIDCPropertySheetImpl 管理着一个 PROPSHEETHEADER 结构以及一组 HPROPSHEETPAGECPropertySheetImpl 中还有一些方法,用于设置某些 PROPSHEETHEADER 域,添加以及删除页。你也可以通过访问 m_psh 成员变量来获得对 PROPSHEETHEADER 的直接访问。

最后要说明的是, CPropertySheetCPropertySheetImpl 的一个特化,如果你根本不需要对属性表进行定制的话,那你就可以使用它。

CPropertySheetImpl 的方法

下面是 CPropertySheetImpl 的一些重要的方法。因为许多方法仅仅是对窗口消息的封装,我不会在这儿列一个详尽的清单,但你可以到 atldlgs.h 里查看方法的完整列表。

CPropertySheetImpl 的构造函数允许你当下就指定一些常用的属性,这样你就不必在以后调用别的方法来设置它们。 title 指定了在属性表的标题上用到的文字。 _U_STRINGorID 是一个 WTL 辅助类,可以使你传递 LPCTSTR 或者字符串资源 ID。例如,如果 IDS_SHEET_TITLE 是字符串表中某个字符串的 ID 的话,那么下面这两行就都可以工作:

uStartPage 是从零开始的页面索引值,当属性表第一次显示时,该页就会被激活。 hWndParent 设置了属性表的父窗口。

向属性表中添加一个属性页。如果该页已经创建的话,你可以将其句柄(一个 HPROPSHEETPAGE)传递给第一个重载版本的函数。更常见的方法是使用第二个重载版本。使用这一版本,你可以先设置一个 PROPSHEETPAGE 结构(此事可以由 CPropertyPageImpl 来做,后文会有介绍),而 CPropertySheetImpl 会为你创建并管理该页。

从属性表中删除一个属性页。你既可以传递属性页的句柄也可以传递基于零的索引值。

设置属性表的活动属性页。你既可以传递要激活的页的句柄,也可以传递基于零的索引。你也可以在创建属性表之前调用此方法,以设置属性表首次显示时要激活哪一页。

设置属性表标题使用的文字。nStyle 可以是 0 或者 PSH_PROPTITLE。如果是 PSH_PROPTITLE,则此风格会被加到属性表上,就会使“Properties for”字样附加到你在 lpszText 参数里传入的文本之前。

设置 PSH_WIZARD 风格,这会使得属性表变成一个向导。你必须在显示属性表之前调用此方法。

设置 PSH_HASHELP 风格,它会为属性表加入帮助按钮。注意,你还需要在为此按钮提供帮助的各个页中启用帮助才能有效。

创建并显示一个模态属性表。正的返回值表示成功,对于返回值的完整描述可以参看 PropertySheet() API 的文档。如果有错误发生且属性表没有创建成功, DoModal() 会返回 -1。

创建并显示一个非模态的属性表,并返回其窗口句柄。如果发生了错误,属性表就创建不出来, Create() 会返回 NULL。

WTL 属性页类

和属性表类相似,此 WTL 类封装了属性页的相关工作,也是既有一个窗口接口类, CPropertyPageWindow,又有一个实现类 CPropertyPageImplCPropertyPageWindow 非常小,其包含的大部分辅助函数都调用了父表中的方法。

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()

此方法获取属性页的父窗口(也即属性表)的 HWND 并将之关联到一个 CPropertySheetWindow 上。再把新的 CPropertySheetWindow 返回给调用者。请注意,此处只是创建一个临时对象,它并不是返回用来创建属性表的实际的 CPropertySheet 或者 CPropertySheetImpl 对象的指针或者是引用。如果你是使用自己的 CPropertySheetImpl 派生类,而又要在属性表对象中访问数据成员的话,这一点就很重要。

剩下的成员仅仅是调用封装了 PSM_* 消息的 CPropertySheetWindow 函数:

例如,在 CPropertyPageImpl 派生类里,你可以调用:

用以代替:

CPropertyPageImpl 的方法

CPropertyPageImpl 管理着一个 PROPSHEETPAGE 结构,即其公用成员 m_pspCPropertyPageImpl 还有一个 operator PROPSHEETPAGE* 转换器,因此你可以把一个 CPropertyPageImpl 传递到接受 LPPROPSHEETPAGE 或者 LPCPROPSHEETPAGE 参数的方法里,比如 CPropertySheetImpl::AddPage()

CPropertyPageImpl 的构造函数允许你设置页的标题,也即出现在页的标签上的文字:

如果你需要手动创建一个页,而不是让属性表来干这件事的话,你可以调用 Create()

Create() 只不过是使用 m_psp 作为参数调用了 CreatePropertySheetPage()。你只有在以下情况下才需要调用 Create(),或者是在属性表创建之后需要向其上添加一个页,或者是要把创建的页传递给其他不受你控制的属性表,例如,一个属性表处理器的外壳扩展

还有三个方法,可以设置页面内的几处标题文字:

第一个函数用于改变该页的标签上的文字。其他的两个用在 Wizard97 风格的向导中,用于设置属性页上方的题头区域内的文字。

m_psp 中设置 PSP_HASHELP 标志,当页面激活时即可以启用 Help 按钮。

处理通知消息

CPropertyPageImpl 中有一个消息映射,它处理了 WM_NOTIFY。如果通知代码是一个 PSN_* 值, OnNotify() 会调用用于此通知的特定的处理器。这由编译时虚函数技术完成,因而在派生类里可以很轻易地覆盖该处理器。

共有两组通知处理器,这是由于 WTL 3 和 7 之间出现了设计上的变化。在 WTL 3 里,通知处理器会返回一个不同于 PSN_* 消息的返回值的值。比如,WTL 3 中 PSN_WIZFINISH 的处理器:

OnWizardFinish() 希望返回 TRUE 以允许向导结束,或者返回 FALSE 来阻止向导的关闭;但是,由于 IE 5 的公用控件添入了可以从 PSN_WIZFINISH 的处理器返回一个窗口句柄的能力用以设置焦点,所以这就行不通了。WTL 3 的应用不能使用此特性,因为所有的非零值都被认为是一样的。

在 WTL 7 里, OnNotify() 不会改变从 PSN_* 处理器返回的任何值。处理器可以返回任何文档化的合法值,因而其行为也就完全正常了。但是,出于后向兼容的考虑,WTL 3 的处理器仍然存在并且被缺省使用。要使用 WTL 7 的处理器,你必须把下列行添加到 stdafx.h 中,而且位于 atldlgs.h 的包含语句之前:

在写新代码的时候,显然没有什么理由不使用 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() 处理器中创建属性表。我们可以使用不可定制的 CPropertySheetCPropertyPage 来做这件事:

结果看起来是这样的:

 [Simple sheet - 27K]

创建一个有用的属性页

因为并不是每个属性表的属性页都和 About 框一样简单,所以大部分的页都会需要是一个 CPropertyPageImpl 的派生类,所以我们现在就来看一下这样的一个类。我们要创建一个新的属性页,其中包含了显示在客户区背景中的图象的设置。对话框如下:

 [Background options - 4K]

此对话框和 About 页的风格一样。对于此页,我们需要一个新类,将其命名为 CBackgroundOptsPage。此类派生于 CPropertyPageImpl,因为它毕竟是一个属性页,同时也派生于 CWinDataExchange,这样可以启用 DDX。

此类中需要注意的有:

  • 其中有一个名为 IDD 的公用成员,里面存放着相关联的对话框资源 ID。
  • 其消息映射与 CDialogImpl 类相似。
  • 消息映射会把消息串联到 CPropertyPageImpl,从而可以处理属性表相关的消息。
  • 其中有一个 OnApply() 处理器,当用户点击属性表上的 OK 时可以保存用户的选择。

OnApply() 相当简单,它调用 DoDataExchange() 来更新 DDX 变量,然后再返回一个代码,指示属性表是否可以关闭:

添加一个 Tools|Options 菜单项,让它来把属性表搬出来,我们把此命令的处理器放到视图类中。此处理器像前面一样创建属性表,不过要把新的 CBackgroundOptsPage 添加到属性表里。

sheet 的构造函数的第二个参数现在是 0,这表示在开始的时候应该看到索引为 0 的页。你可以把此值改为 1 使得属性表出现时首先看到的是 About 页。因为这仅仅是演示代码,我计划偷个懒,让 CBackgroundOptsPage 中连接到单选按钮的变量成为共有的。视图会把当前的选项存放在这些变量里,如果用户点击了属性表的 OK,那就把这些新的值保存起来。

如果用户点击了 OK, DoModal() 会返回 IDOK,于是视图就会使用的新的图片和颜色重绘自己。下面是不同视图的屏幕截图:

 [Alyson background - 35K]  [Strong Bad background - 9K]

创建一个更好的属性表类

OnOptions() 处理器创建的属性表确实不错,但是那一大堆设置和初始化代码,却不应该是 CMainFrame 的职责。更好的方法是从 CPropertySheetImpl 派生一个类,由它来处理这些工作。

有了这个类,我们就把诸如表中有哪些页之类的细节移到了属性表自身里。构造函数处理以下事宜:把属性页添加到属性表里,并设置其它必要的标志:

这样就使 OnOptions() 处理器变得简单了一点:

创建一个向导

创建一个向导,没什么可惊讶的,和创建属性表很相仿。有一点要多做的工作是启用 BackNext 按钮;像在 MFC 的属性页里那样,你要覆盖 OnSetActive() 并调用 SetWizardButtons() 来启用适当的按钮。我们由一个简单的简介页开始,其 ID 设置为 IDD_WIZARD_INTRO

 [Intro page - 3K]

留意一下,此页没有标题文字。由于向导中的每个页通常都具有相同的标题,我建议在 CPropertySheetImpl 的构造函数里设置标题,并让每一页都使用相同的字符串资源。这样,就可以只改动一个字符串而使每一页都能反映出来。

此页的实现都在 CWizIntroPage 类里:

其构造函数通过引用一个字符串资源 ID 来设置页的标题:

在本页成为当前页的时候,字符串 IDS_WIZARD_TITLE(“PSheets Options Wizard”)就会出现在向导的标题栏内。 OnSetActive() 仅仅启用 Next 按钮:

为了实现此向导,我们需要创建一个 COptionsWizard 类,并在应用的菜单里添加一个 Tools|Wizard 菜单选项。 COptionsWizard 的构造函数与 COptionsSheet 的很相似,在其中,它设置了所有必须的风格位或者标志,并向表中添加了页。

接下来,为 Tools|Wizard 菜单写就的处理器如下:

下面就是运行着的向导:

 [Wizard on intro page - 10K]

添加更多的页,处理 DDV

为了让它成为一个有点用处的向导,我们将给它添加一个新的页,可以设置视图的背景颜色。这一页上还有一个复选框,用于演示处理 DDV 失败并阻止用户继续下去的情况。下面就是这个新页,其 ID 为 IDD_WIZARD_BKCOLOR

 [Color selection wizard page - 4K]

此页的实现是在 CWizBkColorPage 类里。下面是相关的部分代码:

OnSetActive() 的工作方式与简介页一样,不过它会把 BackNext 按钮全部启用。 OnKillActive() 是一个新的处理器,它首先执行 DDX,然后再检查 m_bFailDDV 的值,如果为 true,也即复选框是被选中的, OnKillActive() 会阻止向导运行到下一页。

注意,在 OnKillActive() 里的逻辑当然也可以放到 OnWizardNext() 里,这两个处理器都可以将向导停留在当前页上。其不同之处在于 OnKillActive() 是在用户点击了 Back 或者 Next 之时被调用,而 OnWizardNext(),正如其名字所昭示的,仅在用户点击 Next 时被调用。 OnWizardNext() 还可以用作其它用途,如果某些页是可以跳过的,则它可以将向导直接导向另一个不同的页而不是顺序里的下一页。

示例工程里的向导还有另外的两个页 CWizBkPicturePageCWizFinishPage。因为它们和上述的两页类似,故此处不再赘述,要想得到全部的细节可以参看示例代码。

其他的 UI 考虑

居中属性表

属性页和向导的缺省行为会显示到接近其父窗口的左上角的位置:

 [sheet position - 19K]

这看起来很随意,幸好我们还有补救的办法。感谢那些在论坛里提供做这件事情的代码的人们,本文的前一个版本使用了一种更为复杂的方法。

属性表类或者向导类可以处理 WM_SHOWWINDOW 消息。 WM_SHOWWINDOWwParam 参数是一个布尔值,表示窗口是否要被显示出来。如果 wParamtrue,而且窗口又是第一次显示,那就调用 CenterWindow()

下面就是我们可以加入到 COptionsSheet 类里来居中属性表的代码。 m_bCentered 成员用于跟踪属性表是否已经居中过了。

为属性页添加图标

要使用尚未被成员函数封装起来的属性表和属性页的特性,你就需要直接访问相关的结构:CPropertySheetImpl 里的 PROPSHEETHEADER 类型的成员 m_psh,或者 CPropertyPageImpl 里的 PROPSHEETPAGE 类型的成员 m_psp

例如,要为选项属性表的 Background 页添加图标的话,我们就需要添加一个标志,并在该页的 PROPSHEETPAGE 结构里设置几个其他的成员:

下面是结果:

 [Tab icon - 5K]

下一步

在第九部分里,我会介绍 WTL 的辅助类,以及 GDI 对象和公用对话框的封装类。

修订历史

2003 年 9 月 13 日:首次发布
2006 年 1 月 13 日:把选项属性表/向导的代码移到了视图类中。更新了居中属性表一节。

链接:上一部分下一部分

发表回复

您的电子邮箱地址不会被公开。 必填项已用*标注