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

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

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

链接:上一部分下一部分

第二部分 – WTL 中的 GUI 基础类

内容

  • 第二部分介绍
  • WTL 综述
  • 开始一个 WTL EXE
  • WTL 消息映射的增强
  • 使用 WTL AppWizard 可以得到什么
    • 通历向导
    • 检查生成的代码
  • CMessageLoop 内幕
  • CFrameWindowImpl 内幕
  • 回到时钟程序
  • UI 更新
    • 控制时钟的新菜单项
    • 调用 UIEnable()
  • 关于消息映射的最后注意事项
  • 下一站,1995
  • 修订历史

第二部分介绍

好,是实实在在地讲述 WTL 的时候了!在这部分里,我会介绍写一个主框架窗口的基础知识,以及 WTL 引入的比较受欢迎的改进,比如 UI 更新和更好的消息影射。为了最大程度地掌握本部分的内容,你应该安装 WTL 以使其头文件处于 VC 的搜索路径中,而且 AppWizard 也在适当的目录下。WTL 的分发包中附有如何安装 AppWizard 的说明,请参考该文档。

WTL 综述

WTL 的类可以分为几个主要的类别:

  1. 框架窗口的实现 – CFrameWindowImpl, CMDIFrameWindowImpl
  2. 控件封装 – CButton, CListViewCtrl
  3. GDI 封装 – CDC, CMenu
  4. 特殊的 UI 特性 – CSplitterWindow, CUpdateUI, CDialogResize, CCustomDraw
  5. 工具类以及宏 – CString, CRect, BEGIN_MSG_MAP_EX

本文将深入到框架窗口中去,顺便提及一些 UI 特性和工具类。大多数的类都是独立的,不过也有一些像 CDialogResize 这样的嵌入类(mix-in)。

开始一个 WTL EXE

如果你不使用 WTL AppWizard (稍后我们就会提到它),那么一个 WTL EXE 一开始会很像一个 ATL EXE。如同第一部分中的那样,本文中的示例代码是另一个框架窗口,不过为了展示一些 WTL 的特性,较之前者不再那么微不足道。

在本节里,我们会从头开始一个新的 EXE。主窗口会在其客户区显示当前的时间。下面是一个基本的 stdafx.h:

atlapp.h 是要包含的第一个 WTL 头文件。它包含了用于消息处理的类和一个继承自 CComModule 的类 CAppModule。如果你计划使用 CString 那就还应该定义 _WTL_USE_CSTRING,因为 CString 定义在 atlmisc.h 里,而在 atlmisc.h 包含的其他头文件里有的特性会使用到 CString。定义 _WTL_USE_CSTRING 使得 atlapp.h 会前向声明 CString 类,从而使其他的这些头文件知道一个 CString 究竟是什么。

接下来我们来定义我们的框架窗口。像我们这样的 SDI 窗口继承自 CFrameWindowImpl。窗口类名是使用 DECLARE_FRAME_WND_CLASS 而不是 DECLARE_WND_CLASS 来定义。这儿是 MyWindow.h 里我们窗口定义的开头:

DECLARE_FRAME_WND_CLASS 有两个参数,窗口类名(可以为 NULL,ATL 会替你生成一个类名),和一个资源 ID。WTL 会根据此 ID 去寻找图标、菜单以及加速键表,并在窗口创建时加载它们。还会根据此 ID 寻找一个字符串,然后使用该串作为窗口的标题。我们还把消息串联到 CFrameWindowImpl,因为它有自己的一些消息处理器,尤其是 WM_SIZEWM_DESTROY

现在我们来看 WinMain()。它和第一部分中的 WinMain() 极其类似,只是创建主窗口的调用存在差异。

CFrameWindowImplCreateEx() 方法采用了最常用的缺省值,因而我们不需要指定任何参数。 CFrameWindowImpl 还会处理前文提到的资源加载事宜,所以现在你应该使用 IDR_MAINFRAME 这一 ID 生成一些伪资源,或者使用随本文附带的示例代码。

如果你马上运行,就可以看到主框架窗口了,当然,它实际上还没有任何事情。我们需要加入一些消息处理器来干活儿,所以现在是研究 WTL 消息映射宏的好时机。

WTL 消息映射的增强

在使用Win32 API 时,既令人讨厌又易于出错的事情之一就是从随消息一起发送过来的 WPARAMLPARAM 数据中拆封参数。不幸的是,ATL 并未提供更多的帮助,除去 WM_COMMANDWM_NOTIFY 之外,我们仍然需要从其他所有的消息中拆封数据。不过,WTL 正好在这儿对我们施以援手!

WTL 的增强消息映射宏在 atlcrack.h 文件中(此名字来源于 “message cracker”,是一个应用于 windowsx.h 中类似的宏的术语)。首先,把 BEGIN_MSG_MAP 改成 BEGIN_MSG_MAP_EX_EX 版本会产生一些消息拆解器(message crackers)要用到的代码。

对我们的时钟程序来说,我们需要处理 WM_CREATE 并设置一个定时器。WTL 把针对一个消息的消息处理器命名为 MSG_ 后随消息名,比如 MSG_WM_CREATE。这些宏仅接受处理器的名字。我们来为 WM_CREATE 添加一个处理器:

WTL 的消息处理器看起来很像 MFC,每个处理器都根据随消息传入的参数有一个不同的原型。不过,由于没有向导来写处理器,我们不得不自己来查找原型。幸运的是 VC 可以帮上忙。将光标(注:此处原文错误,不应该是光标[cursor],而应该是插入符[caret])放在 “MSG_WM_CREATE” 文本上再按 F12 会转到宏的定义处。这是我们在此工程中第一次使用此特性,VC 必须重新编译以构建浏览信息数据库。这一工作一旦完成,VC 就会在 MSG_WM_CREATE 的定义处打开 atlcrack.h:

红色的是最重要的一行,那是对处理器的实际调用,它告诉我们处理器会返回一个 LRESULT 并接受一个 LPCREATESTRUCT 类型的参数。注意,没有像 ATL 的宏所使用的 bHandled 参数。 SetMsgHandled() 函数替代了该参数,很快我们就要解释这件事情。

现在我们可以为窗口类添加一个 OnCreate() 处理器:

CFrameWindowImpl 间接地从 CWindow 派生而来,因此它具有所有 CWindow 的函数,例如 SetTimer()。这使得调用窗口 API 看起来很像 MFC 代码,在 MFC 里你可以使用许多封装了 API 的 CWnd 方法。

我们调用 SetTimer() 来创建一个每秒(1000 毫秒)激发的定时器。因为我们还想让 CFrameWindowImpl 也能处理 WM_CREATE,所以调用了 SetMsgHandled(false) 从而消息可以通过 CHAIN_MSG_MAP 宏串联到基类。这一调用代替了 ATL 宏所使用的 bHandled 参数。(即使是 CFrameWindowImpl 不处理 WM_CREATE,在使用了基类的时候调用 SetMsgHandled(false) 也是一个好习惯,这样你可以不去记基类处理了哪些消息。与 ClassWizard 生成的代码类似,大部分的处理器在开始或者结束都有对基类处理器的调用。)

我们还需要一个 WM_DESTROY 处理器来停止定时器。执行以上相同的流程,可以找到 MSG_WM_DESTROY 宏,看起来就是这样:

因此我们的 OnDestroy() 处理器既没有参数也没有返回值。 CFrameWindowImpl 的确也处理了 WM_DESTROY,所以这儿仍然需要调用 SetMsgHandled(false)

接着是每秒钟调用一次的 WM_TIMER 处理器。现在你应该已经对 F12 这一技巧很熟悉了,所以我们只呈现处理器本身:

这一处理器仅仅重绘窗口以使新的时间显示在客户区内。最后,我们来处理 WM_ERASEBKGND,在相应的处理器中,把当前时间绘制在客户区的左上角。

此处理器演示了 GDI 的封装类之一, CDCHandle,以及 CRectCString。关于 CString 我想说的是,它和 MFC 的 CString 其实是一样的。我将在稍后讲到这些封装类,不过现在你可以将 CDCHandle 仅仅视为对 HDC 的一个简单封装,就像 MFC 的 CDC 那样。只是当 CDCHandle 离开作用域时,它不会销毁内含的设备上下文。

最后,这就是我们的窗口:

 [clock window - 4K]

示例代码中还有为菜单项加入的 WM_COMMAND 处理器,在这儿我不会讲它们,但是你可以打开示例工程,看一下 WTL 的 COMMAND_ID_HANDLER_EX 宏是怎么运作的。

使用 WTL AppWizard 可以得到什么

WTL 分发包带了一个相当棒的 AppWizard。我们来看一下它可以向 SDI 应用中添加哪些特性。

通历向导

点击 VC 的 File|New 并在列表里选择 ATL/WTL AppWizard。我们来重写时钟程序,输入 WTLClock 作为工程名字:

 [AppWiz screen 1 - 14K]

在接下来的页面里,可以选择是 SDI、MDI 还是基于对话框的应用,以及一些其他的选项。选择下面显示的选项并点击 Next

 [AppWiz screen 2 - 22K]

最后一页里我们可以选择拥有工具栏,复用栏以及状态栏。为了保证应用的简单,去掉所有这些选择并点击 Finish

 [AppWiz screen 3 - 21K]

检查生成的代码

向导结束后,在生成的代码里你会看到三个类: CMainFrameCAboutDlgCWTLClockView。从名字里你就可以猜出每个类的作用。尽管有一个 “view” 类,不过它却是从 CWindowImpl 派生而来的一个 “普通” 窗口,而没有像 MFC 的文档/视图架构中的框架窗口。

还有一个函数是 _tWinMain(),它初始化 COM、公用控件以及 _Module,之后再调用一个全局的 Run() 函数。 Run() 会创建主窗口并开始消息泵,它还使用了一个新类 CMessageLoopRun() 调用 CMessageLoop::Run(),确切地说是后者包含了消息泵。在下一节里我们将了解 CMessageLoop 的更多细节。

CAboutDlg 是一个简单的 CDialogImpl 派生类,它关联到一个 ID 为 IDD_ABOUTBOX 的对话框上。我在第一部分里谈到了对话框,所以你应该能够理解 CAboutDlg 的代码。

CWTLClockView 是我们这一应用的 “view” 类。它工作起来像是一个 MFC 视图,没有标题栏,占据着主框架的客户区。 CWTLClockView 有一个 PreTranslateMessage() 函数,它工作起来也像是 MFC 中的同名函数。再有就是 WM_PAINT 处理器。目前还没有哪个函数在做举足轻重的事情,但我们即将填写 OnPaint() 方法来显示时间。

最后,我们还有 CMainFrame,它有许多有趣的新东西。下面是类定义的一个简化版本:

CMessageFilter 是一个提供了 PreTranslateMessage() 的嵌入类, CIdleHandler 是另一个嵌入类,它提供了 OnIdle()CMessageLoopCIdleHandler 以及 CUpdateUI 一起工作以提供像 MFC 的 ON_UPDATE_COMMAND_UI 那样的 UI 更新功能。

CMainFrame::OnCreate() 创建视图窗口并保存了其窗口句柄,所以当主窗口的大小变化时视图窗口的大小也随之变化。 OnCreate() 还把 CMainFrame 对象添加到由 CAppModule 维护的消息过滤器列表和空闲处理列表中。稍后会介绍这些内容。

m_hWndClientCFrameWindowImpl 的一个成员,也就是在框架窗口大小改变时要相应改变大小的窗口。

生成的 CMainFrame 还有对 File|NewFile|Exit 以及 Help|About 的处理器。对于我们的时钟程序来说,大多数缺省菜单项都不需要,不过留着也没有什么害处。现在可以编译并运行向导生成的代码了,尽管此应用还不是很有用。你可能会对逐步执行全局 Run() 里的 CMainFrame::CreateEx() 函数感兴趣,可以精确地看到框架窗口及其资源是如何被加载和创建的。

我们的 WTL 游览的下一站是 CMessageLoop,它负责消息泵和空闲处理。

CMessageLoop 内幕

CMessageLoop 为我们的应用程序提供了消息泵。除标准的 DispatchMessage/ TranslateMessage 循环之外,它还通过 PreTranslateMessage() 提供了消息过滤功能,通过 OnIdle() 提供了空闲处理功能。以下是 Run() 逻辑伪代码:

CMessageLoop 知道要调用哪一个 PreTranslateMessage() 函数是因为每个需要过滤消息的类都要像 CMainFrame::OnCreate() 所做的那样调用 CMessageLoop::AddMessageFilter()。与之相仿,需要进行空闲处理的类要调用 CMessageLoop::AddIdleHandler()

注意,在消息循环中没有对 TranslateAccelerator() 或者 IsDialogMessage() 进行调用。 CFrameWindowImpl 处理了前者,不过,如果你要在应用里添加任何非模态对话框,你就需要在 CMainFrame::PreTranslateMessage() 中增加对 IsDialogMessage() 的调用。

CFrameWindowImpl 内幕

CFrameWindowImpl 及其基类 CFrameWindowImplBase 提供了许多 MFC 的 CFrameWnd 具有的特性:工具栏、复用栏(Rebar)、状态栏、用于工具栏按钮的工具提示(Tooltip)以及针对菜单项的动态帮助。我会逐渐将到这些特性,因为完整地讨论 CFrameWindowImpl 类需要占用整整两篇文章!至于眼下,看看 CFrameWindowImpl 是如何处理 WM_SIZE 和客户区就足够了。在此,请记住 m_hWndClientCFrameWindowImplBase 的一个成员,用来存放位于框架中的 “view” 的 HWND

CFrameWindowImplWM_SIZE 的一个处理器:

此函数检查了窗口是不是要被最小化。如果不是,它就派发到 UpdateLayout()。下面是 UpdateLayout()

注意代码是如何引用 m_hWndClient 的。由于 m_hWndClient 是一个普通的 HWND,实际上它可以是任何窗口。此处没有窗口种类的限制,不像 MFC 的某些特性(例如分割窗口)需要 CView 的派生类。如果你回到 CMainFrame::OnCreate(),可以看到它创建了一个视图窗口并将其句柄保存到 m_hWndClient 中,以确保视图可以被正确地改变大小。

回到时钟程序

现在,在看完了框架窗口类的一些细节之后,让我们回到时钟程序上来。就像前例中的 CMyWindow 一样,视图窗口可以处理定时器和绘制。以下是类的部分定义:

注意,只要把 BEGIN_MSG_MAP 改成了 BEGIN_MSG_MAP_EX,那你就可以将 ATL 的消息映射宏和 WTL 版本的混合起来使用。 OnPaint() 里使用了前例中在 OnEraseBkgnd() 里的所有绘制代码。下面是新窗口的样子:

 [Clock app w/view window - 3K]

我们要加到应用中的最后一样东西是 UI 更新。出于演示目的,我们要添加一个顶级菜单项,同时具有 StartStop 两个命令以开始或者停止时钟。StartStop 菜单项将被适时地启用或者禁用。

UI 更新

空闲时的 UI 更新是由好几件东西一起工作来提供的:一个 CMessageLoop 对象, CMainFrame 从之继承的嵌入类 CIdleHandlerCUpdateUI,以及 CMainFrame 里的 UPDATE_UI_MAPCUpdateUI 能够操纵五种不同类型的元素:位于菜单栏中的顶级菜单项、弹出菜单中的菜单项、工具栏按钮、状态栏窗格,还有子窗口(比如对话框控件)。每种类型的元素在 CUpdateUIBase 中都有一个对应的常量:

  • 菜单栏项: UPDUI_MENUBAR
  • 弹出菜单项: UPDUI_MENUPOPUP
  • 工具栏按钮: UPDUI_TOOLBAR
  • 状态栏窗格: UPDUI_STATUSBAR
  • 子窗口: UPDUI_CHILDWINDOW

CUpdateUI 可以设置启用状态、勾选状态,还有项目的文本(不过并非所有的项都支持所有的状态,显然你不能勾选一个编辑框子窗口)。它还可以把一个菜单项设置为缺省项而使之文本以粗体显示。

要挂接 UI 更新,我们需要作四件事情:

  1. 将框架窗口从 CUpdateUICIdleHandler 继承
  2. CMainFrameCUpdateUI 串联消息
  3. 把框架窗口添加到模块的空闲处理列表中
  4. 填充框架窗口的 UPDATE_UI_MAP

AppWizard 生成的代码已经为我们照顾到了前三项,剩下的事情就是决定哪个菜单项要更新,以及要在什么时候启用或者禁用。

控制时钟的新菜单项

我们来在菜单栏上添加一个新的 Clock 菜单,包括两项: IDC_STARTIDC_STOP

 [Clock menu - 2K]

然后我们为每一项在 UPDATE_UI_MAP 中添加一个入口:

之后无论何时我们要改变任一项的启用状态,我们就调用 CUpdateUI::UIEnable()UIEnable() 接受项的 ID,还有一个指示启用状态的 bool 值, true 为启用, false 为禁用。

这套系统比起 MFC 的 ON_UPDATE_COMMAND_UI 系统来有点落后。在 MFC 里,我们只写处理器,MFC 在即将显示菜单并需要知道菜单项的状态的时候进行调用。在 WTL 里,要由我们告诉 WTL 什么时候一个项的逻辑状态会改变。不过,在这两个库中,都是在菜单将要显示的时候才应用菜单状态的改变。

调用 UIEnable()

让我们回到 OnCreate() 函数,看一下如何设置 Clock 菜单项的初始状态。

下面是 Clock 菜单在应用刚开始时后的样子:

 [Start item disabled - 4K]

CMainFrame 现在需要这两个新项的处理器。处理器会倒换菜单项的状态,然后再调用视图类的方法开始或者停止时钟。此处是 MFC 的内建消息路由严重遗漏的领域,如果这是一个 MFC 应用,所有的 UI 更新和命令处理可能会被完全放到视图类中。但是在 WTL 里,框架和视图类必须相互通讯,菜单为框架所有,因此框架会收到菜单相关的消息并且有责任响应它们,要么自己处理,要么发给视图类。

通讯可以通过 PreTranslateMessage() 完成,不过 UIEnable() 的调用还必须由 CMainFrame 完成。 CMainFrame 可以通过将自己的 this 指针传递给视图类而逃避责任,于是视图类可以使用该指针来调用 UIEnable()。在本例中,我选择的方案会导致框架与视图类的紧密耦合,但是我发现它既易于理解又易于解释!

每个处理器都会先更新 Clock 菜单,然后调用视图的方法,因为视图是控制时钟的类。 StartClock()StopClock() 方法没有显示在这里,但可以在示例工程中找到。

关于消息映射的最后注意事项

如果你在使用 VC 6,你可能会注意到:当你把 BEGIN_MSG_MAP 改为 BEGIN_MSG_MAP_EX 后,ClassView 会变得一团糟:

 [Messed-up ClassView - 6K]

这是因为 ClassView 不能理解 BEGIN_MSG_MAP_EX,因而它把所有的 WTL 消息映射宏当成了实际的函数。通过把宏改回到 BEGIN_MSG_MAP 可以改正这一问题,只要把以下几行加到 stdafx.h 的末尾即可:

下一站,1995

我们仅仅触及到 WTL 的皮毛。在下一篇文章中,我将给我们的示例时钟程序带来 1995 UI 标准并介绍工具栏和状态栏。同时,对 CUpdateUI 的方法做一些实验,比如尝试调用 UISetCheck() 而不是 UIEnable() 来看看改变菜单项的不同方法。

修订历史

2003 年 3 月 26 日:首次发布

链接:上一部分下一部分

发表评论

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