65.9K
CodeProject 正在变化。 阅读更多。
Home

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

starIconstarIconstarIconstarIcon
emptyStarIcon
starIcon

4.96/5 (296投票s)

2003 年 3 月 22 日

21分钟阅读

viewsIcon

1152591

downloadIcon

7151

面向 MFC 开发人员的 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 之前,如下所示:

 [VC7 include path list - 26K]

您需要了解 MFC,并且要足够了解,以理解消息映射宏背后的内容,并能够毫无问题地编辑标记为“DO NOT EDIT”的代码。

您需要了解 Win32 API 编程,并且要非常了解。如果您通过直接进入 MFC 而没有学习消息如何在 API 级别工作来学习 Windows 编程,那么您很不幸地将在 WTL 中遇到麻烦。如果您不知道消息的 WPARAMLPARAM 的含义,您应该阅读其他关于 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 的 CWndCDialog。幸运的是,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`,因此 `SayHi()` 代码扩展为:

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 类是 CWindowImplCWindowImpl 包含窗口类注册、窗口子类化、消息映射和基本 WindowProc() 等代码。这与 MFC 中所有内容都在一个类 CWnd 中的情况不同。

还有两个单独的类,它们包含对话框的实现,分别是 CDialogImplCAxDialogImplCDialogImpl 用于普通对话框,而 CAxDialogImpl 用于托管 ActiveX 控件的对话框。

定义窗口实现

您创建的任何非对话框窗口都将派生自 CWindowImpl。您的新类需要包含三样东西:

  1. 窗口类定义
  2. 消息映射
  3. 用于窗口的默认样式,称为 窗口特性

窗口类定义使用 DECLARE_WND_CLASSDECLARE_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_MAPEND_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_CLOSEWM_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;
    }
};

您会注意到处理程序获取原始的 WPARAMLPARAM 值;当消息使用这些参数时,您必须自行解包它们。还有第四个参数 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.htchar.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;
}

这是我们的窗口的样子:

 [First ATL window - 4K]

我承认没什么特别激动人心的。为了增添趣味,我们将添加一个“关于”菜单项,它会显示一个对话框。

ATL 中的对话框

如前所述,ATL 有两个对话框类。我们将使用 CDialogImpl 作为我们的关于对话框。创建一个新的对话框类几乎就像创建一个新的框架窗口类;只有两个区别:

  1. 基类是 CDialogImpl 而不是 CWindowImpl
  2. 您需要定义一个名为 `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` 对象。

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

 [About box - 5K]

我保证会讲到 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 基类)

面向 MFC 程序员的 WTL,第一部分 - ATL GUI 类 - CodeProject - 代码之家
© . All rights reserved.