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

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

链接:上一部分下一部分

第五部分 – 高级对话框 UI 类

内容

  • 第五部分简介
  • 属主绘制(Owner Draw)以及定制绘制(Custom Draw)的专用类
    • COwnerDraw
    • CCustomDraw
  • 新的 WTL 控件
    • CBitmapButton
    • CCheckListViewCtrl
    • CTreeViewCtrlEx 和 CTreeItem
    • CHyperLink
  • 对话框控件的 UI 更新
  • DDV
    • 处理 DDV 失败
  • 改变对话框的大小
  • 下一步
  • 参考资料
  • 修订历史

第五部分简介

在上一部分里,我们了解了一些关于对话框和控件的 WTL 特性,其工作方式与 MFC 中的对应类很相似。在本部分里,我们会介绍几个新的 WTL 类,它们实现了一些更加高级的 UI 特性:属主绘制(Owner draw)和定制绘制(Custom draw),新的 WTL 控件,UI 更新,以及对话框数据验证(DDV)。

属主绘制以及定制绘制的专用类

由于属主绘制和定制绘制在 GUI 工作中已经变得相当的普遍,于是 WTL 提供几个嵌入类来处理这些烦人的事。接下来我们会逐一介绍它们,作为我们的上一个示例工程的续集,现在我们从 ControlMania2 开始。如果你是随着 AppWizard 来创建工程,就要确保你的对话框是非模态的。为了使 UI 更新能正常工作,这是必须的。在 UI 更新一节中,我会给出更多的细节。

COwnerDraw

属主绘制包括对四个消息的处理: WM_MEASUREITEMWM_DRAWITEMWM_COMPAREITEMWM_DELETEITEM。而在 atlframe.h 中定义的 COwnerDraw 类则可以简化你的代码。这是因为你不必再需要为这些消息写处理器了,而只需把消息串联到 COwnerDraw,后者就会调用在你的类中实现的覆盖函数。

如何串联消息取决于你是否把消息反射回了控件。下面是 COwnerDraw 的消息映射,它清楚地显示出了其中的差异:

映射的主体节来处理 WM_* 消息,而 ALT_MSG_MAP(1) 节处理消息的反射版本即 OCM_*。属主绘制通知就像 WM_NOTIFY 一样,你既可以在控件的父窗口中处理,也可以将之反射回控件本身。如果你选择了前者,你就可以将消息直接串联到 COwnerDraw

不过如果你希望控件处理消息,你就要使用 CHAIN_MSG_MAP_ALT 宏把消息串联到 ALT_MSG_MAP(1) 节:

COwnerDraw 拆解随消息发送来的参数,然后调用你的类中的实现函数。在上例中,该类实现了 DrawItem(), 那么它在 WM_DRAWITEM 或者 OCM_DRAWITEM 串联到 COwnerDraw 的时候就会被调用到。你可以覆盖的方法有:

如果出于某些原因你不想处理某个消息,你可以调用 SetMsgHandled(false) 从而消息会传递下去,到达其后的消息映射中可能存在的某个处理器。

对于 ControlMania2,我们将从 ControlMania1 中的树控件开始,并添加一个属主绘制按钮,然后在按钮类中处理反射的 WM_DRAWITEM。下面就是资源编辑器中的新按钮:

 [Owner-drawn button 1 - 7K]

现在,我们还需要一个实现此按钮的类:

DrawItem() 使用诸如 BitBlt() 这样的 GDI 调用来在按钮表面上绘制一个图片。代码应该很容易理解,因为 WTL 的类名和方法又一次类似于 MFC。

下面是按钮看起来的样子:

 [Owner-drawn button - 19K]

CCustomDraw

CCustomDrawCOwnerDraw 的工作方法类似,不过它是让你处理并串联 NM_CUSTOMDRAW 消息的。 CCustomDraw 针对每个定制绘制的步骤都有一个可覆盖的方法:

它们的缺省处理全都返回 CDRF_DODEFAULT,因此,如果你需要进行自己的绘制或者是返回其他不同的值,你覆盖其一即可。

你可能在最后一个截图上已经注意到了,“Dawn” 是用绿色显示的。这是由于使用了一个从 CTreeCtrl 派生来的新类(源代码中名为 CBuffyTreeCtrl)把消息串联到了 CCustomDraw 并覆盖了 OnPrePaint()OnItemPrePaint() 方法。在树填充之后,该节点的附加数据被设置成了 1,而 OnItemPrePaint() 会检查此值,并且一旦发现就会改变文本的颜色。

和在 COwnerDraw 中的一样,在定制绘制的消息处理器中你也可以调用 SetMsgHandled(false) 函数以使得消息可以传递到消息映射中的其它处理器那儿。

新的 WTL 控件

WTL 有它自己的几个新控件,有的是对其他封装类的增强(比如 CTreeViewCtrlEx),有的则是提供了内建控件所没有的新功能(比如 CHyperLink)。

CBitmapButton

WTL 的 CBitmapButton,声明于 atlctrlx.h 中,较之于 MFC 版本更加易用。WTL 版本使用了图像列表控件而非四个独立的位图资源,这就意味着你可以在一个位图中保存多个按钮的图像,从而多少降低了一些 GDI 资源占用。如果你的应用有很多的图像而又正好运行于 Windows 9x,那这一点就尤为注目,因为使用大量的独立图像会很快耗尽 GDI 资源并使系统完蛋。

CBitmapButton 是一个 CWindowImpl 派生类,拥有众多特性:自动调整控件大小,自动生成三维边框,热追踪(hot-tracking)支持,每个按钮可以使用多个图像用以区别按钮所处的不同状态。

在 ControlMania2 中,我们将在前面所创建的属主绘制按钮旁边创建一个 CBitmapButton。首先,我们向 CMainDlg 添加一个名为 m_wndBmpBtnCBitmapButton 成员。然后再用常用的方法把它连接到新按钮上,既可以调用 SubclassWindow() 也可以使用 DDX。把一幅位图加载到图像列表中,再告诉按钮将要使用此列表。然后告诉按钮列表中的那个图像对应哪一种控件状态。下面是从 OnInitDialog() 中取来的一段设置按钮的代码:

缺省情况下,按钮会假定它拥有该图像列表,因此 OnInitDialog() 千万不能删除它所创建的图像列表。下面是缺省状态下的新按钮。可以看到该控件的大小已经改变,正好适合图像的大小。

 [WTL bitmap button - 19K]

因为 CBitmapButton 是一个非常有用的类,所以我在这儿会介绍它的公用方法。

CBitmapButton 的方法

CBitmapButtonImpl 类包含了实现一个按钮的所有代码,不过除非你要覆盖某个方法或者消息处理器,你可以为你的控件使用 CBitmapButton 类。

CBitmapButtonImpl 构造函数

构造函数设置按钮的扩展风格(不要与他的窗口风格混淆),而且还可以关联一个图像列表。通常使用缺省值就足够了,因为你可以用其他的方法来设置这些属性。

SubclassWindow() 是一个覆盖版本的方法,用来执行子类化并初始化本类拥有的内部数据。

CBitmapButton 支持一些影响其外观或者操作的扩展风格:

BMPBTN_HOVER
启用热追踪。当鼠标光标位于按钮上的时候,按钮会呈现为其聚焦状态。
BMPBTN_AUTO3D_SINGLE, BMPBTN_AUTO3D_DOUBLE
自动围绕图像生成一个三维边框,以及拥有焦点时的聚焦矩形。另外,如果你没有提供按下状态的图像,则也会为你生成一个。 BMPBTN_AUTO3D_DOUBLE 生成一个略为厚实一点的边框。
BMPBTN_AUTOSIZE
使按钮的大小自动匹配图像的大小。此风格是缺省风格。
BMPBTN_SHAREIMAGELISTS
如果设置了此风格,按钮对象不会销毁用以容纳按钮图像的图像列表。如果没有设置,则图像列表会在 CBitmapButton 的析构函数中销毁。
BMPBTN_AUTOFIRE
如果设置了此风格,点击按钮并保持鼠标键为按下状态会连续产生 WM_COMMAND 消息。

当调用 SetBitmapButtonExtendedStyle() 时, dwMask 参数控制了受影响的风格,缺省值 0 会使新的风格完全取代旧的风格。

调用 SetImageList()GetImageList() 可以把图像列表关联到按钮上,或者取得当前关联于按钮的图像列表。

CBitmapButton 支持当鼠标悬停在按钮上时显示一个工具提示。调用 SetImageList()GetToolTipText() 来指定或者获取工具提示上显示的文字。

调用 SetImages() 来告诉按钮图像列表中的哪个图像用于某种按钮状态。所有参数都是基于 0 的图像列表中的索引值。 nNormal 是必需的,其他的都是可选的。传递 -1 表示对于该状态没有图像。

CCheckListViewCtrl

CCheckListViewCtrl,定义于 atlctrlx.h 中,是 CWindowImpl 的一个派生类,它实现了带有复选框的列表视图控件。此类不同于 MFC 的 CCheckListBox,后者使用的是列表框而不是列表视图。 CCheckListViewCtrl 相当简单,因为它只添加了少许属于自己的功能。不过,它还引入了一个新的辅助类,名为 CCheckListViewCtrlImplTraits,该类与 CWinTraits 相似但有第三个模板参数,该参数表示控件要使用的扩展列表视图风格。如果你没有定义自己的 CCheckListViewCtrlImplTraits 组合,那么该类将会使用这些缺省值: LVS_EX_CHECKBOXES | LVS_EX_FULLROWSELECT

下面是一个特点(traits)定义的示例,使用了不同的扩展视图列表风格,以及一个使用这些特点的新类。(注意,必须在扩展列表视图风格中包括 LVS_EX_CHECKBOXES,否则会得到一个断言失败的消息)。

CCheckListViewCtrl 方法

当你子类化一个现有的列表视图控件时, SubclassWindow() 查看相关的 CCheckListViewCtrlImplTraits 中的扩展列表视图风格,并将之应用到控件上。前两个模板参数(窗口风格以及扩展窗口风格)并未使用。

这两个方法实际上在 CListViewCtrl 中。 SetCheckState() 接受一个条目索引以及指示是否勾选该条目的布尔值。 GetCheckState() 只接受一个索引,而后返回该条目的当前勾选状态。

此方法接受一个条目索引。它切换该条目的勾选(Check)状态,该条目必须已经被选定(Select),并且同时改变其他所有已选定条目的选中状态。你自己通常不会使用此方法,因为 CCheckListViewCtrl 在复选框被点击或者用户按下空格键时会处理条目的选中事宜。

下面是 CCheckListViewCtrl 在 ControlMania2 中的样子:

 [Check list ctrl - 22K]

CTreeViewCtrlEx 和 CTreeItem

通过封装 HTREEITEM,这两个类使得使用树控件的功能更为方便。一个 CTreeItem 对象中保存了一个 HTREEITEM 和一个指向包含此项的树控件的指针,这样你就只需使用 CTreeItem 来操纵该项而不必每次都引用树控件。 CTreeViewCtrlExCTreeViewCtrl 相像,但它的方法是处理 CTreeItem 而非 HTREEITEM。因此,假如你调用 InsertItem(),则它会返回一个 CTreeItem 而不是 HTREEITEM,然后你就可以使用 CTreeItem 操控此新插入的项。下面是一个例子:

CTreeItem 对于每个接受 HTREEITEMCTreeViewCtrl 方法都有一个对应的方法,就像 CWindow 对应每个接受 HWND 的 API 都有方法一样。在 ControlMania2 的代码里有对 CTreeViewCtrlExCTreeItem 的更多方法的演示。

CHyperLink

CHyperLink 是一个 CWindowImpl 派生类,可以子类化一个静态文本控件使之成为可点击的超链接。 CHyperLink 根据用户的 IE 的颜色设置自动处理超链接的绘制,而且还支持键盘导航。类 CHyperLinkImplCHyperLinkImpl 的基类,其中包含了实现一个链接的所有代码,不过除非你要覆盖其方法或者消息处理器,否则你只要对你的控件使用 CHyperLink 就可以了。

CHyperLink 的缺省行为是当链接被点击时在缺省浏览器里启动一个 URL。如果子类化了的静态控件具有 WS_TABSTOP 风格,你还可以用 TAB 键转到该控件并按下空格键或者回车键来点击此链接。 CHyperLink 还可以在光标悬停于链接上的时候显示一个工具提示。缺省情况下, CHyperLink 使用静态控件的文本作为 URL 和工具提示的缺省文字,不过你也可以使用后文介绍的方法来改变这些属性。

在 WTL 7.1 里,为 CHyperLink 添加了很多特性,这些新特性可以使用扩展风格来启用。方法列表之后解释了这些风格及其用法。

CHyperLink 方法

这些是常用的 CHyperLink 方法,另外的一些用于计算控件的大小,解析链接文本等等,可以到 atlctrlx.h 里查看此类的完整方法列表。用以执行子类化,并且初始化类中的内部数据。 CHyperLink 的构造函数没有参数,下面是其余的公有方法。

CHyperLinkImpl 的构造函数接收用于控件的扩展风格。 CHyperLink 缺少相对应的构造函数,不过你可以使用 SetHyperLinkExtendedStyle() 来设置这些风格。

SubclassWindow() 是一个覆盖后的方法,用以执行子类化,并且初始化类中的内部数据。如果你使用 DDX_CONTROL 把一个超链接变量关联到了静态控件上,那么此方法会被自动调用;当然你也可以自己手动调用来子类化一个控件。

获取或者设置控件的扩展风格。你必须在调用 SubclassWindow() 或者 Create() 之前设置扩展风格,这样控件才能知道怎么绘制文本。

获取或者设置控件要用的文本。如果你不设置标签文本,控件会把标签文本设置为静态控件的窗口文字。

获取或者设置关联到控件的 URL。如果你不设置超链接,控件会把超链接设置为静态控件的窗口文字。

获取或者设置光标悬停于链接之上时显示在工具提示里的文本。不过,只有链接具有 HLINK_COMMANDBUTTON 或者 HLINK_NOTIFYBUTTON 扩展风格时才能使用这些方法。后文有关于工具提示的更多信息。

下面是一个“普通的”超链接控件在 ControlMania2 对话框中看起来的样子:

 [WTL hyperlink - 21K]

其 URL 在 OnInitDialog() 中通过以下调用做了设置:

CHyperLink 扩展风格

新的 WTL 7.1 的特性可以通过设置相应的扩展风格位来启用。这些风格有:

HLINK_UNDERLINED
链接文本具有下划线。这是缺省行为。
HLINK_NOTUNDERLINED
链接文本从不使用下划线。
HLINK_UNDERLINEHOVER
链接文本只有在光标悬停在链接上时才有下划线。
HLINK_COMMANDBUTTON
链接被点击时,控件向其父窗口发送一个 WM_COMMAND 消息(命令码设置为 BN_CLICKED)。
HLINK_NOTIFYBUTTON
链接被点击时,控件向其父窗口发送一个 WM_NOTIFY 消息(通知码设置为 NM_CLICK)。
HLINK_USETAGS
控件仅将 <a> 标签内的文字认为是链接,其它文本正常绘制。
HLINK_USETAGSBOLD
HLINK_USETAGS 一样,但是 <a> 标签内的文本以粗体绘制。当设置了此风格时,下划线扩展风格会被忽略,链接文本总不会出现下划线。
HLINK_NOTOOLTIP
控件不显示工具提示。

如果既没有设置 HLINK_COMMANDBUTTON 风格也没有设置 HLINK_NOTIFYBUTTON 风格,则 CHyperLink 对象在被点击后调用其 Navigate() 方法。 Navigate() 调用 ShellExecuteEx() 在缺省浏览器里启动一个 URL。如果你想链接被点击时执行其他的动作,那就设置 HLINK_COMMANDBUTTON 或者 HLINK_NOTIFYBUTTON,然后再处理控件发出的通知消息。

CHyperLink 的其它细节

你可以为静态控件设置 SS_CENTER 或者 SS_RIGHT 风格以使得超链接的文本可以居中或者右对齐。但是如果控件具有 HLINK_USETAGS 或者 HLINK_USETAGSBOLD 风格的话,这些位就会被忽略,而文本总是左对齐的。

如果你使用 CHyperLink 来打开一个 URL,也就是说,没有设置 HLINK_COMMANDBUTTON 或者 HLINK_NOTIFYBUTTON,你就不能用 SetToolTipText() 来改变工具提示的文本。但是,你可以通过 CHyperLink 的成员 m_tip 来直接访问工具提示控件,并使用 AddTool() 来设置文本:

请注意,这儿有一个和 WTL 7.0 不兼容的改变: CHyperLink 在 WTL 7.1 里使用了值是 1 的工具 ID。在 WTL 7.0 里,该 ID 与窗口句柄相同,你可以使用 m_tip.UpdateTipText() 来更改文字。在 WTL 7.1 里没有运气用 UpdateTipText() 了,上面的代码重复了 CHyperLink::Init() 所做的事情,重新设置了工具提示。

由于一些绘制上的问题, HLINK_USETAGSHLINK_USETAGSBOLD 风格最好是在链接的文本总在一行上的时候使用。绘制代码寻找位于 <a> 标签内的文本,并把整个文本分为三个部分:标签之前、标签之内、标签之后。但是,如果某一部分的文字需要断词(word-breaking),那么回绕(wrap)就会不正确。我在 ControlMania2 里的一个单独的对话框里展示了这个问题:

 [Link drawing problems - 13K]

你应该确保 HLINK_UNDERLINEHOVER 不会和 HLINK_USETAGSBOLD 一起设置,因为那会导致一些空白出现在链接文本之后,就好像上图中的第一个超链接一样。

对话框控件的 UI 更新

在对话框里进行控件的 UI 更新比在 MFC 中要简单得多。在 MFC 里,你必须知道未文档化的 WM_KICKIDLE 消息,如何去处理它以及如何触发控件更新。在 WTL 中完全用不着这些窍门,不过 AppWizard 中有一个小错误会需要你添加一行代码。

第一件需要记住的事情是对话框必须是非模态的。这是因为要使 CUpdateUI 能工作,你的应用就必须控制着消息循环。如果你把对话框做成了模态的,那么系统就会处理消息循环,因此空闲处理器就不会被调用。而 CUpdateUI 是在空闲时间内工作,所以没有了空闲处理也就意味着不会有 UI 更新。

ControlMania2 的对话框是非模态的,其类定义的起始部分很像是一个框架窗口类:

可以看到 CMainDlg 派生于 CUpdateUI 并有一个 UI 更新映射。 OnInitDialog() 中的下列代码,你会再次觉得和先前的框架窗口例子一样熟悉:

这一次,我们调用的既非 UIAddToolbar() 也非 UIAddStatusBar(),而是 UIAddChildWindowContainer()。这会告诉 CUpdateUI 我们的对话框包含有需要更新的子窗口。如果你看一下 OnIdle(),说不定你会怀疑漏掉了什么东西:

你可能期望这里会有另外的一个 CUpdateUI 的方法调用去做实际的更新。你想的很对,的确应该有,但 AppWizard 漏掉了一行代码。你需要把它补入到 OnIdle() 中去:

出于演示 UI 更新的目的,当你点击左边的位图按钮时,右边的按钮会被启用或者禁用。所以首先,我们在 UI 更新映射中添加一个入口,使用 UPDUI_CHILDWINDOW 标志以表明此入口用于一个子窗口:

然后在左边按钮的处理器中,我们调用 UIEnable() 以切换另一个按钮的启用状态:

DDV

WTL 中对话框数据验证(DDV)的支持较 MFC 而言要简单一点。在 MFC 里,你要为 DDX(把数据传送到变量)和 DDV(验证数据)分别创建单独的宏,而在 WTL 里,一个宏就把这些全搞定。WTL 通过在 DDX 映射里使用下面的宏来提供基本的 DDV 支持:

DDX_TEXT_LEN
DDX_TEXT 一样作 DDX 并验证字符串的长度(不包括空结束符)是小于还是等于指定的限制。
DDX_INT_RANGEDDX_UINT_RANGE
它们像 DDX_INTDDX_UINT 一样作 DDX,并验证数字是在给定的最小值和最大值之间。
DDX_FLOAT_RANGE
DDX_FLOAT 一样作 DDX 并验证数字是在给定的最小值和最大值之间。
DDX_FLOAT_P_RANGE (WTL 7.1 中新加)
DDX_FLOAT_P 一样作 DDX 并验证数字是在给定的最小值和最大值之间。

这些宏的参数与对应的无验证功能的宏很像,附带上了一两个表明可接受范围的参数。 DDX_TEXT_LEN 接收一个参数,是允许的最大长度;其它的都是接收两个参数,表示允许的最小值和最大值。

ControlMania2 有一个 ID 为 IDC_FAV_SEASON 的编辑框,并绑定到了成员变量 m_nSeason 上。

 [Season selector edit box - 26K]

由于 Buffy 共有七季,故其合法值是 1 到 7(译者注:Buffy,全称“Buffy the Vampire Slayer”,是一部美国电视连续剧,中译名《捉鬼者巴菲》),故 DDV 宏看起来是这样:

OnOK() 调用 DoDataExchange() 来验证季的数值,而且作为 DoDataExchange() 所完成的工作的一部分, m_nSeason 被赋予了值。

处理 DDV 失败

如果一个控件的数据验证失败了, CWinDataExchange 会调用可覆盖函数 OnDataValidateError() 而且 OnDataValidateError() 会返回 false。由于缺省的实现只是在扬声器里发出哔响,所以你可能会希望提供更为友好的错误提示。 OnDataValidateError() 的原型是:

_XData 是一个结构, CWinDataExchange 会把输入的数据以及允许的范围等细节填入其中。下面是此结构的定义:

nDataType 标示着三个成员中的哪一个是有意义的。其可能的值为:

在我们当前的情况下, nDataType 应该是 ddxDataInt,说明 _XData 中的 _XIntData 成员被填入了内容。 _XIntData 是一个简单的结构:

我们覆盖后的 OnDataValidateError() 会显示一个错误消息来告诉用户允许的范围是什么:

查看 atlddx.h 可以看到 _XData 结构里的其他数据类型 – _XTextData_XFloatData

改变对话框的大小

WTL 引起我注意的首要的几件事情之一就是其对可变大小的对话框的内建支持。此前,我曾就此主题写过一篇文章,欲知详情请参考该文。概要地讲,你要做的就是把 CDialogResize 类添加到对话框的继承列表中,在 OnInitDialog() 中调用 DlgResize_Init(),然后把消息串联到 CDialogResize

下一步

在下一篇文章中,我们来看一下在对话框中掌控 ActiveX 控件,以及如何处理控件激发的事件。

参考资料

Using WTL’s Built-in Dialog Resizing Class – Michael Dunn
Using DDX and DDV with WTL – Less Wright

修订历史

2003 年 4 月 28 日:首次发布
2005 年 12 月 31 日:更新,包括了 WTL 7.1 中的改动

链接:上一部分下一部分

发表评论

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