特别注:由于本页内容栏宽度不够,会导致部分内容看不见,请点击这里以获得最佳浏览效果。
链接:下一部分
第一部分 – ATL 中的 GUI 类
本章内容
- README.TXT
- 本系列介绍
- 第一部分介绍
- ATL 背景知识
- ATL 和 WTL 的历史
- ATL 风格的模板
- ATL 窗口类
- 定义窗口实现
- 填充消息映射
- 高级消息映射链和嵌入(Mix-in)类
- ATL EXE 的结构
- VC 6 的情形
- VC 7 的情形
- ATL 中的对话框
- 就要到 WTL 了,我保证!
- 修订历史
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,你必须对缺省的目录列表做一个改动。请确保 $(VCInstallDir)PlatformSDK\include 在列表的第一位,优先于 $(VCInstallDir)include,如下图所示:
你应该了解 MFC,并且要了解到你知道消息映射宏的实质是什么,而且能够编辑那些被标记为“DO NOT EDIT”的代码而不出问题。
你需要了解 Win32 API 编程,而且是很好地了解。如果你是直接通过 MFC 学习 Windows 编程而没有学习在 API 级消息是如何工作的,那很不幸,你会在使用 WTL 时遇到麻烦。如果你不知道一个消息的 WPARAM 和 LPARAM 是什么意思,你应该阅读其他的 API 级编程的文章(CodeProject 上就有很多)以使你能够了解。
你需要了解 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 正适合你。 不过,还是有一些我们必须跨越的障碍:
- 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 中 CWnd 和 CDialog 的等价物。幸运的是,这些 GUI 类很灵活,可以让像 WTL 这样的东西构筑于其上。
作为微软所有的一个项目,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 还是会有两件事可能成为拦路虎。比如说:
1 2 3 4 |
class CMyWnd : public</span> CWindowImpl<CMyWnd> { ... }; |
这样做是合法的,因为 C++ 规范中声称紧随 class CMyWnd 部分之后,名字 CMyWnd 即被定义;并且可以被用在继承列表中。之所以把类名作为模板参数是因为要让 ATL 能做第二件技巧性的工作 – 编译期虚函数调用。
如果要看一下实际运作,可以看一下这几个类:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 |
template <class T> class B1 { public: void SayHi() { <span style="color: red;">T* pT = static_cast<T*>(this);</span> // 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" } |
这儿的 static_cast<T*>(this) 是一个技巧。它把 B1* 类型的 this ,通过被调用的特化转型为 D1* 或者 D2* 。因为模板代码在编译时生成,所以保证了此转型是安全的,只要正确的书写了继承列表。 (如果你写成 class D3 : public B1<D2> 你就会遇到麻烦。)转型是安全的是因为 this 对象只能是类型 D1* 或者 D2* (相应的),而不是其他。注意,这和正常的 C++ 多态几乎一样,只不过 SayHi() 方法不是虚拟的。
要解释这是如何工作的,我们来看一下 SayHi() 的每个调用。在第一个调用中,特化 B1<D1> 被采用,所以 SayHi() 代码展开为:
1 2 3 4 5 6 |
void B1<D1>::SayHi() { D1* pT = static_cast<D1*>(this); pT->PrintClassName(); } |
由于 D1 没有覆盖 PrintClassName(),所以会搜索 D1 的基类。 B1 有 PrintClassName() 方法,所以就是被调到的那个。
现在,看 SayHi() 的第二次调用。这一次使用了特化 B1<D2>,于是 SayHi() 展开为:
1 2 3 4 5 6 |
void B1<D2>::SayHi() { D2* pT = static_cast<D2*>(this); pT->PrintClassName(); } |
这次 D2 确实包含了一个 PrintClassName() 方法,所以它是被调用到的那个。
这种技术的好处是:
- 不需要使用指向对象的指针
- 由于不需要 vtbl 而节省内存
- 不会因为未初始化的 vtbl 而在运行时通过空指针调用虚函数
- 所有函数调用在编译时被解析,所以可以被优化
虽然在这个例子里 vtbl 的节约看起来并不明显(只不过 4 个字节),不过可以考虑在有 15 个基类,有的类有 20 个方法的情况下,累计的节省有多少。
ATL 窗口类
很好,背景知识足够了!是钻研 ATL 的时候了。ATL 采用严格的接口/实现相分离来设计,这在窗口类中也很明显。这和 COM 类似,接口的定义与实现完全分离(或者可能有多个实现)。
ATL 有一个类定义了窗口的“接口”,也就是对一个窗口可以做什么。这个类就是 CWindow。它只是对 HWND 的一个封装,提供了几乎所有的以 HWND 作为第一个参数的 User32 API,如 SetWindowText() 和 DestroyWindow()。 CWindow 有一个当你需要原始 HWND 时可以访问的公用成员 m_hWnd。 CWindow 也有一个 operator HWND 方法,于是你可以传递 CWindow 对象到接受 HWND 的函数中。没有 CWnd::GetSafeHwnd()的等价物。
CWindow 和 MFC 的 CWnd 很不同。创建 CWindow 对象不需多少代价,因为它只有一个数据成员,而且也没有 MFC 内部用以保存 HWND 到 CWnd 对象的对应关系的对象映射表。还与 CWnd不一样的是,当一个 CWindow 对象离开作用域,关联的窗口不会被销毁。这意味着你不须总是记住要把你创建的临时 CWindow 对象与关联的窗口脱离。
ATL 中窗口的实现类是 CWindowImpl。 CWindowImpl 包含了做这些事情的代码,比如窗口类注册,窗口子类化,消息映射,以及一个基本的 WindowProc()。再次不同于 MFC,在MFC 中,所有这些在同一个类里: CWnd。
还有两个独立的类,包含了对对话框的实现, CDialogImpl 和 CAxDialogImpl。 CDialogImpl 用于普通的对话框,而 CAxDialogImpl 用于要掌控 ActiveX 控件的对话框。
定义窗口实现
任何要创建的非对话框窗口应该从 CWindowImpl 派生。新类里需要包含三样:
- 窗口类定义
- 消息映射
- 窗口使用的默认风格,称作窗口修饰
窗口类的定义使用 DECLARE_WND_CLASS 或者 DECLARE_WND_CLASS_EX 宏来完成。它们都定义了一个封装了 WNDCLASSEX 结构的 ATL 结构 CWndClassInfo。 DECLARE_WND_CLASS 允许指定新窗口类的名字,其余成员使用缺省值;而 DECLARE_WND_CLASS_EX 还允许指定类风格和窗口背景色。类名也可以用 NULL,ATL 将生成一个。
我们从一个新的类的定义开始,随后的章节里我会逐步向其中增加内容。
1 2 3 4 5 |
class CMyWindow : public CWindowImpl<CMyWindow> { public: DECLARE_WND_CLASS(_T("My Window Class")) }; |
接下来是消息映射。ATL 的消息映射比 MFC 的映射简单的多。一个 ATL 映射被展开为一个大的 switch 语句。switch 查找合适的处理器并调用相应的函数。消息映射的宏是 BEGIN_MSG_MAP 和 END_MSG_MAP 。向窗口中添加一个空的映射。
1 2 3 4 5 6 7 8 |
class CMyWindow : public CWindowImpl<CMyWindow> { public: DECLARE_WND_CLASS(_T("My Window Class")) BEGIN_MSG_MAP(CMyWindow) END_MSG_MAP() }; |
我会在下一节中讲解如何向映射中添加处理器。最后,我们要为我们的类定义窗口修饰。窗口修饰是窗口风格和窗口扩展风格的组合,这些风格在创建窗口时会被用到。这些风格作为模板参数被指定,所以调用者在创建窗口时可以不被如何得到正确的风格而烦恼。这是一个示例的修饰定义,使用了 ATL 类 CWinTraits:
1 2 3 4 5 6 7 8 9 10 11 |
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() }; |
调用者可以在 CMyWindowTraits 定义中覆盖这些风格,不过通常没有必要。ATL 还有一些专用的预定义 CWinTraits ,其中适用于像我们这样的顶级窗口的一个是 CFrameWinTraits:
1 2 3 4 |
typedef CWinTraits<WS_OVERLAPPEDWINDOW | WS_CLIPCHILDREN | WS_CLIPSIBLINGS, WS_EX_APPWINDOW | WS_EX_WINDOWEDGE> CFrameWinTraits; |
填充消息映射
ATL 的消息映射对开发人员来讲是一个缺乏友好性的地方,同时也是 WTL 极大的增强了的地方。ClassView 提供了添加消息处理器的功能,但 ATL 没有类似于 MFC 的特定消息相关的宏和自动化参数解析。在 ATL 里,只有三种类型的消息处理器,一个针对 WM_NOTIFY,一个针对 WM_COMMAND,一个针对其他所有的消息。我们从向窗口添加 WM_CLOSE 和 WM_DESTROY 的消息处理器开始。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 |
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; } }; |
你会注意到处理器接收原始的 WPARAM 和 LPARAM 值,当消息使用这些参数时你需要自己去解析它们。还有第四个参数, bHandled。这个参数在处理期调用之前被 ATL 设为 TRUE。如果你希望 ATL 的缺省 WindowProc() 在你的处理器返回之后也能处理消息,你可以将 bHandled 设为 FALSE。这和 MFC 不一样,MFC 中必须显式调用消息处理器的基类实现。
我们再添加 WM_COMMAND 的处理器。假定我们窗口的菜单有一个 ID 为 IDC_ABOUT 的 About 项:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 |
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; } }; |
注意, COMMAND_HANDLER 宏为你做了解析消息参数的工作。 NOTIFY_HANDLER 宏类似地解析 WM_NOTIFY 的消息参数。
高级消息映射和嵌入(Mix-in)类
ATL 中一个最大的不同是任何 C++ 类都可以处理消息,不像 MFC 里消息处理的任务在 CWnd 和 CCmdTarget 间分割开来,再加上几个有 PreTranslateMessage() 方法的类。这一能力允许我们写通常被称作嵌入类的东西,以便通过向继承列表中添加类就可以向我们的窗口中增加特性。
带有消息映射的基类通常是一个以派生类名作为模板参数的模板,这样就能访问派生类中像 m_hWnd( CWindow 中的 HWND 成员)这样的成员。我们来看一下这个嵌入类,它通过处理 WM_ERASEBKGND 来绘制窗口背景。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 |
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; }; |
我们来审视一下这个新类。首先, CPaintBkgnd 有两个模板参数:派生类的名字: CPaintBkgnd,和背景的颜色( t_ 前缀通常用于值类型的模板参数)
构造函数和析构函数相当简单,它们创建/销毁了一个 t_crBrushColor 颜色的画刷。接下来的消息映射处理了 WM_ERASEBKGND。最后, OnEraseBkgnd() 处理器使用构造函数中创建的画刷来填充窗口。 OnEraseBkgnd() 中有两件事值得注意。首先,它使用了派生类的窗口函数(名为 GetClientRect())。我们怎么知道派生类里恰好有一个 GetClientRect() 函数?但如果没有的话,代码根本不能编译!编译器确保派生类 T 派生于 CWindow。其次, OnEraseBkgnd() 从 wParam 处得到设备上下文。
在我们的窗口中使用这一嵌入类,要做两件事。首先,我们把它加到继承列表中:
1 2 |
class CMyWindow : public CWindowImpl<CMyWindow, CWindow, CFrameWinTraits>, <span style="color: red;">public CPaintBkgnd<CMyWindow, RGB(0,0,255)></span> |
然后,我们要使 CMyWindow 传递消息到 CPaintBkgnd。这叫做串联消息映射。在 CMyWindow 的消息映射里,加入 CHAIN_MSG_MAP 宏:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 |
class CMyWindow : public CWindowImpl<CMyWindow, CWindow, CFrameWinTraits>, public CPaintBkgnd<CMyWindow, RGB(0,0,255)> { ... <span style="color: red;">typedef CPaintBkgnd<CMyWindow, RGB(0,0,255)> CPaintBkgndBase;</span> BEGIN_MSG_MAP(CMyWindow) MESSAGE_HANDLER(WM_CLOSE, OnClose) MESSAGE_HANDLER(WM_DESTROY, OnDestroy) COMMAND_HANDLER(IDC_ABOUT, OnAbout) <span style="color: red;">CHAIN_MSG_MAP(CPaintBkgndBase)</span> END_MSG_MAP() ... }; |
任何到达 CMyWindow 映射而没有处理的消息将被传递到 CPaintBkgnd 的映射中。注意, WM_CLOSE, WM_DESTROY,和 IDC_ABOUT 不会被 串联,因为只要它们一被处理,对消息映射的搜索就会终止。typedef 是必要的,因为 CHAIN_MSG_MAP 是个接收单个参数的预处理宏;如果我们以 CPaintBkgnd<CMyWindow, RGB(0,0,255)> 作为参数,其中的逗号会使预处理器认为我们以不止一个参数来调用该宏。
你可以在继承列表中放心的使用多个嵌入类,对每一个类使用 CHAIN_MSG_MAP 宏以使消息能够传入。这不同于 MFC,每个 CWnd 派生类只能有一个基类,而且 MFC 自动向基类传递未处理的消息。
ATL EXE 的结构
现在我们有了一个完整的(虽然不怎么有用)主窗口,我们来看看如何在程序中使用它。ATL 的可执行程序里有一个或者多个大致对应于 MFC 程序中的全局 CWinApp (通常名为 theApp)的全局变量。这一领域在 VC6 和 VC7 之间从根本上被改变了,所以我需要分别介绍这两个版本。
VC 6 的情形
ATL 的可执行程序包含一个全局的 CComModule 变量,这个变量必须命名为 _Module。我们的 stdafx.h 以此开始:
1 2 3 4 5 6 7 |
// 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 文件里,声明(译者注:应该为定义) _Module 变量:
1 2 |
// main.cpp: CComModule _Module; |
CComModule 中有我们需要在 WinMain() 中调用的显式初始化/退出函数,所以我们以此为始:
1 2 3 4 5 6 7 8 9 |
// main.cpp: CComModule _Module; int WINAPI WinMain(HINSTANCE hInst, HINSTANCE hInstPrev, LPSTR szCmdLine, int nCmdShow) { _Module.Init(NULL, hInst); _Module.Term(); } |
传给 Init() 的第一个参数仅在 COM 服务器中使用。而我们的 EXE 不是 COM 服务器,所以我们只需要传入 NULL。ATL 并不像 MFC 那样提供自己的 WinMain() 或者消息泵,所以要使我们的程序运行起来,需要创建 CMyWindow 对象并添加消息泵。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 |
// 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; } |
以上代码中唯一与众不同的是 CWindow::rcDefault,它是 CWindow 的一个 RECT 成员。将它作为窗口的初始 RECT 就像在 CreateWindow() API 里用 CW_USEDEFAULT 表示宽和高(译者注:此处有误, CW_USEDEFAULT 是用以表示左坐标和宽的)。
ATL 在底下使用了一些汇编语言的把戏来把主窗口的句柄和与之相应的 CMyWindow 对象联系起来。在此之上就是我们可以把 CWindow 对象在线程间传来传去而不出问题,而如果在 MFC 里对 CWnd 这样干会死得很惨。
VC 7 的情形
ATL 7 把模块管理代码分散到了好几个类中。出于兼容的目的, CComModule 仍然存在,在 VC 6 里写的代码经过 VC 7 转换后并不是总能干干净净地编译过去,如果不是根本编译不过去的话。因此在这我介绍一下新的类。
在 VC 7 里,ATL 的头文件自动声明所有模块类的全局实例,而且还为你调用了 Init() 和 Term() 方法,因此这些手动步骤就不再需要了。于是我们的 stdafx.h 看起来就是这样:
1 2 3 4 5 6 |
// stdafx.h: #define STRICT #define WIN32_LEAN_AND_MEAN #include <atlbase.h> // Base ATL classes #include <atlwin.h> // ATL windowing classes |
WinMain() 函数不调用任何 _Module 方法,就像这样:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 |
// 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 有两个对话框类。我们将为我们的对话框使用 CDialogImpl。创建一个新的对话框类与创建一个新的框架窗口类大致相当,只不过有两处不同:
- 基类是 CDialogImpl 而不是 CWindowImpl
- 需要定义一个名为 IDD 的公用成员,其中包含有对话框的资源 ID
这是新的 About 对话框类的初始定义:
1 2 3 4 5 6 7 8 |
class CAboutDlg : public CDialogImpl<CAboutDlg> { public: enum { IDD = IDD_ABOUT }; BEGIN_MSG_MAP(CAboutDlg) END_MSG_MAP() }; |
ATL 没有针对 OK 和 Cancel 按钮的内建处理器,所以我们需要自己写代码,顺便还有 WM_CLOSE 的处理器,当用户点击标题条上的关闭按钮时此处理器会被调用。我们还需要处理 WM_INITDIALOG 以使对话框出现时能正确的设置键盘焦点。这是带有消息处理器的完整的类定义。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 |
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 使用同一个处理器以演示 wID 参数,该参数的值被设为 IDOK 或 IDCANCEL,这取决于哪个按钮被点击。
显示对话框和 MFC 相似,创建新类的一个对象并调用 DoModal()。回到我们的主窗口并添加一个拥有 About 菜单项的菜单,该项将显示我们的 About 对话框。我们需要添加两个消息处理器,一个是为 WM_CREATE 而另一个是为新的菜单项 IDC_ABOUT。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 |
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 里是向 CDialog 的构造函数传递父窗口,而在 ATL 中应该把父窗口作为 DoModal() 的第一个参数传递。如果像上面的代码一样不指定,ATL 使用 GetActiveWindow()(将会是我们的框架窗口 )的返回结果作为父窗口。
LoadMenu() 调用也演示了 CComModule 的一个方法: GetResourceInstance()。它返回一个含有资源的模块的 HINSTANCE,就像 AfxGetResourceHandle() 一样,默认的行为是返回 EXE 的 HINSTANCE(还有一个 CComModule::GetModuleInstance(),此函数与 AfxGetInstanceHandle()相仿 )。
注意,由于模块管理类的不同,VC6 和 7 的 OnCreate() 是不同的, GetModuleInstance() 现在是在 CAtlBaseModule 里,而我们调用的是 ATL 为我们准备好的 _AtlBaseModule 对象。
这是我们修改后的主窗口和 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 日:首次发布
2005 年 12 月 15 日:更新,包括了 VC 7.1 中 ATL 的改变
链接:下一部分