MFC 程序员的 WTL 教程 ( 1 ) — ATL 中的 GUI 类
第一部分 – ATL 中的 GUI 类
本章内容
README.TXT在继续或者在本文的讨论板块中发布帖子之前,我希望你能先阅读以下内容。 本系列原来是为 VC 6 用户写的,介绍 WTL 7.0 的内容。现在 VC 8 已经出来了,我觉得也到了更新本系列来介绍 VC 7.1 的时候了。;)(不过,VC 7.1 的从 6 到 7 的自动转换工作并不是总能平滑地完成,所以 VC 7.1 的用户在试着使用示例源代码的时候可能会遭遇失败)因而,我将继续下去,持续更新本系列。文章将更新到可以反映 WTL 7.1 的特性,并会在下载的源代码中包括 VC 7.1 的工程。 针对 VC 2005 用户的重要提示:VC 2005 的 Express 版本并不附带 ATL 或者 MFC,因此不能使用此版本编译 ATL 或者 WTL 工程。
如果你在使用 VC 6,那你就需要有 Platform SDK。没有它你将不能使用 WTL。你可以使用Web 安装版本或者下载 CAB 文件或者是 ISO 映像,然后在本地运行安装程序。请使用工具把 SDK 的 include 以及 lib 目录加入到 VC 的搜索路径中,该工具可以在 Platform SDK 程序组中的 Visual Studio Registration 文件夹下找到。即使你在用 VC 7,使用最新的 Platform SDK 仍然是一个好主意,因为你可以得到最新的头文件和库。 你需要有 WTL。可以从微软下载版本 7。在文章 “Introduction to WTL – Part 1″ 以及 “Easy installation of WTL” 中有一些关于安装的提示。这些文章已经很老了,不过还是有一些不错的信息。WTL 分发包里也有一个 readme 文件,里面有安装指令。我认为在这些文章中没有提到的一件事情是如何把 WTL 的文件加入到 VC 的包含路径里。在 VC 6 里,点击 Tools|Options 并切换到 Directories 标签页,在 Show directories for 组合框中,选中 Include files,然后添加一个新项,使其指向你放置 WTL 头文件的目录。在 VC 7 里,点击 Tools|Options,再点击 Projects,然后是 VC++ Directories,在 Show directories for 组合框中,选中 Include files,然后添加一个新项,使其指向你放置 WTL 头文件的目录。 重要:我们正在提及 VC 7 的包含路径这一话题,如果你还没有更新 Platform SDK,你必须对缺省的目录列表做一个改动。请确保 你应该了解 MFC,并且要了解到你知道消息映射宏的实质是什么,而且能够编辑那些被标记为“DO NOT EDIT”的代码而不出问题。 你需要了解 Win32 API 编程,而且是很好地了解。如果你是直接通过 MFC 学习 Windows 编程而没有学习在 API 级消息是如何工作的,那很不幸,你会在使用 WTL 时遇到麻烦。如果你不知道一个消息的 你需要了解 C++ 模板的语法,在 VC Forum FAQ 上有 C++ FAQ 和模板 FAQ 的链接。 因为我还没有使用 VC 8,所以我不知道示例代码在 8 上是不是可以编译,希望 7 到 8 的升级过程能比 6 到 7 的强。如果在 VC 8 上有任何问题,请张贴到本文的论坛里。 本系列介绍WTL 确实震动了所有人。它具有许多 MFC GUI 类的强大功能,但是可以生成相当小的可执行代码。如果你和我一样,用 MFC 学习 GUI 编程,对 MFC 所提供的控件封装感到相当舒服,并且对 MFC 内建的灵活的消息处理也有同感;如果你和我一样,不喜欢好几百 K 的 MFC 框架附着到自己的程序上,WTL 正适合你。 不过,还是有一些我们必须跨越的障碍:
另一方面,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 中 作为微软所有的一个项目,WTL 有两个大的修订版,3 和 7。(选定的版本号是为了与 ATL 的版本号匹配,所以不是 1 和 2。)版本 3.1 已经相当古老了,本系列中将不再涉及。版本 7 是版本 3 的一个重要升级,而版本 7.1 仅仅加入了一些纠错和少许的特性。 在版本 7.1 之后,微软将 WTL 作为了一个开源工程,托管于 Sourceforge 上。此站点上最新的版本是 7.5(译者注:目前已经是 8.0 了),我还没有看 7.5,所以现在本系列不会涵盖 7.5 的内容(我总是会落后两个版本,而且会周期性地赶上来!) ATL 风格的模板即使你可以毫不头痛的阅读 C++ 模板,但一开始 ATL 还是会有两件事可能成为拦路虎。比如说: class CMyWnd : public CWindowImpl<CMyWnd> { ... }; 这样做是合法的,因为 C++ 规范中声称紧随 如果要看一下实际运作,可以看一下这几个类: template <class T> class B1 { public: void SayHi() { T* pT = static_cast<T*>(this); // HUH?? I'll explain this below pT->PrintClassName(); } protected: void PrintClassName() { cout << "This is B1"; } }; class D1 : public B1<D1> { // No overridden functions at all }; class D2 : public B1<D2> { protected: void PrintClassName() { cout << "This is D2"; } }; main() { D1 d1; D2 d2; d1.SayHi(); // prints "This is B1" d2.SayHi(); // prints "This is D2" } 这儿的 要解释这是如何工作的,我们来看一下 void B1<D1>::SayHi() { D1* pT = static_cast<D1*>(this); pT->PrintClassName(); } 由于 现在,看 void B1<D2>::SayHi() { D2* pT = static_cast<D2*>(this); pT->PrintClassName(); } 这次 这种技术的好处是:
虽然在这个例子里 vtbl 的节约看起来并不明显(只不过 4 个字节),不过可以考虑在有 15 个基类,有的类有 20 个方法的情况下,累计的节省有多少。 ATL 窗口类很好,背景知识足够了!是钻研 ATL 的时候了。ATL 采用严格的接口/实现相分离来设计,这在窗口类中也很明显。这和 COM 类似,接口的定义与实现完全分离(或者可能有多个实现)。 ATL 有一个类定义了窗口的“接口”,也就是对一个窗口可以做什么。这个类就是
ATL 中窗口的实现类是 还有两个独立的类,包含了对对话框的实现, 定义窗口实现任何要创建的非对话框窗口应该从
窗口类的定义使用 我们从一个新的类的定义开始,随后的章节里我会逐步向其中增加内容。 class CMyWindow : public CWindowImpl<CMyWindow> { public: DECLARE_WND_CLASS(_T("My Window Class")) }; 接下来是消息映射。ATL 的消息映射比 MFC 的映射简单的多。一个 ATL 映射被展开为一个大的 switch 语句。switch 查找合适的处理器并调用相应的函数。消息映射的宏是 class CMyWindow : public CWindowImpl<CMyWindow> { public: DECLARE_WND_CLASS(_T("My Window Class")) BEGIN_MSG_MAP(CMyWindow) END_MSG_MAP() }; 我会在下一节中讲解如何向映射中添加处理器。最后,我们要为我们的类定义窗口修饰。窗口修饰是窗口风格和窗口扩展风格的组合,这些风格在创建窗口时会被用到。这些风格作为模板参数被指定,所以调用者在创建窗口时可以不被如何得到正确的风格而烦恼。这是一个示例的修饰定义,使用了 ATL 类 typedef CWinTraits<WS_OVERLAPPEDWINDOW | WS_CLIPCHILDREN, WS_EX_APPWINDOW> CMyWindowTraits; class CMyWindow : public CWindowImpl<CMyWindow, CWindow, CMyWindowTraits> { public: DECLARE_WND_CLASS(_T("My Window Class")) BEGIN_MSG_MAP(CMyWindow) END_MSG_MAP() }; 调用者可以在 typedef CWinTraits<WS_OVERLAPPEDWINDOW | WS_CLIPCHILDREN |
WS_CLIPSIBLINGS,
WS_EX_APPWINDOW | WS_EX_WINDOWEDGE>
CFrameWinTraits;
填充消息映射ATL 的消息映射对开发人员来讲是一个缺乏友好性的地方,同时也是 WTL 极大的增强了的地方。ClassView 提供了添加消息处理器的功能,但 ATL 没有类似于 MFC 的特定消息相关的宏和自动化参数解析。在 ATL 里,只有三种类型的消息处理器,一个针对 class CMyWindow : public CWindowImpl<CMyWindow, CWindow, CFrameWinTraits> { public: DECLARE_WND_CLASS(_T("My Window Class")) BEGIN_MSG_MAP(CMyWindow) MESSAGE_HANDLER(WM_CLOSE, OnClose) MESSAGE_HANDLER(WM_DESTROY, OnDestroy) END_MSG_MAP() LRESULT OnClose(UINT uMsg, WPARAM wParam, LPARAM lParam, BOOL& bHandled) { DestroyWindow(); return 0; } LRESULT OnDestroy(UINT uMsg, WPARAM wParam, LPARAM lParam, BOOL& bHandled) { PostQuitMessage(0); return 0; } }; 你会注意到处理器接收原始的 我们再添加 class CMyWindow : public CWindowImpl<CMyWindow, CWindow, CFrameWinTraits> { public: DECLARE_WND_CLASS(_T("My Window Class")) BEGIN_MSG_MAP(CMyWindow) MESSAGE_HANDLER(WM_CLOSE, OnClose) MESSAGE_HANDLER(WM_DESTROY, OnDestroy) COMMAND_ID_HANDLER(IDC_ABOUT, OnAbout) END_MSG_MAP() LRESULT OnClose(UINT uMsg, WPARAM wParam, LPARAM lParam, BOOL& bHandled) { DestroyWindow(); return 0; } LRESULT OnDestroy(UINT uMsg, WPARAM wParam, LPARAM lParam, BOOL& bHandled) { PostQuitMessage(0); return 0; } LRESULT OnAbout(WORD wNotifyCode, WORD wID, HWND hWndCtl, BOOL& bHandled) { MessageBox ( _T("Sample ATL window"), _T("About MyWindow") ); return 0; } }; 注意, 高级消息映射和嵌入(Mix-in)类ATL 中一个最大的不同是任何 C++ 类都可以处理消息,不像 MFC 里消息处理的任务在 带有消息映射的基类通常是一个以派生类名作为模板参数的模板,这样就能访问派生类中像 template <class T, COLORREF t_crBrushColor> class CPaintBkgnd : public CMessageMap { public: CPaintBkgnd() { m_hbrBkgnd = CreateSolidBrush(t_crBrushColor); } ~CPaintBkgnd() { DeleteObject ( m_hbrBkgnd ); } BEGIN_MSG_MAP(CPaintBkgnd) MESSAGE_HANDLER(WM_ERASEBKGND, OnEraseBkgnd) END_MSG_MAP() LRESULT OnEraseBkgnd(UINT uMsg, WPARAM wParam, LPARAM lParam, BOOL& bHandled) { T* pT = static_cast<T*>(this); HDC dc = (HDC) wParam; RECT rcClient; pT->GetClientRect ( &rcClient ); FillRect ( dc, &rcClient, m_hbrBkgnd ); return 1; // we painted the background } protected: HBRUSH m_hbrBkgnd; }; 我们来审视一下这个新类。首先, 构造函数和析构函数相当简单,它们创建/销毁了一个 在我们的窗口中使用这一嵌入类,要做两件事。首先,我们把它加到继承列表中: class CMyWindow : public CWindowImpl<CMyWindow, CWindow, CFrameWinTraits>, public CPaintBkgnd<CMyWindow, RGB(0,0,255)> 然后,我们要使 class CMyWindow : public CWindowImpl<CMyWindow, CWindow, CFrameWinTraits>, public CPaintBkgnd<CMyWindow, RGB(0,0,255)> { ... typedef CPaintBkgnd<CMyWindow, RGB(0,0,255)> CPaintBkgndBase; BEGIN_MSG_MAP(CMyWindow) MESSAGE_HANDLER(WM_CLOSE, OnClose) MESSAGE_HANDLER(WM_DESTROY, OnDestroy) COMMAND_HANDLER(IDC_ABOUT, OnAbout) CHAIN_MSG_MAP(CPaintBkgndBase) END_MSG_MAP() ... }; 任何到达 你可以在继承列表中放心的使用多个嵌入类,对每一个类使用 ATL EXE 的结构现在我们有了一个完整的(虽然不怎么有用)主窗口,我们来看看如何在程序中使用它。ATL 的可执行程序里有一个或者多个大致对应于 MFC 程序中的全局 VC 6 的情形ATL 的可执行程序包含一个全局的 // stdafx.h: #define STRICT #define VC_EXTRALEAN #include <atlbase.h> // Base ATL classes extern CComModule _Module; // Global _Module #include <atlwin.h> // ATL windowing classes atlbase.h 会包含基本的 Windows 头文件,所以不必再包含 windows.h,tchar.h 等。在 CPP 文件里,声明(译者注:应该为定义) // main.cpp:
CComModule _Module;
// main.cpp: CComModule _Module; int WINAPI WinMain(HINSTANCE hInst, HINSTANCE hInstPrev, LPSTR szCmdLine, int nCmdShow) { _Module.Init(NULL, hInst); _Module.Term(); } 传给 // main.cpp: #include "MyWindow.h" CComModule _Module; int WINAPI WinMain(HINSTANCE hInst, HINSTANCE hInstPrev, LPSTR szCmdLine, int nCmdShow) { _Module.Init(NULL, hInst); CMyWindow wndMain; MSG msg; // Create & show our main window if ( NULL == wndMain.Create ( NULL, CWindow::rcDefault, _T("My First ATL Window") )) { // Bad news, window creation failed return 1; } wndMain.ShowWindow(nCmdShow); wndMain.UpdateWindow(); // Run the message loop while ( GetMessage(&msg, NULL, 0, 0) > 0 ) { TranslateMessage(&msg); DispatchMessage(&msg); } _Module.Term(); return msg.wParam; } 以上代码中唯一与众不同的是 ATL 在底下使用了一些汇编语言的把戏来把主窗口的句柄和与之相应的 VC 7 的情形ATL 7 把模块管理代码分散到了好几个类中。出于兼容的目的, 在 VC 7 里,ATL 的头文件自动声明所有模块类的全局实例,而且还为你调用了 // stdafx.h: #define STRICT #define WIN32_LEAN_AND_MEAN #include <atlbase.h> // Base ATL classes #include <atlwin.h> // ATL windowing classes
// main.cpp: #include "MyWindow.h" int WINAPI WinMain(HINSTANCE hInst, HINSTANCE hInstPrev, LPSTR szCmdLine, int nCmdShow) { CMyWindow wndMain; MSG msg; // Create & show our main window if ( NULL == wndMain.Create ( NULL, CWindow::rcDefault, _T("My First ATL Window") )) { // Bad news, window creation failed return 1; } wndMain.ShowWindow(nCmdShow); wndMain.UpdateWindow(); // Run the message loop while ( GetMessage(&msg, NULL, 0, 0) > 0 ) { TranslateMessage(&msg); DispatchMessage(&msg); } return msg.wParam; } 我们的窗口看起来像是这样: 我得承认,没什么特别激动人心的事情。为了能增添些情趣,我们会加一个能显示对话框的 About 菜单项。 ATL 中的对话框正如已经提到的,ATL 有两个对话框类。我们将为我们的对话框使用
这是新的 About 对话框类的初始定义: class CAboutDlg : public CDialogImpl<CAboutDlg> { public: enum { IDD = IDD_ABOUT }; BEGIN_MSG_MAP(CAboutDlg) END_MSG_MAP() }; ATL 没有针对 OK 和 Cancel 按钮的内建处理器,所以我们需要自己写代码,顺便还有 class CAboutDlg : public CDialogImpl<CAboutDlg> { public: enum { IDD = IDD_ABOUT }; BEGIN_MSG_MAP(CAboutDlg) MESSAGE_HANDLER(WM_INITDIALOG, OnInitDialog) MESSAGE_HANDLER(WM_CLOSE, OnClose) COMMAND_ID_HANDLER(IDOK, OnOKCancel) COMMAND_ID_HANDLER(IDCANCEL, OnOKCancel) END_MSG_MAP() LRESULT OnInitDialog(UINT uMsg, WPARAM wParam, LPARAM lParam, BOOL& bHandled) { CenterWindow(); return TRUE; // let the system set the focus } LRESULT OnClose(UINT uMsg, WPARAM wParam, LPARAM lParam, BOOL& bHandled) { EndDialog(IDCANCEL); return 0; } LRESULT OnOKCancel(WORD wNotifyCode, WORD wID, HWND hWndCtl, BOOL& bHandled) { EndDialog(wID); return 0; } }; 我们为 OK 和 Cancel 使用同一个处理器以演示 显示对话框和 MFC 相似,创建新类的一个对象并调用 class CMyWindow : public CWindowImpl<CMyWindow, CWindow, CFrameWinTraits>, public CPaintBkgnd<CMyWindow,RGB(0,0,255)> { public: BEGIN_MSG_MAP(CMyWindow) MESSAGE_HANDLER(WM_CREATE, OnCreate) COMMAND_ID_HANDLER(IDC_ABOUT, OnAbout) // ... CHAIN_MSG_MAP(CPaintBkgndBase) END_MSG_MAP() LRESULT OnCreate(UINT uMsg, WPARAM wParam, LPARAM lParam, BOOL& bHandled) { HMENU hmenu = LoadMenu ( _Module.GetResourceInstance(), // _AtlBaseModule in VC7 MAKEINTRESOURCE(IDR_MENU1) ); SetMenu ( hmenu ); return 0; } LRESULT OnAbout(WORD wNotifyCode, WORD wID, HWND hWndCtl, BOOL& bHandled) { CAboutDlg dlg; dlg.DoModal(); return 0; } // ... }; 模态对话框的一个小小的不同是在什么地方指定对话框的父窗口。MFC 里是向
注意,由于模块管理类的不同,VC6 和 7 的 这是我们修改后的主窗口和 About 对话框: 就要到 WTL 了,我保证!不过会是在第二部分里。因为我是在为 MFC 开发人员写这些文章,所以我认为在进入 WTL 之前,最好对 ATL 先做个介绍。如果这是你对 ATL 的第一次亲密接触,那现在也许是你自己写一些简单应用的好机会,从而可以熟悉消息映射以及嵌入类的使用。你也可以实践一下 ClassView 对 ATL 消息映射的支持,它可以为你添加消息处理器。想要在 VC 6 里开始,请右击 CMyWindow 项并选择关联菜单中的 Add Windows Message Handler。在 VC 7 里,请右击 CMyWindow 项并选择关联菜单中的 Properties。在属性窗口里,点击工具栏上的 Message 按钮可以看到一个窗口消息的列表。要为消息添加处理器,可以到消息对应的行上,点击右面的一列,使之改变为一个组合框,点击组合框的箭头,然后再点击下拉列表中的 <Add> 项。 在第二部分里,我将讲解基本的 WTL 窗口类、WTL AppWizard,以及更好的消息映射宏。 修订历史2003 年 3 月 22 日:首次发布 |
链接:下一部分

转载的文章,也不说明出处?