💎一站式轻松地调用各大LLM模型接口,支持GPT4、智谱、星火、月之暗面及文生图 广告
# Part I - ATL GUI Classes 原作 :[**Michael Dunn**](http://www.codeproject.com/wtl/WTL4MFC1.asp) 翻译 :[Orbit(桔皮干了)](http://www.winmsg.com/cn/orbit.htm) ## 本章内容 * [README.TXT](#readme) * [对本系列文章的总体介绍](#seriesintro) * [对第一章的简单介绍](#intopart1) * [ATL 背景知识](#atlbackground) * [ATL 和 WTL的发展历史](#atlhistory) * [ATL-style 模板](#atltemplates) * [ATL 窗口类](#atlwindowing) * [定义一个窗口的实现](#windowimpl) * [填写消息映射链(message map)](#msgmap) * [高级消息映射链和嵌入类(Mix-in Classes)](#advmsgmap) * [ATL程序的结构](#exestructure) * [ATL中的对话框](#dialogs) * [我会继续讲WTL, 我保证!](#whereswtlman) * [修改记录](#revisionhistory) ## README.TXT 在你开始使用WTL或着在本文章的讨论区张贴消息之前,我想请你先阅读下面的材料。 你需要开发平台SDK(Platform SDK)。你要使用WTL不能没有它,你可以使用[在线升级](http://www.microsoft.com/msdownload/platformsdk/sdkupdate/)安装开发平台SDK,也可以[下载全部文件](http://www.microsoft.com/msdownload/platformsdk/sdkupdate/psdk-full.htm)后在本地安装。在使用之前要将SDK的包含文件(.h头文件)和库文件(.Lib文件)路径添加到VC的搜索目录,SDK有现成的工具完成这个工作,这个工具位于开发平台SDK程序组的“_Visual Studio Registration_”文件夹里。 你需要安装 WTL。你可以从微软的网站上[下载WTL的7.0版](http://download.microsoft.com/download/VisualStudioNET/Install/7.0/WXP/EN-US/WTL70.exe),在安装之前可以先查看“[Introduction to WTL - Part 1](http://www.codeproject.com/wtl/wtlintro1.asp)”和“[Easy installation of WTL](http://www.codeproject.com/wtl/wtlinst.asp)”这两篇文章,了解一下所要安装的文件的信息,虽然现在这些文章有些过时,但还是可以提供很多有用的信息。有一件我认为不该在本篇文章中提到的事是告诉VC如何搜索WTL的包含文件路径,如果你用的VC6,用鼠标点击 _Tools\Options_,转到_Directories_标签页,在显示路径的列表框中选择_Include Files_,然后将WTL的包含文件的存放路径添加到包含文件搜索路径列表中。 你需要了解MFC。很好地了解MFC将有助于你理解后面提到的有关消息映射的宏并能够编辑那些标有“不要编辑(DO NOT EDIT)”的代码而不会出现问题。 你需要清楚地知道如何使用Win32 API编程。如果你是直接从MFC开始学习Windows编程,没有学过API级别的消息处理方式,那很不幸你会在使用WTL时遇到麻烦。如果不了解Windows消息中WPARAM参数和LPARAM参数的意义,应该明白需要读一些这方面的文章(在CodeProject有大量的此类文章)。 你需要知道 C++ 模板的语法,你可以到[VC Forum FAQ](http://www.codeproject.com/cpp/cppforumfaq.asp#other_cpp) 相关的连接寻求答案。 我只是讨论了一些涵盖VC 6的特点,不过据我了解所有的程序都可以在VC 7上使用。由于我不使用VC 7,我无法对那些在VC 7中出现的问题提供帮助,不过你还是可以放心的在此张贴你的问题,因为其他的人可能会帮助你。 ## 对本系列文章的总体介绍 WTL 具有两面性,确实是这样的。它没有MFC的界面(GUI)类库那样功能强大,但是能够生成很小的可执行文件。如果你象我一样使用MFC进行界面编程,你会觉得MFC提供的界面控件封装使用起来非常舒服,更不用说MFC内置的消息处理机制。当然,如果你也象我一样不希望自己的程序仅仅因为使用了MFC的框架就增加几百K的大小的话,WTL就是你的选择。当然,我们还要克服一些障碍: * ATL样式的模板类初看起来有点怪异 * 没有类向导的支持,所以要手工处理所有的消息映射。 * MSDN没有正式的文档支持,你需要到处去收集有关的文档,甚至是查看WTL的源代码。 * 买不到参考书籍 * 没有微软的官方支持 * ATL/WTL的窗口与MFC的窗口有很大的不同,你所了解的有关MFC的知识并不全部适用与WTL。 从另一方面讲,WTL也有它自身的优势: * 不需要学习或掌握复杂的文档/视图框架。 * 具有MFC的基本的界面特色,比如DDX/DDV和命令状态的自动更新功能(译者加:比如菜单的Check标记和Enable标记)。 * 增强了一些MFC的特性(比如更加易用的分隔窗口)。 * 可生成比静态链接的MFC程序更小的可执行文件(译者加:WTL的所有源代码都是静态链接到你的程序中的)。 * 你可以修正自己使用的WTL中的错误(BUG)而不会影响其他的应用程序(相比之下,如果你修正了有BUG的MFC/CRT动态库就可能会引起其它应用程序的崩溃。 * 如果你仍然需要使用MFC,MFC的窗口和ATL/WTL的窗口可以“和平共处”。(例如我工作中的一个原型就使用了了MFC的CFrameWnd,并在其内包含了WTL的CSplitterWindow,在CSplitterWindow中又使用了MFC的CDialogs -- 我并不是为了炫耀什么,只是修改了MFC的代码使之能够使用WTL的分割窗口,它比MFC的分割窗口好的多)。 在这一系列文章中,我将首先介绍ATL的窗口类,毕竟WTL是构建与ATL之上的一系列附加类,所以需要很好的了解ATL的窗口类。介绍完ATL之后我将介绍WTL的特性以并展示它是如何使界面编程变得轻而易举。 ## 对第一章的简单介绍 WTL是个很酷的工具,在理解这一点之前需要首先介绍ATL。WTL是构建与ATL之上的一系列附加类,如果你是个严格使用MFC的程序员那么你可能没有机会接触到ATL的界面类,所以请容忍我在开始WTL之前先罗索一些别的东西,绕道来介绍一下ATL是很有必要地。 在本文的第一部分,我将给出一点ATL的背景知识,包括一些编写ATL代码必须知道的基本知识,快速的解释一些令人不知所措的ATL模板类和基本的ATL窗口类。 ## ATL 背景知识 ### ATL 和 WTL 的发展历史 “活动模板库”(Active Template Library)是一个很古怪的名字,不是吗?那些年纪大的人可能还记得它最初被称为“网络组件模板库”,这可能是它更准确的称呼,因为ATL的目的就是使编写组件对象和ActiveX控件更容易一些(ATL是在微软开发新产品ActiveX-某某的过程中开发的,那些ActiveX-某某现在被称为某某.NET)。由于ATL是为了便于编写组件对象而存在的,所以只提供了简单的界面类,相当于MFC的窗口类(CWnd)和对话框类(CDialog)。幸运的是这些类非常的灵活,能够在其基础上构建象WTL这样的附加类。 WTL现在已经是第二次修正了,最初的版本是3.1,现在的版本是7(WTL的版本号之所以这样选择是为了与ATL的版本匹配,所以不存在1和2这样的版本号)。WTL 3.1可以与VC 6和VC 7一起使用,但是在VC 7下需要定义几个预处理标号。WTL 7向下兼容WTL 3.1,并且不作任何修改就可以与VC 7一起使用,现在看来没有任何理由还使用3.1来进行新的开发工作。 ### ATL-style 模板 即使你能够毫不费力地阅读C++的模板类代码,仍然有两件事可能会使你有些头晕,以下面这个类的定义为例: ``` class CMyWnd : public CWindowImpl<CMyWnd> { ... }; ``` 这样作是合法的,因为C++的语法解释说即使CMyWnd类只是被部分定义,类名CMyWnd已经被列入递归继承列表,是可以使用的。将类名作为模板类的参数是因为ATL要做另一件诡秘的事情,那就是编译期间的虚函数调用机制。 如果你想要了解它是如何工作地,请看下面的例子: ``` template <class T> class B1 { public: void SayHi() { T* pT = static_cast<T*>(this); // HUH?? 我将在下面解释 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&lt;T*&gt;(this) 就是窍门所在。它根据函数调用时的特殊处理将指向B1类型的指针this指派为D1或D2类型的指针,因为模板代码是在编译其间生成的,所以只要编译器生成正确的继承列表,这样指派就是安全的。(如果你写成: ``` class D3 : public B1<D2> ``` 就会有麻烦) 之所以安全是因为this对象只可能是指向D1或D2(在某些情况下)类型的对象,不会是其他的东西。注意这很像C++的多态性(polymorphism),只是SayHi()方法不是虚函数。 要解释这是如何工作的,首先看对每个SayHi()函数的调用,在第一个函数调用,对象B1被指派为D1,所以代码被解释成: ``` void B1<D1>::SayHi() { D1* pT = static_cast<D1*>(this); pT->PrintClassName(); } ``` 由于D1没有重载PrintClassName(),所以查看基类B1,B1有PrintClassName(),所以B1的PrintClassName()被调用。 现在看第二个函数调用SayHi(),这一次对象被指派为D2类型,SayHi()被解释成: ``` void B1<D2>::SayHi() { D2* pT = static_cast<D2*>(this); pT->PrintClassName(); } ``` 这一次,D2含有PrintClassName()方法,所以D2的PrintClassName()方法被调用。 这种技术的有利之处在于: * 不需要使用指向对象的指针。 * 节省内存,因为不需要虚函数表。 * 因为没有虚函数表所以不会发生在运行时调用空指针指向的虚函数。 * 所有的函数调用在编译时确定(译者加:区别于C++的虚函数机制使用的动态编连),有利于编译程序对代码的优化。 节省虚函数表在这个例子中看起来无足轻重(每个虚函数只有4个字节),但是设想一下如果有15个基类,每个类含有20个方法,加起来就相当可观了。 ## ATL 窗口类 好了,关于ATL的背景知识已经讲的构多了,到了该正式讲ATL的时候了。ATL在设计时接口定义和实现是严格区分开的,这在窗口类的设计中是最明显的,这一点类似于COM,COM的接口定义和实现是完全分开的(或者可能有多个实现)。 ATL有一个专门为窗口设计的接口,可以做全部的窗口操作,这就是CWindow。它实际上就是对HWND操作的包装类,对几乎所有以HWND句柄为第一个参数的窗口API的进行了封装,例如:SetWindowText() 和 DestroyWindow()。CWindow类有一个公有成员m_hWnd,使你可以直接对窗口的句柄操作,CWindow还有一个操作符HWND,你可以讲CWindow对象传递给以HWND为参数的函数,但这与CWnd::GetSafeHwnd()(译者加:MFC的方法)没有任何等同之处。 CWindow 与 MFC 的CWnd类有很大的不同,创建一个CWindow对象占用很少的资源,因为只有一个数据成员,没有MFC窗口中的对象链,MFC内部维持这一个对象链,此对象链将HWND映射到CWnd对象。还有一点与MFC的CWnd类不同的是当一个CWindow对象超出了作用域,它关联的窗口并不被销毁掉,这意味着你并不需要随时记得分离你所创建的临时CWindow对象。 在ATL类中对窗口过程的实现是CWindowImpl。CWindowImpl 含有所有窗口实现代码,例如:窗口类的注册,窗口的子类化,消息映射以及基本的WindowProc()函数,可以看出这与MFC的设计有很大的不同,MFC将所有的代码都放在一个CWnd类中。 还有两个独立的类包含对话框的实现,它们分别是CDialogImpl 和 CAxDialogImpl,CDialogImpl 用于实现普通的对话框而CAxDialogImpl实现含有ActiveX控件的对话框。 ## 定义一个窗口的实现 任何非对话框窗口都是从CWindowImpl 派生的,你的新类需要包含三件事情: 1. 一个窗口类的定义 2. 一个消息映射链 3. 窗口使用的默认窗口类型,称为_window traits_ 窗口类的定义通过DECLARE_WND_CLASS宏或DECLARE_WND_CLASS_EX宏来实现。这辆个宏定义了一个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提供了几个预先定义的特殊的类型,其中之一就是CFrameWinTraits,一个非常棒的框架窗口: ``` typedef CWinTraits<WS_OVERLAPPEDWINDOW | WS_CLIPCHILDREN | WS_CLIPSIBLINGS, WS_EX_APPWINDOW | WS_EX_WINDOWEDGE> CFrameWinTraits; ``` ### 填写消息映射链 ATL的消息映射链是对开发者不太友好的部分,也是WTL对其改进最大的部分。类向导至少可以让你添加消息响应,然而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的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()`方法的类。ATL的这种特性允许我们编写所谓的“嵌入类”,为我们的窗口添加特性只需将该类添加到继承列表中就行了,就这么简单! 一个基本的带有消息映射链的类通常是模板类,将派生类的类名作为模板的参数,这样它就可以访问派生类中的成员,比如m_hWnd(CWindow类中的HWND成员)。让我们来看一个嵌入类的例子,这个嵌入类通过响应`WM_ERASEBKGND`消息来画窗口的背景。 ``` 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_ 前缀通常用来作为模板类的模板参数的前缀)CPaintBkgnd也是从CMessageMap派生的,这并不是必须的,因为所有需要响应消息的类只需使用`BEGIN_MSG_MAP`宏就足够了,所以你可能看到其他的一些嵌入类的例子代码,它们并不是从该基类派生的。 构造函数和析构函数都相当简单,只是创建和销毁Windows画刷,这个画刷由参数t_crBrushColor决定颜色。接着是消息映射链,它响应WM_ERASEBKGND消息,最后由响应函数OnEraseBkgnd()用构造函数创建的画刷填充窗口的背景。在OnEraseBkgnd()中有两件事需要注意,一个是它使用了一个派生的窗口类的方法(即GetClientRect()),我们如何知道派生类中有GetClientRect()方法呢?如果派生类中没有这个方法我们的代码也不会有任何抱怨,由编译器确认派生类T是从CWindow派生的。另一个是OnEraseBkgnd()没有将消息参数wParam展开为设备上下文(DC)。(WTL最终会解决这个问题,我们很快就可以看到,我保证) 要在我们的窗口中使用这个嵌入类需要做两件事:首先,将它加入到继承列表: ``` 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() ... }; ``` 任何CMyWindow没有处理的消息都被传递给CPaintBkgnd。应该注意的是WM_CLOSE,WM_DESTROY和IDC_ABOUT消息将不会传递,因为这些消息一旦被处理消息映射链的查找就会中止。使用typedef是必要地,因为宏是预处理宏,只能有一个参数,如果我们将CPaintBkgnd&lt;CMyWindow, RGB(0,0,255)&gt;作为参数传递,那个“,”会使预处理器认为我们使用了多个参数。 你可以在继承列表中使用多个嵌入类,每一个嵌入类使用一个CHAIN_MSG_MAP宏,这样消息映射链就会将消息传递给它。这与MFC不同,MFC地CWnd派生类只能有一个基类,MFC自动将消息传递给基类。 ## ATL程序的结构 到目前为止我们已经有了一个完整地主窗口类(即使不完全有用),让我们看看如何在程序中使用它。一个ATL程序包含一个CComModule类型的全局变量_Module,这和MFC的程序都有一个CWinApp类型的全局变量theApp有些类似,唯一不同的是在ATL中这个变量必须命名为_Module。 下面是stdafx.h文件的开始部分: ``` // stdafx.h: #define STRICT #define VC_EXTRALEAN #include <atlbase.h> // 基本的ATL类 extern CComModule _Module; // 全局_Module #include <atlwin.h> // ATL窗口类 ``` atlbase.h已经包含最基本的Window编程的头文件,所以我们不需要在包含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不含有COM对象,我们只需将NULL传递给Init()就行了。ATL不提供自己的WinMain()和类似MFC的消息泵,所以我们需要创建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。和调用CreateWindow() API时使用CW_USEDEFAULT指定窗口的宽度和高度一样,ATL使用rcDefault作为窗口的最初大小。 在ATL代码内部,ATL使用了一些类似汇编语言的魔法将主窗口的句柄与相应的CMyWindow对象联系起来,在外部看来就是可以毫无问题的在线程之间传递CWindow对象,而MFC的CWnd却不能这样作。 这就是我们的窗口: ![](https://box.kancloud.cn/2016-01-13_56962fba5c8f1.png) 我得承认这确实没有什么激动人心的地方。我们将添加一个About菜单并显示一个对话框,主要是为它增加一些情趣。 ## ATL中的对话框 我们前面提到过,ATL有两个对话框类,我们的About对话框使用CDialogImpl。生成一个新对话框和生成一个主窗口几乎一样,只有两点不同: 1. 窗口的基类是CDialogImpl而不是CWindowImpl。 2. 你需要定义名称为IDD的公有成员用来保存对话框资源的ID。 现在开始为About对话框定义一个新类: ``` class CAboutDlg : public CDialogImpl<CAboutDlg> { public: enum { IDD = IDD_ABOUT }; BEGIN_MSG_MAP(CAboutDlg) END_MSG_MAP() }; ``` ATL没有在内部实现对“OK”和“Cancel”两个按钮的响应处理,所以我们需要自己添加这些代码,如果用户用鼠标点击标题栏的关闭按钮,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; } }; ``` 我使用一个消息响应函数同时处理“OK”和“Cancel”两个按钮的WM_COMMAND消息,因为命令响应函数的wID参数就已经指明了消息是来自“OK”按钮还是来自“Cancel”按钮。 显示对话框的方法与MFC相似,创建一个新对话框类的实例,然后调用DoModal()方法。现在我们返回主窗口,添加一个带有About菜单项的菜单用来显示我们的对话框,这需要再添加两个消息响应函数,一个是响应`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(), MAKEINTRESOURCE(IDR_MENU1) ); SetMenu ( hmenu ); return 0; } LRESULT OnAbout(WORD wNotifyCode, WORD wID, HWND hWndCtl, BOOL& bHandled) { CAboutDlg dlg; dlg.DoModal(); return 0; } // ... }; ``` 在指定对话框的父窗口的方式上有些不同,MFC是通过构造函数将父窗口的指针传递给对话框而在ATL中是将父窗口的指针作为DoModal()方法的第一个参数传递给对话框的,如果象上面的代码一样没有指定父窗口,ATL会使用`GetActiveWindow()`得到的窗口(也就是我们的主框架窗口)作为对话框的父窗口。 对LoadMenu()方法的调用展示了CComModule的另一个方法-GetResourceInstance(),它返回你的EXE的HINSTANCE实例,和MFC的AfxGetResourceHandle()方法相似。(当然还有CComModule::GetModuleInstance(),它相当于MFC的AfxGetInstanceHandle()。) 这就是主窗口和对话框的显示效果: ![](https://box.kancloud.cn/2016-01-13_56962fba6cb9f.png) ## 我会继续讲WTL,我保证! 我会继续讲WTL的,只是会在第二部分。我觉得既然这些文章是写给使用MFC的开发者的,所以有必要在投入WTL之前先介绍一些ATL。如果你是第一次接触到ATL,那现在你就可以尝试写一些小程序,处理消息和使用嵌入类,你也可以尝试用类向导支持消息映射链,使它能够自动添加消息响应。现在就开始,右键单击CMyWindow项,在弹出的上下文菜单中单击“_Add Windows Message Handler_”菜单项。 在第二部分,我将全面介绍基本的WTL窗口类和一个更好的消息映射宏。 ## 修改记录 2003年3月22日,本文第一次发表。