这是一个稍显冗长的继承链,不过我并不打算对它进行详细的解说。在此,我只请你看这个继承层次的最底层和最上层。从最底层来看,CHelloATLWnd继承自CWindowImpl,CWindowImpl有三个模板参数:T、TBase、TWinTraits。再看最上层,CWindowImplRoot继承自TBase和CMessageMap。T参数即是你所继承下来的子类名,通常用于编译期的虚函数机制(后边我会对这一机制进行介绍);TBase参数为对窗口方法和句柄的封装;TWinTraits是窗口样式的类封装;CMessageMap是对窗口事件响应的封装。
下面,就让李马来逐一将这些组成部分介绍给你吧。
窗口样式的封装
窗口样式通常由CWinTraits类封装,这个类很简单,如下:
/////////////////////////////////////////////////////////////////////////////// CWinTraits - Defines various default values for a windowtemplate DWORD t_dwStyle = 0, DWORD t_dwExStyle = 0class 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 mapsclass 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的使用方法来进行讲解。
组合
葫芦兄弟单打独斗都不是蛇精的对手,所以葫芦山神就会派仙鹤携带七色彩莲找到他们,最后七个葫芦娃合体成为威力无比的葫芦小金刚,消灭了妖精,人世间重获太平
(本文来源于图老师网站,更多请访问http://m.tulaoshi.com/bianchengyuyan/)这自然是一个非常老套的故事,但想必如我一样的80s生人看到后仍然会感慨不已。在那个少儿的精神食粮异常匮乏的年代,这部有些程式化脸谱化的动画片告诉了我们一个简单的道理:只有团结起来,才能发挥最大的力量。
ATL的窗口类也是如此,单凭CWinTraits、CWindow、CMessageMap这哥仨单打独斗是不可能成就大气候的。我们需要做的,就是使用某种方法来将它们组合起来。感谢C++为我们带来的多重继承和模板多重继承让我们能够将它们组合,模板让我们能够将它们灵活地组合(所谓灵活地组合,即是在CWindowImpl层通过填入模板参数来决定继承链的顶层CWindowImplRoot的多重继承情况)。那么,再回到上一章的窗口类CHelloATLWnd:
class CHelloATLWnd : public CWindowImpl CHelloATLWnd, CWindow, CWinTraits WS_OVERLAPPEDWINDOW {public: CHelloATLWnd() { CWndClassInfo& wci = GetWndClassInfo(); wci.m_bSystemCursor = TRUE; wci.m_lpszCursorID = IDC_ARROW; wci.m_wc.hbrBackground = (HBRUSH)GetStockObject( WHITE_BRUSH ); wci.m_wc.hIcon = LoadIcon( NULL, IDI_APPLICATION ); }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窗口类是如何注册的?
(本文来源于图老师网站,更多请访问http://m.tulaoshi.com/bianchengyuyan/)如果你是前已经偷偷看过CWindowImpl::Create的代码,那么相信这个问题你已经知道答案了。不过我还是要把相关代码列出来:
// from CWindowImpl::Createif (T::GetWndClassInfo().m_lpszOrigName == NULL) T::GetWndClassInfo().m_lpszOrigName = GetWndClassName();ATOM atom = T::GetWndClassInfo().Register(&m_pfnSuperWindowProc);
也就是说,窗口类的注册是在窗口创建前完成的。