第三章 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类封装,这个类很简单,如下:

C++代码
  1. /////////////////////////////////////////////////////////////////////////////
  2. // CWinTraits – Defines various default values for a window
  3. template <DWORD t_dwStyle = 0, DWORD t_dwExStyle = 0>
  4. class CWinTraits
  5. {
  6. public:
  7. static DWORD GetWndStyle(DWORD dwStyle)
  8. {
  9. return dwStyle == 0 ? t_dwStyle : dwStyle;
  10. }
  11. static DWORD GetWndExStyle(DWORD dwExStyle)
  12. {
  13. return dwExStyle == 0 ? t_dwExStyle : dwExStyle;
  14. }
  15. };

这个类有两个模板参数: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就是由这个函数组成的:

C++代码
  1. /////////////////////////////////////////////////////////////////////////////
  2. // CMessageMap – abstract class that provides an interface for message maps
  3. class ATL_NO_VTABLE CMessageMap
  4. {
  5. public:
  6. virtual BOOL ProcessWindowMessage(HWND hWnd, UINT uMsg, WPARAM wParam, LPARAM lParam,
  7. LRESULT& lResult, DWORD dwMsgMapID) = 0;
  8. };

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:

C++代码
  1. class CHelloATLWnd : public CWindowImpl< CHelloATLWnd, CWindow, CWinTraits< WS_OVERLAPPEDWINDOW > >
  2. {
  3. public:
  4. CHelloATLWnd()
  5. {
  6. CWndClassInfo& wci     = GetWndClassInfo();
  7. wci.m_bSystemCursor    = TRUE;
  8. wci.m_lpszCursorID     = IDC_ARROW;
  9. wci.m_wc.hbrBackground = (HBRUSH)GetStockObject( WHITE_BRUSH );
  10. wci.m_wc.hIcon         = LoadIcon( NULL, IDI_APPLICATION );
  11. }
  12. public:
  13. DECLARE_WND_CLASS( _T("HelloATL") )
  14. public:
  15. BEGIN_MSG_MAP( CHelloATLWnd )
  16. MESSAGE_HANDLER( WM_DESTROY, OnDestroy )
  17. MESSAGE_HANDLER( WM_PAINT, OnPaint )
  18. END_MSG_MAP()
  19. public:
  20. LRESULT OnDestroy( UINT uMsg, WPARAM wParam, LPARAM lParam, BOOL& hHandled )
  21. {
  22. ::PostQuitMessage( 0 );
  23. return 0;
  24. }
  25. LRESULT OnPaint( UINT uMsg, WPARAM wParam, LPARAM lParam, BOOL& hHandled )
  26. {
  27. HDC hdc;
  28. PAINTSTRUCT ps;
  29. hdc = BeginPaint( &ps );
  30. DrawText( hdc, _T("Hello, ATL!"), -1, &ps.rcPaint, DT_CENTER | DT_VCENTER | DT_SINGLELINE );
  31. EndPaint( &ps );
  32. return 0;
  33. }
  34. };

不知道你现在再看到这个类是否会少几分生疏?在这里,CWindowImpl就担任了“七色彩莲”的角色——BEGIN_MSG_MAP/END_MSG_MAP是CMessageMap由继承带来的,BeginPaint/EndPaint是CWindow由模板和多重继承带来的,以及控制窗口样式的CWinTraits(在这里要提醒一点,在将CWinTraits作为CWindowImpl的模板参数时,一定要将CWinTraits的模板参数右尖括号与CWindowImpl的模板参数右尖括号用空格分隔开,否则凑在一起的两个右尖括号“>>”将会被编译器判断为右移操作符)是由模板带来的。

当然,我还要回答上一章遗留下来的问题:WNDCLASSEX窗口类是如何注册的?

如果你是前已经偷偷看过CWindowImpl::Create的代码,那么相信这个问题你已经知道答案了。不过我还是要把相关代码列出来:

C++代码
  1. // from CWindowImpl::Create
  2. if (T::GetWndClassInfo().m_lpszOrigName == NULL)
  3. T::GetWndClassInfo().m_lpszOrigName = GetWndClassName();
  4. ATOM atom = T::GetWndClassInfo().Register(&m_pfnSuperWindowProc);

也就是说,窗口类的注册是在窗口创建前完成的。

下面,李马请你注意上面代码中GetWndClassInfo的部分。这个函数是由窗口类的编写者——也就是我们,ATL的GUI开发者——完成的,它的主要功能是用来获取窗口类的属性。在通常的情况下,GetWndClassInfo使用DECLARE_WND_CLASS/DECLARE_WND_CLASS_EX的形式来实现。参看DECLARE_WND_CLASS宏的定义:

C++代码
  1. #define DECLARE_WND_CLASS(WndClassName)
  2. static CWndClassInfo& GetWndClassInfo()
  3. {
  4. static CWndClassInfo wc =
  5. {
  6. { sizeof(WNDCLASSEX), CS_HREDRAW | CS_VREDRAW | CS_DBLCLKS, StartWindowProc,
  7. 0, 0, NULL, NULL, NULL, (HBRUSH)(COLOR_WINDOW + 1), NULL, WndClassName, NULL },
  8. NULL, NULL, IDC_ARROW, TRUE, 0, _T("")
  9. };
  10. return wc;
  11. }

这里已经为要注册的窗口类设置好了绝大多数的常用属性,当然,如果你仍然觉得自己需要更改更多的属性的话,可以像CHelloATLWnd的构造函数里那么做。特别要指出的一点是,ATL对窗口类的光标(cursor)属性是进行特殊处理的,对CWndClassInfo::m_wc.hCursor直接赋值是不行的。

编译期的虚函数机制

ATL的效率远远高于MFC,其中一方面的原因就是它把很多的工作都通过模板来交给编译器了,比如我上文提到的编译期的虚函数机制。这个机制可以避免虚函数带来的一切开销而静态实现虚函数的特性。考虑以下代码:

C++代码
  1. template < typename T >
  2. class Parent
  3. {
  4. public:
  5. void f()
  6. {
  7. cout << "f from Parent." << endl;
  8. }
  9. void g()
  10. {
  11. T* pT = (T*)this;
  12. pT->f();
  13. }
  14. };
  15. class Child1 : public Parent< Child1 >
  16. {
  17. public:
  18. void f()
  19. {
  20. cout << "f from Child1." << endl;
  21. }
  22. };
  23. class Child2 : public Parent< Child2 >
  24. {
  25. };

然后,这样进行调用:

C++代码
  1. Child1 c1;
  2. Child2 c2;
  3. c1.g(); // f from Child1.
  4. c2.g(); // f from Parent.

所有的奥秘尽在Parent::g之中,它通过一个类型转换在编译期就决定了调用哪个函数,颇有些多态性的味道。ATL就是借助这样的机制来保证效率的,如果你深入到atlwin.h的源代码之中,肯定会发现更多诸如此类的例子。

ATL的GUI程序设计(3)的更多相关文章

  1. ATL的GUI程序设计(4)

    第四章 对话框和控件 对于Win32 GUI的程序设计来说,其实大部分的情况下我们都不需要自己进行窗口类的设计,而是可以使用Win32中与用户交互的标准方式--对话框(Dialog Box).我们可以 ...

  2. ATL的GUI程序设计(2)

    from:http://blog.titilima.com/atlgui-2.html 第二章 一个最简单窗口程序的转型 我知道,可能会有很多朋友对上一章的"Hello, World!&qu ...

  3. ATL的GUI程序设计(前言)

    前言 也许,你是一个顽固的SDK簇拥者: 也许,你对MFC抱着无比排斥的态度,甚至像我一样对它几乎一无所知: 也许,你符合上面两条,而且正在寻求着一种出路: 也许,你找到了一条出路--WTL,但是仍然 ...

  4. ATL的GUI程序设计(1)

    from:http://blog.titilima.com/atlgui-1.html 第一章 不能免俗的"Hello, World!" 在这一章里,就像所有的入门级教程一样,我也 ...

  5. Java GUI程序设计

    在实际应用中,我们见到的许多应用界面都属于GUI图形型用户界面.如:我们点击QQ图标,就会弹出一个QQ登陆界面的对话框.这个QQ图标就可以被称作图形化的用户界面. 其实,用户界面的类型分为两类:Com ...

  6. GUI程序设计2

    8. 按钮(JButton)使用示例 例14. 按钮使用示例. package GUI; import java.awt.BorderLayout; import java.awt.Container ...

  7. GUI程序设计

    1. 对话框(JDialog)使用示例 例1. JDialog简单使用示例. import javax.swing.JLabel; public class demoJDialog { JFrame ...

  8. Matlab GUI程序设计入门——信号发生器+时域分析

    背景:学习matlab gui编程入门,完成一个基于GUIDE的图形化界面程序,结合信号生成及分析等. 操作步骤: 1.新建程序 新建一个GUIDE程序 这里选择第一个选项,即创建一个空白的GUIDE ...

  9. MATLAB GUI程序设计中ListBox控件在运行期间消失的原因及解决方法

    在运行期间,ListBox控件突然消失,同时给出如下错误提示: Warning: single-selection listbox control requires that Value be an ...

随机推荐

  1. SingletonPattern(单例模式)-----Java/.Net

    单例模式(Singleton Pattern)是 Java 中最简单的设计模式之一. 这种类型的设计模式属于创建型模式,它提供了一种创建对象的最佳方式. 这种模式涉及到一个单一的类,该类负责创建自己的 ...

  2. Django进阶一

    目录 表关系创建 django请求生命周期流程 路由层 无名分组 有名分组 反向解析 路由分发 名称空间 虚拟环境 django版本区别 伪静态 视图层 三板斧 JsonResponse前后端交互数据 ...

  3. iscsi,nfs

    存储概述 存储的目标 存储是根据不同的应用环境通过采取合理.安全.有效的方式将数据保存到某些介质上并能保证有效的访问. 一方面它是数据临时或长期驻留的物理媒介. 另一方面,它是保证数据完整安全存放的方 ...

  4. 22.Python安装和卸载第三方模块方法

    安装和卸载第三方开源模块的步骤:下例,安装urllib3模块的步骤. 1.安装开源模块步骤: 按键盘windows键+r键,输出cmd回车.或开始->windows系统->命令提示符: 输 ...

  5. bootstrap:按钮下拉菜单

    <!DOCTYPE html> <html> <head> <meta charset="UTF-8"> <meta name ...

  6. 类加载器在Tomcat中的应用

    之前有文章已经介绍过了JVM中的类加载机制,JVM中通过类加载加载class文件,通过双亲委派模型完成分层加载.实际上类加载机制并不仅仅是在JVM中得以运用,通过影响字节码生成和类加载器目前已经有了许 ...

  7. 【转】小波与小波包、小波包分解与信号重构、小波包能量特征提取 暨 小波包分解后实现按频率大小分布重新排列(Matlab 程序详解)

    转:https://blog.csdn.net/cqfdcw/article/details/84995904 小波与小波包.小波包分解与信号重构.小波包能量特征提取   (Matlab 程序详解) ...

  8. 【原理】从零编写ILI9341驱动全过程(基于Arduino)

    最近在淘宝入手了一块ILI9341彩色屏幕,支持320x240分辨率.之前一直很好奇这类单片机驱动的彩色屏幕的原理,就打算自己写一个驱动,从电流层面操控ILI9341屏幕.话不多说,我们开始吧( ̄▽ ̄ ...

  9. mysql的简单命令

    MySQL的命令介绍:   连接数据库服务器命令: mysql -u 用户名 -p 密码   mysql是连接MySQL数据库的命令 -u表示后跟用户名 -p 后跟密码   如果登录后展示 " ...

  10. Java HashSet集合的子类LinkedHashSet集合

    说明 HashSet保证元素的唯一性,可是元素存放进去是没有顺序的. 在HashSet下面有一个子类java.util.LinkedHashSet,它是 链表 + 哈希表(数组+链表 或者 数组+红黑 ...