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

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

链接:上一部分下一部分

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

内容

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

第二部分介绍

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

记住,如果你安装 WTL 或者编译示例代码时遇到了任何问题,请在张贴你的问题之前阅读第一部分的 ReadMe 一节

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 究竟是什么。

(注意,我们需要一个全局的 CAppModule 变量尽管在第一部分里这不是必需的。 CAppModule 的一些特性与我们所需的空闲处理以及 UI 更新相关,所以我们需要 CAppModule 的存在)

接下来我们来定义我们的框架窗口。像我们这样的 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 中类似的宏的术语)。要使用这些宏的第一个步骤在 VC 6 和 VC 7 里是不一样的,在 atlcrack.h 中的以下提示解释了这一不同:

对于 ATL 3.0,使用了解拆处理器的消息映射必须使用 BEGIN_MSG_MAP_EX

对于 ATL 7.0/7.1,你可以为 CWindowImpl/ CDialogImpl 的派生类使用 BEGIN_MSG_MAP,但是对于不是派生于 CWindowImpl/ CDialogImpl 的类则必须使用 BEGIN_MSG_MAP_EX

所以,如果你在使用 VC 6,你需要这样改动你的 MyWindow.h

_EX 宏对于 VC 6 来讲是必需的,因为包含于其中的某些代码是消息处理器宏需要使用的。出于可读性的原因,这里就不列出 VC 6 和 VC 7 版本的头文件了,因为它们仅仅是一个宏上面的不同。只需记住 _EX 宏在 VC 7 里是不需要的即可。)

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

WTL 的消息处理器看起来很像 MFC,每个处理器都根据随消息传入的参数有一个不同的原型。不过,由于没有向导来写处理器,我们不得不自己来查找原型。幸运的是 VC 可以帮上忙。将光标(注:此处原文错误,不应该是光标[cursor],而应该是插入符[caret])放在 “MSG_WM_CREATE” 文本上再按 F12 会转到宏的定义处。在 VC 6 里,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 宏是怎么运作的。

如果你在使用 VC 7.1,可以去找 Sergey Solozhentsev 的 WTL Helper,它会为你处理添加消息映射宏的这种麻烦事。

使用 WTL AppWizard 可以得到什么

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

通历向导(VC 6)

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

 [AppWiz screen 1 - 14K]

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

 [AppWiz screen 2 - 22K]

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

 [AppWiz screen 3 - 21K]

通历向导(VC 7)

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

 [AppWiz screen 1 - 14K]

当 AppWizard 界面出来后,点击 Application Type。在本页里,你可以选择是 SDI、MDI 还是基于对话框的应用,以及一些其他的选项。选择下面显示的选项并点击 User Interface Features

 [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 更新。出于演示目的,我们要添加一个 Clock 顶级菜单项,并具有 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 里,我们为需要更新其状态的 UI 元素写 UI 更新的处理器,MFC 在空闲的时候,或者即将显示菜单的时候对处理器进行调用。在 WTL 里,我们在项要改变的时候调用 CUpdateUI 的方法, CUpdateUI 跟踪 UI 元素及其状态,并且在空闲的时候,或者即将显示菜单的时候对元素进行更新。

调用 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 日:首次发布
2005 年 12 月 15 日:更新,包括了 VC 7.1 里 ATL 的改动

链接:上一部分下一部分

发表回复

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