面向 MFC 程序员的 WTL,第一部分 - ATL GUI 类






4.96/5 (296投票s)
2003 年 3 月 22 日
21分钟阅读

1152591

7151
面向 MFC 开发人员的 WTL 编程简介。
目录
- README.TXT
- 系列介绍
- 第一部分介绍
- ATL 背景
- ATL 窗口类
- 定义窗口实现
- 高级消息映射和混入类
- ATL EXE 的结构
- ATL 中的对话框
- 我保证会讲到 WTL!
- 版权和许可
- 修订历史
README.TXT
这是在继续阅读或向本文讨论区发布消息之前,我希望您首先阅读的内容。
本系列文章最初涵盖 WTL 7.0,并为 VC 6 用户编写。现在 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),因此您无法使用 Express 版构建 ATL 或 WTL 项目。
如果您正在使用 VC 6,那么您需要 Platform SDK。没有它,您无法使用 WTL。您可以使用网络安装版本,或者下载 CAB 文件或 ISO 镜像并在本地运行设置。请务必使用该工具将 SDK include 和 lib 目录添加到 VC 搜索路径中。您可以在 Platform SDK 程序组的 Visual Studio Registration 文件夹中找到它。即使您正在使用 VC 7,也最好获取最新的 Platform SDK,以便拥有最新的头文件和库。
您需要 WTL。从 Microsoft 下载 7.1 版。有关安装文件的一些提示,请参阅文章 “WTL 简介 - 第一部分”和 “WTL 的轻松安装”。这些文章现在已经相当过时,但仍然包含一些有用的信息。WTL 分发包中也有一个包含安装说明的 readme 文件。这些文章中没有提到的一点是如何将 WTL 文件添加到 VC include 路径中。在 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 include 路径,如果您尚未更新 Platform SDK,您必须更改默认目录列表。确保 $(VCInstallDir)PlatformSDK\include
在列表中排在 ($VCInstallDir)include
之前,如下所示:
您需要了解 MFC,并且要足够了解,以理解消息映射宏背后的内容,并能够毫无问题地编辑标记为“DO NOT EDIT”的代码。
您需要了解 Win32 API 编程,并且要非常了解。如果您通过直接进入 MFC 而没有学习消息如何在 API 级别工作来学习 Windows 编程,那么您很不幸地将在 WTL 中遇到麻烦。如果您不知道消息的 WPARAM
和 LPARAM
的含义,您应该阅读其他关于 API 级别编程的文章(CodeProject 上有很多),以便您理解。
您需要了解 C++ 模板语法。有关 C++ FAQ 和模板 FAQ 的链接,请参阅 VC 论坛 FAQ。
由于我尚未开始使用 VC 8,因此我不知道示例代码是否能在 8 上编译。希望 7 到 8 的升级过程能比 6 到 7 的过程更好。如果您在使用 VC 8 时遇到任何问题,请在本文章的论坛中发布。
系列介绍
WTL 太棒了。确实如此。它拥有 MFC GUI 类的大部分功能,但生成的 EXE 文件却小得多。如果您和我一样,通过 MFC 学习了 GUI 编程,您会非常习惯 MFC 提供的所有控件包装器,以及 MFC 中内置的灵活消息处理。如果您也和我一样,并且不喜欢在程序中添加数百 KB 的 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`,而 `CSplitterWindow` 又包含了 MFC `CDialog`s —— 这不是我炫耀,而是在修改现有 MFC 代码但使用更友好的 WTL 分割器)。
在本系列中,我将首先介绍 ATL 窗口类。毕竟,WTL 是 ATL 的一组附加类,因此对 ATL 窗口的良好理解至关重要。一旦我涵盖了这一点,我将介绍 WTL 功能,并展示它们如何使 GUI 编程变得更加容易。
第一部分简介
WTL 太棒了。但在我们讨论原因之前,我们首先需要了解 ATL。WTL 是 ATL 的一组附加类,如果您以前一直是一名纯粹的 MFC 程序员,您可能从未接触过 ATL GUI 类。所以,如果我没有立刻讲到 WTL,请耐心等待;绕道进入 ATL 是必要的。
在第一部分中,我将简要介绍 ATL 的背景,涵盖在编写 ATL 代码之前需要了解的一些基本要点,快速解释那些奇特的 ATL 模板,并涵盖基本的 ATL 窗口类。
ATL 背景
ATL 和 WTL 历史
Active Template Library... 有点奇怪的名字,不是吗?老用户可能还记得它最初是 ActiveX Template Library,这是一个更准确的名字,因为 ATL 的目标是使编写 COM 对象和 ActiveX 控件变得更容易。(ATL 也是在微软将新产品命名为 ActiveX-something 的时期开发的,就像现在的新产品都叫做 something.NET 一样。) 由于 ATL 实际上是关于编写 COM 对象的,因此它只拥有最基本的 GUI 类,相当于 MFC 的 CWnd
和 CDialog
。幸运的是,GUI 类足够灵活,允许在其之上构建像 WTL 这样的东西。
WTL 作为微软拥有的项目经历了两次主要修订,编号为 3 和 7。(版本号选择与 ATL 版本号匹配,这就是为什么它们不是 1 和 2。)版本 3.1 现已过时,因此本系列将不涵盖。版本 7.0 是对版本 3 的重大更新,版本 7.1 添加了一些错误修复和小功能。
在版本 7.1 之后,Microsoft 将 WTL 变成了开源项目,托管在 Sourceforge。该网站的最新版本是 7.5。我还没有查看 7.5,因此本系列目前不会涵盖 7.5。(我已经落后两个版本了!我最终会赶上的。)
ATL 风格的模板
即使您阅读 C++ 模板不会头疼,ATL 做的两件事也可能一开始让您感到困惑。以这个类为例:
class CMyWnd : public CWindowImpl<CMyWnd> { ... };
这实际上是合法的,因为 C++ 规范规定,在 `class CMyWnd` 部分之后,`CMyWnd` 名称被定义并可以在继承列表中使用。将类名作为模板参数的原因是 ATL 可以实现第二个棘手的事情,即编译时虚函数调用。
要亲身了解这一点,请看这组类:
template <class T> class B1 { public: void SayHi() { T* pT = static_cast<T*>(this); // HUH?? I'll explain this below pT->PrintClassName(); } void PrintClassName() { cout << "This is B1"; } }; class D1 : public B1<D1> { // No overridden functions at all }; class D2 : public B1<D2> { void PrintClassName() { cout << "This is D2"; } }; int main() { D1 d1; D2 d2; d1.SayHi(); // prints "This is B1" d2.SayHi(); // prints "This is D2" return 0; }
static_cast<T*>(this)
是这里的诀窍。它将 this
(类型为 B1*
)转换为 D1*
或 D2*
,具体取决于调用哪个特化。因为模板代码是在编译时生成的,所以只要继承列表编写正确,此转换就保证是安全的。(如果您编写 class D3 : public B1<D2>
,您就会遇到麻烦。)它是安全的,因为 this
对象只能是 D1*
或 D2*
(视情况而定),而不能是其他类型。请注意,这几乎与正常的 C++ 多态性完全相同,只是 SayHi()
方法不是虚的。
为了解释这是如何工作的,让我们看看对 `SayHi()` 的每个调用。在第一次调用中,使用了特化 `B1
void B1<D1>::SayHi() { D1* pT = static_cast<D1*>(this); pT->PrintClassName(); }
由于 D1
没有重写 PrintClassName()
,因此会搜索 D1
的基类。B1
有一个 PrintClassName()
方法,因此调用该方法。
现在,来看第二次调用 SayHi()
。这次,它使用了特化 B1<D2>
,SayHi()
扩展为:
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
有一个公共成员 m_hWnd
,如果您需要原始的 HWND
,可以访问它。CWindow
还有一个 operator HWND
方法,因此您可以将 CWindow
对象传递给接受 HWND
的函数。没有等同于 CWnd::GetSafeHwnd()
的方法。
CWindow
与 MFC 的 CWnd
非常不同。CWindow
对象的创建成本很低,因为它只有一个数据成员,并且没有等同于 MFC 内部维护的对象映射(用于将 HWND
映射到 CWnd
对象)。此外,与 CWnd
不同,当 CWindow
对象超出作用域时,关联的窗口不会被销毁。这意味着您不必记住分离任何可能创建的临时 CWindow
对象。
包含窗口实现的 ATL 类是 CWindowImpl
。CWindowImpl
包含窗口类注册、窗口子类化、消息映射和基本 WindowProc()
等代码。这与 MFC 中所有内容都在一个类 CWnd
中的情况不同。
还有两个单独的类,它们包含对话框的实现,分别是 CDialogImpl
和 CAxDialogImpl
。CDialogImpl
用于普通对话框,而 CAxDialogImpl
用于托管 ActiveX 控件的对话框。
定义窗口实现
您创建的任何非对话框窗口都将派生自 CWindowImpl
。您的新类需要包含三样东西:
- 窗口类定义
- 消息映射
- 用于窗口的默认样式,称为 窗口特性
窗口类定义使用 DECLARE_WND_CLASS
或 DECLARE_WND_CLASS_EX
宏完成。这两个宏都定义了一个 ATL 结构 CWndClassInfo
,它包装了 WNDCLASSEX
结构。DECLARE_WND_CLASS
允许您指定新的窗口类名并使用其他成员的默认值,而 DECLARE_WND_CLASS_EX
允许您还指定类样式和窗口背景颜色。您也可以对类名使用 NULL
,ATL 将为您生成一个名称。
让我们开始一个新类的定义,我将在本节中逐步添加内容。
class CMyWindow : public CWindowImpl<CMyWindow> { public: DECLARE_WND_CLASS(_T("My Window Class")) };
接下来是消息映射。ATL 消息映射比 MFC 映射简单得多。ATL 映射扩展为一个大的 switch 语句;switch 查找正确的处理程序并调用相应的函数。消息映射的宏是 BEGIN_MSG_MAP
和 END_MSG_MAP
。让我们为窗口添加一个空映射。
class CMyWindow : public CWindowImpl<CMyWindow> { public: DECLARE_WND_CLASS(_T("My Window Class")) BEGIN_MSG_MAP(CMyWindow) END_MSG_MAP() };
我将在下一节介绍如何将处理程序添加到映射中。最后,我们需要为我们的类定义 *窗口特性*。窗口特性是窗口样式和扩展窗口样式的组合,用于创建窗口时。样式被指定为模板参数,这样调用者就不必在创建我们的窗口时为样式正确而烦恼。这是一个使用 ATL 类 `CWinTraits` 的示例特性定义:
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`。
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
的处理程序。
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
的“关于”项:
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` 消息参数。
高级消息映射和混入类
ATL 的一个主要区别是 *任何* C++ 类都可以处理消息,不像 MFC 中消息处理职责在 `CWnd` 和 `CCmdTarget` 之间划分,加上一些具有 `PreTranslateMessage()` 方法的类。这种能力让我们能够编写通常被称为“混入”类,这样我们只需向继承列表中添加类就可以向窗口添加功能。
一个带有消息映射的基类通常是一个模板,它将派生类名作为模板参数,这样它就可以访问派生类的成员,例如 `m_hWnd`(`CWindow` 中的 `HWND` 成员)。让我们来看一个通过处理 `WM_ERASEBKGND` 来绘制窗口背景的混入类。
template <class T, COLORREF t_crBrushColor> class CPaintBkgnd { 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` 是否包含我们通过 `pT` 变量调用的方法。其次,`OnEraseBkgnd()` 必须从 `wParam` 中解包设备上下文。
要将这个混入类与我们的窗口一起使用,我们需要做两件事。首先,将其添加到继承列表中:
class CMyWindow : public CWindowImpl<CMyWindow, CWindow, CFrameWinTraits>, public CPaintBkgnd<CMyWindow, RGB(0,0,255)>
其次,我们需要让 CMyWindow
将消息传递给 CPaintBkgnd
。这称为 *链式* 消息映射。在 CMyWindow
消息映射中,我们添加 CHAIN_MSG_MAP
宏:
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() ... };
任何在未处理的情况下到达 `CHAIN_MSG_MAP` 行的消息都将传递给 `CPaintBkgnd` 中的映射。请注意,`WM_CLOSE`、`WM_DESTROY` 和 `IDC_ABOUT` 将**不会**被链式处理,因为一旦它们被处理,消息映射搜索就结束了。`typedef` 是必要的,因为 `CHAIN_MSG_MAP` 是一个接受一个参数的预处理器宏;如果我们将 `CPaintBkgnd
您可以想象在继承列表中有几个混入类,每个类都有一个 CHAIN_MSG_MAP
宏,以便消息可以传递给它。这与 MFC 不同,MFC 中每个派生自 CWnd
的类只能有一个基类,并且 MFC 会自动将未处理的消息传递给基类。
ATL EXE 的结构
现在我们有了一个完整(尽管不完全有用)的主窗口,让我们看看如何在程序中使用它。ATL EXE 有一个或多个全局变量,大致对应于 MFC 程序中的全局 CWinApp
变量(通常称为 theApp
)。ATL 的这部分在 VC6 和 VC7 之间发生了巨大变化,因此我将分别介绍这两个版本。
在 VC 6 中
ATL 可执行文件包含一个全局 `CComModule` 变量,该变量*必须*命名为 `_Module`。下面是我们的 *stdafx.h* 的开头:
// stdafx.h: #define STRICT #define WIN32_LEAN_AND_MEAN #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
变量:
// main.cpp:
CComModule _Module;
CComModule
具有显式的初始化和关闭函数,我们需要在 WinMain()
中调用它们,所以让我们从这些开始:
// main.cpp: CComModule _Module; int WINAPI WinMain(HINSTANCE hInst, HINSTANCE hInstPrev, LPSTR szCmdLine, int nCmdShow) { _Module.Init(NULL, hInst); _Module.Term(); }
Init()
的第一个参数仅用于 COM 服务器。由于我们的 EXE 不是服务器,我们可以传递 NULL
。ATL 不像 MFC 那样提供自己的 WinMain()
或消息泵,所以为了让程序运行,我们创建一个 CMyWindow
对象并添加一个消息泵。
// 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
来表示宽度和高度一样。
在底层,ATL 使用一些汇编语言的“黑魔法”将主窗口句柄连接到其对应的 CMyWindow
对象。这样做的好处是,在线程之间传递 CWindow
对象没有问题,而这在 MFC 中使用 CWnd
对象时会惨败。
在 VC 7 中
ATL 7 将模块管理代码分成了几个类。CComModule
仍然存在以实现向后兼容性,但 VC 7 转换的 VC 6 中编写的代码并不总是能干净地编译(如果能编译的话),因此我将在这里介绍新类。
在 VC 7 中,ATL 头文件会自动声明所有模块类的全局实例,并且会自动为您调用 Init()
和 Term()
方法,因此这些手动步骤不是必需的。因此,我们的 stdafx.h 看起来像:
// stdafx.h: #define STRICT #define WIN32_LEAN_AND_MEAN #include <atlbase.h> // Base ATL classes #include <atlwin.h> // ATL windowing classes
WinMain()
函数不调用任何 _Module
方法,看起来像:
// 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; }
这是我们的窗口的样子:
我承认没什么特别激动人心的。为了增添趣味,我们将添加一个“关于”菜单项,它会显示一个对话框。
ATL 中的对话框
如前所述,ATL 有两个对话框类。我们将使用 CDialogImpl
作为我们的关于对话框。创建一个新的对话框类几乎就像创建一个新的框架窗口类;只有两个区别:
- 基类是
CDialogImpl
而不是CWindowImpl
。 - 您需要定义一个名为 `IDD` 的公共成员,该成员保存对话框的资源 ID。
下面是一个关于对话框新类定义的开头:
class CAboutDlg : public CDialogImpl<CAboutDlg> { public: enum { IDD = IDD_ABOUT }; BEGIN_MSG_MAP(CAboutDlg) END_MSG_MAP() };
ATL 没有为“确定”和“取消”按钮提供内置处理程序,因此我们需要自己编写它们,以及一个 WM_CLOSE
处理程序,该处理程序在用户单击标题栏中的关闭按钮时调用。我们还需要处理 WM_INITDIALOG
,以便在对话框出现时正确设置键盘焦点。以下是带有消息处理程序的完整类定义。
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; } };
我为“确定”和“取消”使用了同一个处理程序,以演示 `wID` 参数,它根据单击的按钮设置为 `IDOK` 或 `IDCANCEL`。
显示对话框类似于 MFC,您创建一个新类的对象并调用 DoModal()
。让我们回到主窗口,添加一个菜单,其中包含一个“关于”项,用于显示我们新的关于对话框。我们需要添加两个消息处理程序,一个用于 WM_CREATE
,一个用于新的菜单项 IDC_ABOUT
。
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()
。)
请注意,`OnCreate()` 在 VC6 和 7 之间有所不同,这是由于模块管理类的不同。`GetResourceInstance()` 现在位于 `CAtlBaseModule` 中,我们调用 ATL 为我们设置的全局 `_AtlBaseModule` 对象。
这是我们修改后的主窗口和关于对话框:
我保证会讲到 WTL!
但这将在第二部分中。由于我为 MFC 开发人员编写这些文章,我觉得最好先介绍 ATL,然后再深入 WTL。如果这是您第一次接触 ATL,现在可能是时候自己编写一些简单的应用程序了,这样您就可以掌握消息映射和使用混入类。您还可以尝试 ClassView 对 ATL 消息映射的支持,因为它能够为您添加消息处理程序。要在 VC 6 中开始,右键单击 *CMyWindow* 项并在上下文菜单中选择 *Add Windows Message Handler*。在 VC 7 中,右键单击 *CMyWindow* 项并在上下文菜单中选择 *Properties*。在属性窗口中,单击工具栏上的 *Messages* 按钮以查看窗口消息列表。您可以通过转到其行、单击右列将其转换为组合框、单击组合框箭头,然后单击下拉列表中的 `
在第二部分中,我将介绍 WTL 的基本窗口类、WTL AppWizard 和更友好的消息映射宏。
版权和许可
本文受版权保护,版权所有 (c)2003-2005 Michael Dunn。我知道这并不能阻止人们在网络上随意复制它,但我还是要说一下。如果您有兴趣翻译本文,请给我发邮件告知。我预计不会拒绝任何人的翻译许可,我只是想知道翻译的存在,以便我可以在此处发布链接。
本文附带的演示代码已发布到公共领域。我以这种方式发布,以便代码可以造福所有人。(我没有将文章本身发布到公共领域,因为仅在 CodeProject 上提供文章有助于提高我自己的知名度以及 CodeProject 网站的知名度。)如果您在自己的应用程序中使用演示代码,如果您能给我发一封电子邮件告知(只是为了满足我的好奇心,想知道人们是否从我的代码中受益),我将不胜感激,但这不是必需的。在您自己的源代码中注明出处也值得赞赏,但不是必需的。
修订历史
- 2003 年 3 月 22 日:文章首次发布。
- 2005 年 12 月 15 日:更新以涵盖 VC 7.1 中的 ATL 更改。
系列导航:» 第二部分(WTL GUI 基类)