第三章 ATL的窗口类
CWindowImpl、CWindow、CWinTraits,ATL窗口类的奥秘尽在此三者之中。在本章里,李马将为你详细解说它们的使用方法。另外,本章的内容也可以算是本书的核心部分——如果你要进行ATL的GUI程序设计的话,就必须将ATL的窗口类设计理念了然于心。
窗口的组成
把ATL的窗口类撇开不谈先。我在上一章中提到:窗口类并非任何一种OOP语言中的类——它所包括的并不是通称的属性和方法(在C++中称作成员变量和成员函数),而是属性和响应。现在是解释这句话的时候了。
所谓窗口的属性,无非是窗口的样式(style)、背景画刷(brush)、图标(icon)、光标(cursor)……等元素。你可以从WNDCLASS及WNDCLASSEX中找到它们。需要特别指出的是,窗口的样式事实上包括窗口类的样式和窗口实例的样式,窗口类的样式在注册窗口类之前经由WNDCLASS::style或WNDCLASSEX::style指定,而窗口实例的样式则是在创建窗口(CreateWindow/CreateWindowEx)的时候指定的。
对于窗口的响应,即是指窗口收到某消息后的处理。(在VB、Delphi等RAD环境中,处理窗口的响应亦称作窗口的事件处理。)对于SDK而言,为窗口提供响应也就是为窗口类提供一个回调函数,在回调函数中对我们感兴趣的窗口消息进行特殊处理,譬如上一章中针对WM_DESTROY和WM_PAINT的处理。
另外,我们在进行Win32程序设计的时候,往往还需要对窗口进行操作,譬如ShowWindow和UpdateWindow——姑且让我称之为“方法”。
属性、方法、事件,这回这哥仨算齐了。我们在对窗口进行C++封装时,需要考虑的也正是这三者。自然,依据OO的理念,我们可以很简单地将句柄作为成员变量,将方法作为成员函数,然后将事件经由某种特定的消息分流手段移交给各个成员函数进行响应处理,加之对不同种类的窗口使用继承进行区分——这就是MFC的封装做法。大家如果有兴趣的话,可以打开MFC的afxwin.h看一看CWnd类的代码。
ATL窗口类的活版封装
MFC的CWnd是一个冗长得有些过分的类。究其原因,窗口类的封装理念决定了窗口类的消息分流,而消息分流则决定了类的代码篇幅。如果你已经打开了afxwin.h文件,就可以发现CWnd花了很大的篇幅在“On”开头的事件响应函数上。其实在我们进行Win32程序设计的时候,真正感兴趣的事件没有几个,所以说“万能”势必造就冗长。
另外,考虑MFC的诞生年代,所以对于窗口的封装只是采用了C++的低端特性——例如薄层的封装和单向继承。(题外话:而且MFC中还存在着一些诸如CString、CArray、CList之类的工具,盖因其时STL还未标准化之故。)随着MFC的发展,任凭它做出任何优化,也无法避免当初架构理念带来的效率阴影和偏差。
ATL的诞生年代晚于MFC,使之能够有机会使用C++的高端特性,也就是模板和多重继承。于是,它使用了一种全新的封装理念:将属性、方法、事件分别独立出来,然后利用模板和多重继承的特性将这三者根据需要而组合在一起——打个比方来说,如果MFC的窗口封装是雕版印刷术,那么ATL的窗口封装就是活版印刷术。以上一章的CHelloATLWnd类为例,它的继承层次如下图:
这是一个稍显冗长的继承链,不过我并不打算对它进行详细的解说。在此,我只请你看这个继承层次的最底层和最上层。从最底层来看,CHelloATLWnd继承自CWindowImpl,CWindowImpl有三个模板参数:T、TBase、TWinTraits。再看最上层,CWindowImplRoot继承自TBase和CMessageMap。T参数即是你所继承下来的子类名,通常用于编译期的虚函数机制(后边我会对这一机制进行介绍);TBase参数为对窗口方法和句柄的封装;TWinTraits是窗口样式的类封装;CMessageMap是对窗口事件响应的封装。
下面,就让李马来逐一将这些组成部分介绍给你吧。
窗口样式的封装
窗口样式通常由CWinTraits类封装,这个类很简单,如下:
///////////////////////////////////////////////////////////////////////////// // CWinTraits - Defines various default values for a window
template <DWORD t_dwStyle = 0, DWORD t_dwExStyle = 0> class CWinTraits { public: static DWORD GetWndStyle(DWORD dwStyle) { return dwStyle == 0 ? t_dwStyle : dwStyle; } static DWORD GetWndExStyle(DWORD dwExStyle) { return dwExStyle == 0 ? t_dwExStyle : dwExStyle; } }; |
这个类有两个模板参数:dwStyle和dwExStyle,也就是CreateWindowEx中要用到的那两个样式参数。在CHelloATLWnd::Create(其实也就是CWindowImpl::Create)调用的时候,窗口的样式就是由CWinTraits::GetWndStyle/CWinTraits::GetWndExStyle决定的。
另外,ATL还为常用的窗口样式提供了几个typedef,如CControlWinTraits、CFrameWinTraits、CMDIChildWinTraits。在你需要它们这些特定样式或者需要对它们进行扩展的时候,可以直接进行使用或者使用CWinTraitsOR类来进行进一步的样式组合,这里我就不多介绍了。
窗口方法的封装
说白了,窗口方法的封装其实就是把窗口句柄和常用的窗口操作API函数(也就是那些第一个参数为HWND类型的API函数)进行一层薄薄的绑定。这样做的好处有二:第一,使代码更有逻辑性,符合OO的设计理念;第二,在对SendMessage进行封装后,可以增加对消息参数的类型检查。
CWindow类的内容我就不列出了,因为它同样十分冗长,大家可以参看atlwin.h的相关内容。在这里我仅对其中的几个地方进行解说:
- 它只有一个非static的成员变量,也就是窗口的句柄m_hWnd。这样做的好处是使得CWindow类的对象占用最小的资源,同时给程序员提供最大的自由度。与MFC的CWnd类相比,CWindow的优点体现得尤为明显。CWnd之中还存在着一些MFC Framework要用到的东西,比如RTTI信息等等。此外,MFC内部还会为每个窗口句柄维护一个相对应的CWnd对象,形成一个对象链,这样程序员可以通过GetDlgItem获取CWnd类的指针,但是这同时也为系统增加了很多额外的负担。
- CWindow提供了对operator=操作符的重载,这样程序员可以直接将一个HWND赋给一个CWindow对象。
- CWindow::Attach/CWindow::Detach提供了CWindow对象与HWND的绑定/解除绑定功能。
- CWindow提供了对operator HWND类型转换操作符的重载,这样在用到HWND类型变量的时候,可以直接使用CWindow对象来代替。
有了CWindow类之后,如果你需要对窗口进行更多的操作,就可以对其进行继承,例如CButton、CListBox、CEdit等等。这样一来,代码的复用性就大大提高了。
窗口事件响应的封装
窗口事件响应的封装,也就是这个类如何对窗口消息进行分流。你应该还记得,CHelloATLWnd类是通过BEGIN_MSG_MAP、END_MSG_MAP和MESSAGE_HANDLER宏实现的。如果你参阅了atlwin.h中它们的定义,你就会发现其实它们会组成一个ProcessWindowMessage函数。是的,CMessageMap就是由这个函数组成的:
///////////////////////////////////////////////////////////////////////////// // CMessageMap - abstract class that provides an interface for message maps
class ATL_NO_VTABLE CMessageMap { public: virtual BOOL ProcessWindowMessage(HWND hWnd, UINT uMsg, WPARAM wParam, LPARAM lParam, LRESULT& lResult, DWORD dwMsgMapID) = 0; }; |
CWindowImplRoot派生自CMessageMap,所以CWindowImplRoot及至CWindowImpl都需要实现ProcessWindowMessage以完成窗口消息的分流。大家可以看到,这个函数的前四个参数是在SDK程序设计中窗口回调的原班人马,在此不多介绍。lResult用来接收各消息处理函数的返回值,然后返回给最初的WndProc作为返回值。dwMsgMapID是一个神秘参数,且待李马留到以后再进行讲解。
“等等!”也许你会突然打断我,“——ATL是如何将WndProc封装到类的成员函数中的?”的确,在编译器的处理下,C++类中非static成员函数的参数尾部会被加入一个隐藏的this指针,这就使得它实际与回调函数的规格不合,所以非static成员函数是不能作为Win32的回调函数的。
先看MFC是如何做的吧。它采用一张庞大的消息映射表避开了这个敏感的地方,对此感兴趣的朋友们可参见JJHou先生的《深入浅出MFC》。也正因此,CWnd不得不为大部分消息各实现一个消息处理函数。还好这些消息处理函数不是虚函数,否则CWnd会维护多么庞大的一张虚函数表!
而ATL的奇妙之处也正是在此。它采用了thunk机制,即是在执行真正的WndProc回调之前刷改了内存中的机器码,将HWND参数用本窗口类的this指针替换了,然后在执行真正的代码之前再将这个指针转换回来。这样,就将this指针的矛盾巧妙化解了。由于本书讲解的是关于如何使用ATL进行GUI程序设计方面的内容,所以李马不在此进行过多探讨了就,感兴趣的朋友们可以自己研究atlwin.h中CWindowImplBaseT的代码,或者参考Zeeshan Amjad先生的《ATL Under the Hook Part 5》一文。
在thunk机制的帮助下,ATL的窗口类就可以直接将不感兴趣的消息交由DefWindowProc进行处理,而不用像MFC一样实现那么多消息处理函数。对于我们感兴趣的消息,可以使用ATL中的BEGIN_MSG_MAP/END_MSG_MAP宏来在窗口类的成员函数ProcessWindowMessage中完成。此外对于消息的分流,除了MESSAGE_HANDLER宏,我们还可以使用其它的几个宏进行各种消息(命令消息、普通控件通知消息、公共控件通知消息)的分流,我将在后边专门的一章中对ATL的CMessageMap的使用方法来进行讲解。
组合
葫芦兄弟单打独斗都不是蛇精的对手,所以葫芦山神就会派仙鹤携带七色彩莲找到他们,最后七个葫芦娃合体成为威力无比的葫芦小金刚,消灭了妖精,人世间重获太平……
这自然是一个非常老套的故事,但想必如我一样的80s生人看到后仍然会感慨不已。在那个少儿的精神食粮异常匮乏的年代,这部有些程式化脸谱化的动画片告诉了我们一个简单的道理:只有团结起来,才能发挥最大的力量。
ATL的窗口类也是如此,单凭CWinTraits、CWindow、CMessageMap这哥仨单打独斗是不可能成就大气候的。我们需要做的,就是使用某种方法来将它们组合起来。感谢C++为我们带来的多重继承和模板——多重继承让我们能够将它们组合,模板让我们能够将它们灵活地组合(所谓“灵活地组合”,即是在CWindowImpl层通过填入模板参数来决定继承链的顶层CWindowImplRoot的多重继承情况)。那么,再回到上一章的窗口类CHelloATLWnd:
class CHelloATLWnd : public CWindowImpl< CHelloATLWnd, CWindow, CWinTraits< WS_OVERLAPPEDWINDOW > > { public: CHelloATLWnd() public: DECLARE_WND_CLASS( _T("HelloATL") ) public: BEGIN_MSG_MAP( CHelloATLWnd ) MESSAGE_HANDLER( WM_DESTROY, OnDestroy ) MESSAGE_HANDLER( WM_PAINT, OnPaint ) END_MSG_MAP() public: LRESULT OnDestroy( UINT uMsg, WPARAM wParam, LPARAM lParam, BOOL& hHandled ) { ::PostQuitMessage( 0 ); return 0; } LRESULT OnPaint( UINT uMsg, WPARAM wParam, LPARAM lParam, BOOL& hHandled ) { HDC hdc; PAINTSTRUCT ps;
hdc = BeginPaint( &ps ); DrawText( hdc, _T("Hello, ATL!"), -1, &ps.rcPaint, DT_CENTER | DT_VCENTER | DT_SINGLELINE ); EndPaint( &ps ); return 0; } }; |
不知道你现在再看到这个类是否会少几分生疏?在这里,CWindowImpl就担任了“七色彩莲”的角色——BEGIN_MSG_MAP/END_MSG_MAP是CMessageMap由继承带来的,BeginPaint/EndPaint是CWindow由模板和多重继承带来的,以及控制窗口样式的CWinTraits(在这里要提醒一点,在将CWinTraits作为CWindowImpl的模板参数时,一定要将CWinTraits的模板参数右尖括号与CWindowImpl的模板参数右尖括号用空格分隔开,否则凑在一起的两个右尖括号“>>”将会被编译器判断为右移操作符)是由模板带来的。
当然,我还要回答上一章遗留下来的问题:WNDCLASSEX窗口类是如何注册的?
如果你是前已经偷偷看过CWindowImpl::Create的代码,那么相信这个问题你已经知道答案了。不过我还是要把相关代码列出来:
// from CWindowImpl::Create if (T::GetWndClassInfo().m_lpszOrigName == NULL) T::GetWndClassInfo().m_lpszOrigName = GetWndClassName(); ATOM atom = T::GetWndClassInfo().Register(&m_pfnSuperWindowProc); |
也就是说,窗口类的注册是在窗口创建前完成的。
下面,李马请你注意上面代码中GetWndClassInfo的部分。这个函数是由窗口类的编写者——也就是我们,ATL的GUI开发者——完成的,它的主要功能是用来获取窗口类的属性。在通常的情况下,GetWndClassInfo使用DECLARE_WND_CLASS/DECLARE_WND_CLASS_EX的形式来实现。参看DECLARE_WND_CLASS宏的定义:
#define DECLARE_WND_CLASS(WndClassName) static CWndClassInfo& GetWndClassInfo() { static CWndClassInfo wc = { { sizeof(WNDCLASSEX), CS_HREDRAW | CS_VREDRAW | CS_DBLCLKS, StartWindowProc, 0, 0, NULL, NULL, NULL, (HBRUSH)(COLOR_WINDOW + 1), NULL, WndClassName, NULL }, NULL, NULL, IDC_ARROW, TRUE, 0, _T("") }; return wc; } |
这里已经为要注册的窗口类设置好了绝大多数的常用属性,当然,如果你仍然觉得自己需要更改更多的属性的话,可以像CHelloATLWnd的构造函数里那么做。特别要指出的一点是,ATL对窗口类的光标(cursor)属性是进行特殊处理的,对CWndClassInfo::m_wc.hCursor直接赋值是不行的。
编译期的虚函数机制
ATL的效率远远高于MFC,其中一方面的原因就是它把很多的工作都通过模板来交给编译器了,比如我上文提到的编译期的虚函数机制。这个机制可以避免虚函数带来的一切开销而静态实现虚函数的特性。考虑以下代码:
template < typename T > class Parent { public: void f() void g() { T* pT = (T*)this; pT->f(); } };
class Child1 : public Parent< Child1 > { public: void f() };
class Child2 : public Parent< Child2 > ; |
然后,这样进行调用:
Child1 c1; Child2 c2; c1.g(); // f from Child1. c2.g(); // f from Parent. |
所有的奥秘尽在Parent::g之中,它通过一个类型转换在编译期就决定了调用哪个函数,颇有些多态性的味道。ATL就是借助这样的机制来保证效率的,如果你深入到atlwin.h的源代码之中,肯定会发现更多诸如此类的例子。