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

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

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

链接:下一部分

第一部分 – ATL 中的 GUI 类

本章内容

  • README.TXT
  • 本系列介绍
  • 第一部分介绍
  • ATL 背景知识
    • ATL 和 WTL 的历史
    • ATL 风格的模板
  • ATL 窗口类
  • 定义窗口实现
    • 填充消息映射
  • 高级消息映射链和嵌入(Mix-in)类
  • ATL EXE 的结构
  • ATL 中的对话框
  • 就要到 WTL 了,我保证!
  • 修订历史

README.TXT

在继续或者在本文的讨论板块中发布帖子之前,我希望你能先阅读以下内容。

你需要有 Platform SDK。没有它你将不能使用 WTL。你可以使用在线的 SDK 升级站点或者下载 CAB 文件并在本地运行安装程序。请使用工具把 SDK 的 include 以及 lib 目录加入到 VC 的搜索路径中,该工具可以在 Platform SDK 程序组中的 Visual Studio Registration 文件夹下找到。

你需要有 WTL。可以从微软下载版本 7。在文章 “Introduction to WTL – Part 1” 以及 “Easy installation of WTL” 中有一些关于安装的提示。这些文章已经很老了,不过还是有一些不错的信息。在这些文章中没有提到的一件事情是如何使 VC 搜索 WTL 的 include 目录。在 VC 6 里,点击 Tools|Options 并切换到 Directories 标签页,在 Show directories for 组合框中,选中 Include files,然后添加一个新项,使其指向你放置 WTL 头文件的目录。

你应该了解 MFC,并且要了解到你知道消息映射宏的实质是什么,而且能够编辑那些被标记为“DO NOT EDIT”的代码而不出问题。

你需要了解 Win32 API 编程,而且是很好地了解。如果你是直接通过 MFC 学习 Windows 编程而没有学习在 API 级消息是如何工作的,那很不幸,你会在使用 WTL 时遇到麻烦。如果你不知道一个消息的 WPARAMLPARAM 是什么意思,你应该阅读其他的文章(CodeProject 上就有很多)以使你能够了解。

你需要了解 C++ 模板的语法,在 VC Forum FAQ 上有 C++ FAQ 和模板 FAQ 的链接。

我将只涉及 VC 6 的特性,不过据我所知在 VC 7 上一切都很正常。因为我不使用 VC 7,所以不能对 VC 7 上的问题提供帮助。但可以随便提问 VC 7 的问题,因为其他读者可能能够提供帮助。

本系列介绍

WTL 确实震动了所有人。它具有许多 MFC GUI 类的强大功能,但是可以生成相当小的可执行代码。如果你和我一样,用 MFC 学习 GUI 编程,对 MFC 所提供的控件封装感到相当舒服,并且对 MFC 内建的灵活的消息处理也有同感;如果你和我一样,不喜欢好几百 K 的 MFC 框架附着到自己的程序上,WTL 正适合你。 不过,还是有一些我们必须跨越的障碍:

  • ATL 风格的模板乍看起来很怪异。
  • 没有 ClassWizard 支持,所以写消息映射成了手工劳动。
  • 在 MSDN 里没有文档,需要到其他地方去找,甚至需要去看 WTL 源程序。
  • 没有能买到并放到书架上的参考书。
  • 它具有“不被微软官方支持”的污名
  • ATL/WTL 窗口非常不同于 MFC 窗口,并非你所有的知识都能够对应过来

另一方面,WTL 的好处有:

  • 不需学习或者使用复杂的文档/视图框架
  • 具有源于 MFC 的一些基本 UI 特性,例如 DDX/DDV 和“更新命令 UI”功能
  • 增强了的一些 MFC 特性(例如,更灵活的分割条窗口)
  • 与静态链接 MFC 的应用相比,可执行代码非常小
  • 你自己可以改正 WTL 的错误而不影响现存的应用(相比之下,一个应用替换掉 MFC/CRT 的 DLL 来改正错误将引起其他应用崩溃)
  • 如果仍然需要 MFC,MFC 和 ATL/WTL 窗口可以和平共处(在我工作的一个原型中,我创建了一个包含有 WTL CSplitterWindow 的 MFC CFrameWnd ,而前者中又包含有 MFC CDialog。– 并不是我卖弄,只不过是修改了 MFC 代码而使用了更好的 WTL 分割条)

在本系列中,我将先介绍 ATL 窗口类。毕竟 WTL 是一组 ATL 的附加类,所以对 ATL 窗口有很好的理解相当重要。介绍完 ATL 之后我将介绍 WTL 的特性并展示它如何使界面编程变得轻而易举。

第一部分介绍

WTL 令人震惊。不过在知道为什么之前,我们首先需要了解 ATL。WTL 是一组 ATL 的附加类,如果过去你是一名仅使用 MFC 的程序员,你可能从来没有遇到过 ATL 的 GUI 类。所以请原谅我没有立即涉及 WTL,到 ATL 那儿绕些弯路是有必要的。

在第一部分里,我将给出一些 ATL 的背景知识,包括在写 ATL 代码之前需要知道的一些要点,迅速的解释那些令人胆寒的 ATL 模板,并涵盖了基本的 ATL 窗口类。

ATL 背景知识

ATL 和 WTL 的历史

活动(Active)模板库是一个古怪的名字,不是吗?年长点的可能会记得它原来的名字是 ActiveX 模板库,这是一个更准确的名字,因为 ATL 的目标就是要让 COM 对象和 ActiveX 控件写起来更轻松。(ATL 是在微软将新产品命名为“ActiveX-什么什么”的时候开发的,就像现在微软的新产品被称作“什么什么 .NET”一样)因为 ATL 只是用来写 COM 对象的,所以它只有 GUI 类中最基本的部分,即 MFC 中 CWndCDialog 的等价物。幸运的是,这些 GUI 类很灵活,可以让像 WTL 这样的东西构筑于其上。

当前是 WTL 的第二个修订版。第一个是版本 3.1,第二个是版本 7。(选定的版本号是为了与 ATL 的版本号匹配,所以不是 1 和 2。)版本 3.1 工作于 VC 6 和 VC 7,不过你需要为 VC 7 定义一组预处理符号。WTL 版本 7 是版本 3.1 的向下兼容替代品,不用修改就可以在 VC 7 上工作,所以对新的开发工作来讲确实没有理由还使用 3.1。

ATL风格的模板

即使你可以毫不头痛的阅读 C++ 模板,但一开始 ATL 还是会有两件事可能成为拦路虎。比如说:

这样做是合法的,因为 C++ 规范中声称紧随 class CMyWnd 部分之后,名字 CMyWnd 即被定义;并且可以被用在继承列表中。之所以把类名作为模板参数是因为要让 ATL 能做第二件技巧性的工作 – 编译期虚函数调用。

如果要看一下实际运作,可以看一下这几个类:

这儿的 static_cast<T*>(this) 是一个技巧。它把 B1* 类型的 this ,通过被调用的特化转型为 D1* 或者 D2* 。因为模板代码在编译时生成,所以保证了此转型是安全的,只要正确的书写了继承列表。(如果你写成

你会遇到麻烦。)转型是安全的是因为 this 对象能是类型 D1* 或者 D2* (相应的),而不是其他。注意,这和正常的 C++ 多态几乎一样,只不过 SayHi() 方法不是虚拟的。

要解释这是如何工作的,我们来看一下 SayHi() 的每个调用。在第一个调用中,特化 B1<D1> 被采用,所以 SayHi() 代码展开为:

由于 D1 没有覆盖 PrintClassName(),所以会搜索 D1 的基类。 B1PrintClassName() 方法,所以就是被调到的那个。

现在,看 SayHi() 的第二次调用。这一次使用了特化 B1<D2>,于是 SayHi() 展开为:

这次 D2 确实包含了一个 PrintClassName() 方法,所以它是被调用到的那个。

这种技术的好处是:

  • 不需要使用指向对象的指针
  • 由于不需要 vtbl 而节省内存
  • 不会因为未初始化的 vtbl 而在运行时通过空指针调用虚函数
  • 所有函数调用在编译时被解析,所以可以被优化

虽然在这个例子里 vtbl 的节约看起来并不明显(只不过 4 个字节),不过可以考虑在有 15 个基类,有的类有 20 个方法的情况下,累计的节省有多少。

ATL 窗口类

很好,背景知识足够了!是钻研 ATL 的时候了。ATL 采用严格的接口/实现相分离来设计,这在窗口类中也很明显。这和 COM 类似,接口的定义与实现完全分离(或者可能有多个实现)。

ATL 有一个类定义了窗口的“接口”,也就是对一个窗口可以做什么。这个类就是 CWindow。它只是对 HWND 的一个封装,提供了几乎所有的以 HWND 作为第一个参数的 User API,如 SetWindowText()DestroyWindow()CWindow 有一个当你需要原始 HWND时可以访问的公用成员 m_hWndCWindow 也有一个 operator HWND 方法,于是你可以传递 CWindow 对象到接受 HWND 的函数中。没有 CWnd::GetSafeHwnd()的等价物。

CWindow 和 MFC 的 CWnd 很不同。创建 CWindow 对象不需多少代价,因为它只有一个数据成员,而且也没有 MFC 内部用以保存 HWNDCWnd 对象的对应关系的对象映射表。还与 CWnd不一样的是,当一个 CWindow 对象离开作用域,关联的窗口不会被销毁。这意味着你不须总是记住要把你创建的临时 CWindow 对象与关联的窗口脱离。

ATL 中窗口的实现类是 CWindowImplCWindowImpl 包含了做这些事情的代码,比如窗口类注册,窗口子类化,消息映射,以及一个基本的 WindowProc()。再次不同于 MFC,在MFC 中,所有这些在同一个类里: CWnd

还有两个独立的类,包含了对对话框的实现, CDialogImplCAxDialogImplCDialogImpl 用于普通的对话框,而 CAxDialogImpl 用于要包容 ActiveX 控件的对话框。

定义窗口实现

任何要创建的非对话框窗口应该从 CWindowImpl 派生。新类里需要包含三样:

  1. 窗口类定义
  2. 消息映射
  3. 窗口使用的默认风格,称作窗口修饰

窗口类的定义使用 DECLARE_WND_CLASS 或者 DECLARE_WND_CLASS_EX 宏来完成。它们都定义了一个封装了 WNDCLASSEX 结构的 ATL 结构 CWndClassInfoDECLARE_WND_CLASS 允许指定新窗口类的名字,其余成员使用缺省值;而 DECLARE_WND_CLASS_EX 还允许指定类风格和窗口背景色。类名也可以用 NULL,ATL 将生成一个。

我们从一个新的类的定义开始,随后的章节里我会逐步向其中增加内容。

接下来是消息映射。ATL 的消息映射比 MFC 的映射简单的多。一个 ATL 映射被展开为一个大的 switch 语句。switch 查找合适的处理器并调用相应的函数。消息映射的宏是 BEGIN_MSG_MAPEND_MSG_MAP 。向窗口中添加一个空的映射。

我会在下一节中讲解如何向映射中添加处理器。最后,我们要为我们的类定义窗口修饰 。窗口修饰是窗口风格和窗口扩展风格的组合,这些风格在创建窗口时会被用到。这些风格作为模板参数被指定,所以调用者在创建窗口时可以不被如何得到正确的风格而烦恼。这是一个示例的修饰定义,使用了 ATL 类 CWinTraits

调用者可以CMyWindowTraits 定义中覆盖这些风格,不过通常没有必要。ATL 还有一些专用的预定义 CWinTraits ,其中适用于像我们这样的顶级窗口的一个是 CFrameWinTraits

填充消息映射

ATL 的消息映射对开发人员来讲是一个缺乏友好性的地方,同时也是 WTL 极大的增强了的地方。ClassView 提供了添加消息处理器的功能,但 ATL 没有类似于 MFC 的特定消息相关的宏和自动化参数解析。在 ATL 里,只有三种类型的消息处理器,一个针对 WM_NOTIFY,一个针对 WM_COMMAND,一个针对其他所有的消息。我们从向窗口添加 WM_CLOSEWM_DESTROY 的消息处理器开始。

你会注意到处理器接收原始的 WPARAMLPARAM 值,当消息使用这些参数时你需要自己去解析它们。还有第四个参数, bHandled。这个参数在处理期调用之前被 ATL 设为 TRUE。如果你希望 ATL 的缺省 WindowProc() 在你的处理器返回之后也能处理消息,你可以将 bHandled 设为 FALSE。这和 MFC 不一样,MFC 中必须显式调用消息处理器的基类实现。

我们再添加 WM_COMMAND 的处理器。假定我们窗口的菜单有一个 ID 为 IDC_ABOUT 的 About 项:

注意, COMMAND_HANDLER 宏为你做了解析消息参数的工作。 NOTIFY_HANDLER 宏类似地解析 WM_NOTIFY 的消息参数。

高级消息映射和嵌入(Mix-in)类

ATL 中一个最大的不同是任何 C++ 类都可以处理消息,不像 MFC 里消息处理的任务在 CWndCCmdTarget 间分割开来,再加上几个有 PreTranslateMessage() 方法的类。这一能力允许我们写通常被称作嵌入类的东西,以便通过向继承列表中添加类就可以向我们的窗口中增加特性。

带有消息映射的基类通常是一个以派生类名作为模板参数的模板,这样就能访问派生类中像 m_hWndCWindow 中的 HWND 成员)这样的成员。我们来看一下这个嵌入类,它通过处理 WM_ERASEBKGND 来绘制窗口背景。

我们来审视一下这个新类。首先, CPaintBkgnd 有两个模板参数:派生类的名字: CPaintBkgnd,和背景的颜色( t_ 前缀通常用于值类型的模板参数) CPaintBkgnd 也从 ATL 类 CMessageMap 派生,这不是严格地必需的,因为 BEGIN_MSG_MAP 宏对于要处理消息的类就已经足够了;因此当你再看其它示例代码时可能看不到把该类作为基类。

构造函数和析构函数相当简单,它们创建/销毁了一个 t_crBrushColor 颜色的画刷。接下来的消息映射处理了 WM_ERASEBKGND。最后, OnEraseBkgnd() 处理器使用构造函数中创建的画刷来填充窗口。 OnEraseBkgnd() 中有两件事值得注意。首先,它使用了派生类的窗口函数(名为 GetClientRect())。我们怎么知道派生类里恰好一个 GetClientRect() 函数?但如果没有的话,代码根本不能编译!编译器确保派生类 T 派生于 CWindow。其次, OnEraseBkgnd()wParam 处得到设备上下文(WTL 将关照此事,我们最终会到那儿的,我保证!)

在我们的窗口中使用这一嵌入类,要做两件事。首先,我们把它加到继承列表中:

然后,我们要使 CMyWindow 传递消息到 CPaintBkgnd。这叫做串联消息映射。在 CMyWindow 的消息映射里,加入 CHAIN_MSG_MAP 宏:

任何到达 CMyWindow 映射而没有处理的消息将被传递到 CPaintBkgnd 的映射中。注意, WM_CLOSEWM_DESTROY,和 IDC_ABOUT 会被串联,因为只要它们一被处理,对消息映射的搜索就会终止。typedef 是必要的,因为 CHAIN_MSG_MAP 是个接收单个参数的预处理宏;如果我们以 CPaintBkgnd<CMyWindow, RGB(0,0,255)> 作为参数,其中的逗号会使预处理器认为我们以不止一个参数来调用该宏。

你可以在继承列表中放心的使用多个嵌入类,对每一个类使用 CHAIN_MSG_MAP 宏以使消息能够传入。这不同于 MFC,每个 CWnd 派生类只能有一个基类,而且 MFC 自动向基类传递未处理的消息。

ATL EXE 的结构

现在我们有了一个完整的(虽然不怎么有用)主窗口,我们来看看如何在程序中使用它。ATL 的可执行程序包含一个全局的 CComModule  变量,名为 _Module。这与 MFC 程序中名为 theApp 的全局 CWinApp 变量类似;唯一的不同是,在 ATL 里,这个变量必须命名为 _Module

我们的 stdafx.h 以此开始:

atlbase.h 会包含基本的 Windows 头文件,所以不必再包含 windows.h,tchar.h 等。在 CPP 文件里,声明(译者注:应该为定义) _Module 变量:

CComModule 中有我们需要在 WinMain() 中调用的显式初始化/退出函数,所以我们以此为始:

传给 Init() 的第一个参数仅在 COM 服务器中使用。而我们的 EXE 不包括 COM 对象,所以我们只需要传入 NULL。ATL 并不像 MFC 那样提供自己的 WinMain() 或者消息泵,所以要使我们的程序运行起来,需要创建 CMyWindow 对象并添加消息泵。

以上代码中唯一与众不同的是 CWindow::rcDefault,它是 CWindow 的一个 RECT 成员。将它作为窗口的初始 RECT 就像在 CreateWindow() API 里用 CW_USEDEFAULT 表示宽和高。

ATL 在底下使用了一些汇编语言的把戏来把主窗口的句柄和与之相应的 CMyWindow 对象联系起来。在此之上就是我们可以把 CWindow 对象在线程间传来传去而不出问题,而如果在 MFC 里对 CWnd 这样干会死得很惨。

我们的窗口看起来像是这样:

 [First ATL window - 4K]

我得承认,没什么特别激动人心的事情。为了能增添些情趣,我们会加一个能显示对话框的 About 菜单项。

ATL中的对话框

正如已经提到的,ATL 有两个对话框类。我们将为我们的对话框使用 CDialogImpl。创建一个新的对话框类与创建一个新的框架窗口类大致相当,只不过有两处不同:

  1. 基类是 CDialogImpl 而不是 CWindowImpl
  2. 需要定义一个名为 IDD 的公用成员,其中包含有对话框的资源 ID

这是新的 about 对话框类的初始定义:

ATL 没有针对 OKCancel 按钮的内建处理器,所以我们需要自己写代码,顺便还有 WM_CLOSE 的处理器,当用户点击标题条上的关闭按钮时此处理器会被调用。我们还需要处理 WM_INITDIALOG 以使对话框出现时能正确的设置键盘焦点。这是带有消息处理器的完整的类定义。

我们为 OKCancel 使用同一个处理器以演示 wID 参数,该参数的值被设为 IDOKIDCANCEL,这取决于哪个按钮被点击。

先是对话框和 MFC 相似,创建新类的一个对象并调用 DoModal()。回到我们的主窗口并添加一个拥有 About 菜单项的菜单,该项将显示我们的 about 对话框。我们需要添加两个消息处理器,一个是为 WM_CREATE 而另一个是为新的菜单项 IDC_ABOUT

模态对话框的一个小小的不同是在什么地方指定对话框的父窗口。MFC 里是向 CDialog 的构造函数传递父窗口,而在 ATL 中应该把父窗口作为 DoModal() 的第一个参数传递。如果像上面的代码一样不指定,ATL 使用 GetActiveWindow()(将会是我们的框架窗口)的返回结果作为父窗口。

LoadMenu() 调用也演示了 CComModule 的一个方法: GetResourceInstance()。他返回我们的 EXE 的 HINSTANCE ,就像 AfxGetResourceHandle() 一样(还有一个 CComModule::GetModuleInstance(),此函数与 AfxGetInstanceHandle()相仿)。

这是我们修改后的主窗口和 about 对话框:

 [About box - 5K]

就要到 WTL 了,我保证!

不过会是在第二部分里。我是在为 MFC 开发人员写这些文章,所以我认为在进入 WTL 之前,最好对 ATL 先做个介绍。如果这是你对 ATL 的第一次亲密接触,那现在也许是你自己写一些简单应用的好机会,从而可以熟悉消息映射以及嵌入类的使用。你也可以实践一下 ClassView 对 ATL 消息映射的支持,它可以为你添加消息处理器。想要开始,请右击 CMyWindow 项并选择关联菜单中的 Add Windows Message Handler

在第二部分里,我将讲解基本的 WTL 窗口类、WTL AppWizard,以及更好的消息映射宏。

修订历史

2003 年 3 月 22 日:首次发布

链接:下一部分

发表回复

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