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

引言:好几年前就读过这个系列了,也曾经有过翻译的念头,都因种种原因作罢。前些日子在网上看到了一位网友对此系列的翻译,虽然看起来要比看原文省劲,但却发现许多处不忠实原文的地方,而且还有一些翻译上的错误,所以就生出了重新翻译的念头。这是第五章,敬请大家指正。

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

链接:上一部分下一部分
第五部分 – 高级对话框 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 节处理消息的反射版本即 OCM_*。属主绘制通知就像 WM_NOTIFY 一样,你既可以在控件的父窗口中处理,也可以将之反射回控件本身。如果你选择了前者,你就可以将消息直接串联到 COwnerDraw

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

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

如果处于某些原因你不想处理某个消息,你可以调用 SetMsgHandled(false) 从而消息会传递下去,到达其后的消息映射中可能存在的某个处理器。此 SetMsgHandled() 函数实际上是 COwnerDraw 的成员,但它的工作与你使用 BEGIN_MSG_MAP_EX() 时的 SetMsgHandled() 一样。

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

 [Owner-drawn button 1 - 7K]

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

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

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

 [Owner-drawn button - 11K]

CCustomDraw

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

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

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

CCustomDraw 也有它自己的可覆盖的 SetMsgHandled() 函数,这和在 COwnerDraw 中的一样。

新的 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 - 12K]

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

CBitmapButton 的方法

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

CBitmapButtonImpl 构造函数

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

SubclassWindow()

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() 来把图像列表关联到按钮上。

工具提示管理

CBitmapButton 支持当鼠标悬停在按钮上时显示一个工具提示。调用 SetToolTipText() 来指定要显示的文字。

设置使用的图像

调用 SetImages() 来告诉按钮图像列表中的哪个图像用于某种按钮状态。 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()

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

SetCheckState() 和 GetCheckState()

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

CheckSelectedItems()

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

下面是 CCheckListViewCtrl 在 ControlMania2 中的样子:

 [Check list ctrl - 12K]

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 的颜色设置自动处理超链接的绘制,而且还支持键盘导航。 CHyperLink 的构造函数没有参数,下面是其余的公有方法。

CHyperLink 方法

CHyperLinkImpl 中包含了实现一个链接的所有代码,不过除非你要覆盖其方法或者消息处理器,否则你只要对你的控件使用 CHyperLink 就可以了。

SubclassWindow()

SubclassWindow() 是一个覆盖后的方法,用以执行子类化,并且初始化类中的内部数据。

文本标签的管理

获取或者设置控件要用的文本。如果你不设置标签文本,控件会使用你在资源编辑器中为静态控件赋予的文字。

超链接的管理

获取或者设置控件被点击时要启动的 URL。如果你不设置超链接,控件会把文本标签作为 URL。

导航

导航到用 SetHyperLink() 设置的或者缺省以窗口文本作为的超链接 URL。

工具提示的管理

没有设置工具提示的方法,因此你需要直接访问 CToolTipCtrl 类型的成员: m_tip

下面是 ControlMania2 对话框中超链接控件看起来的样子:

 [WTL hyperlink - 12K]

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

对话框控件的 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 并验证数字是在给定的最小值和最大值之间。

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

 [Season selector edit box - 13K]

由于季节的合法值是 1 到 7(译者注:原文如此,尚不知原因,难道这个家伙疯了,或者我没有找到 season 的另一个意思?好在不影响本文宏旨),故 DDV 宏看起来是这样:

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

处理 DDV 失败

如果一个控件的数据验证失败了, CWinDataExchange 会调用可覆盖函数 OnDataValidateError()。由于缺省的实现只是在扬声器里发出哔响,所以你可能会希望提供更为友好的错误提示。 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 日:首次发布

链接:上一部分下一部分

发表回复

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