现在有一个MFC写的自定义ComboBox打算移植到WTL上,于是根据WTL的书写方法修改了程序,就得到下面的代码:
Class CComboBoxEx : public CComboBox { protected: void OnDrawItem(UINT wParam, LPDRAWITEMSTRUCT lpDrawItemStruct); public: BEGIN_MSG_MAP_EX(CComboBoxEx) MSG_OCM_DRAWITEM(OnDrawItem) END_MSG_MAP() } Class CMainDlg : public CDialogImpl< CMainDlg > { Protected: CComboBoxEx m_cmbEx; Public: BEGIN_DDX_MAP(CPageConfigFont) DDX_CONTROL_HANDLE(IDC_COMBOBOXEX, m_cmbEx); END_DDX_MAP() BEGIN_MSG_MAP_EX(CPageConfigFont) MSG_WM_INITDIALOG(OnInitDialog) REFLECT_NOTIFICATIONS() END_MSG_MAP() } |
如何生成以上代码及代码的含义,原书都有介绍,由于不是本文的重点,不再一一解释。
要说的是,在WTL
7.1中添加了DDX_CONTROL_HANDLE宏,可以用来设置控件,与DDX_CONTROL不同的是,它不要求控件类由CWindowImpl
派生,即不需要包含SubclassWindow()函数,这样我们才可以使用DDX来设置我们从CComboBox派生的类(听上去很有道理,其实却是
在MFC编程习惯带动下错误思维)。
当然,要实现还有一个小小的问题,DDX_CONTROL_HANDLE宏需要我们的类包含一个操作符“=”,怎么写这个函数呢?参看了一下基类的实现方法:
CComboBoxExT< TBase >& operator =(HWND hWnd) { m_hWnd = hWnd; return *this; } |
参看WTL文件<atlctrls.h>
原来只是将m_hWnd赋值,于是我们在我们的类中添加如下的代码:
CComboBoxEx& operator=(HWND hWnd) { m_hWnd = hWnd; return *this; } |
于是编译通过了。(殊不知潜在的错误就这样被深深的埋起来了)
可是为什么DDX_CONTROL_HANDLE宏需要我们的类包含操作符“=”呢?我们来看看DDX_CONTROL_HANDLE宏是怎么实现的:
整
个DDX_MAP其实是定义了一个DoDataExchange函数,BEGIN_DDX_MAP宏定义了函数头,而END_DDX_MAP定义了函数
尾,中间一项项的DDX定义函数的具体内容,而当你在代码中定义DDX_MAP的时候就等于重载了CWinDataExchange::
DoDataExchange()函数,具体代码如下:
#define BEGIN_DDX_MAP(thisClass) \ BOOL DoDataExchange(BOOL bSaveAndValidate = FALSE, UINT nCtlID = (UINT)-1) \ { \ bSaveAndValidate; \ nCtlID; #define END_DDX_MAP() \ return TRUE; \ } |
参看WTL文件<atlddx.h>
对于DDX_CONTROL_HANDLE宏,它其实是调用了CWinDataExchange:: DDX_Control_Handle函数,具体代码如下:
// Simple control attaching (for HWND wrapper controls) template <class TControl> void DDX_Control_Handle(UINT nID, TControl& ctrl, BOOL bSave) { if(!bSave && ctrl.m_hWnd == NULL) { T* pT = static_cast<T*>(this); ctrl = pT->GetDlgItem(nID); } } |
参看WTL文件<atlddx.h>
正如上面的代码,DDX_CONTROL_HANDLE宏是直接将ID所对应的窗体句柄直接赋值给DDX所链接的控件类,于是我们在DDX_MAP中定义的语句与下面的语句是等价的:
m_cmbEx = this->GetDlgItem(IDC_COMBOBOXEX); |
所以要想使上面的语句能够使用,重载操作符就变成了一个解决问题的好办法,这就是DDX_CONTROL_HANDLE宏需要我们的类包含操作符“=”的原因。
到这里,我们已经知道了为什么,也作了应该做的事,移植的工作就剩下测试了。当然如果你熟悉WTL或者仔细看了上面的代码,也许会发现有一个很大的问题潜伏着。可是我们是MFC的程序员,习惯用MFC的方法去思考,于是奇怪的事情在测试的时候发生了。
运行一切正常,只是我们在重画函数中的代码没有运行,换句话说,就是重画事件没有被触发。
为什么?我们的所有代码都是按照正确的方法写成的,在CComboBoxEx的MSG_MAP中添加MSG_OCM_DRAWITEM宏来映射重画事件,在CMainDlg的MSG_MAP中添加REFLECT_NOTIFICATIONS()宏。
该做得都做了。为什么不行呢?
在原书中提到,使用 DEFAULT_REFLECTION_HANDLER来处理缺省的反射事件,难道因为缺少这个宏吗?虽然这不是一个符合逻辑的想法,可是现在也把它拿来当活马医一医了。
于是我们在原来的类中添加这个宏,结果错误出现了,提示没有DefaultReflectionHandler函数的定义,哦?这是什么意思啊?我们来查查原码:
#define DEFAULT_REFLECTION_HANDLER() \ if(DefaultReflectionHandler(hWnd, uMsg, wParam, lParam, lResult)) \ return TRUE; |
参看ATL文件<atlwin.h>
原来DEFAULT_REFLECTION_HANDLER宏只是调用
DefaultReflectionHandler函数,那么这个函数又是何许人也呢?DefaultReflectionHandler是
CWindowImplRoot的成员函数,也可以说是CWindowImpl的成员函数,因为CWindowImpl由CWindowImplBase
派生,而CWindowImplBase由CWindowImplRoot派生,DefaultReflectionHandler函数其实是对API函
数DefWindowProc的封装,不过它只限于处理OCM_的事件。如下面的代码:
template <class TBase> BOOL CWindowImplRoot< TBase >::DefaultReflectionHandler(HWND hWnd, UINT uMsg, WPARAM wParam, LPARAM lParam, LRESULT& lResult) { switch(uMsg) { case OCM_COMMAND: case OCM_NOTIFY: case OCM_PARENTNOTIFY: case OCM_DRAWITEM: case OCM_MEASUREITEM: case OCM_COMPAREITEM: case OCM_DELETEITEM: case OCM_VKEYTOITEM: case OCM_CHARTOITEM: case OCM_HSCROLL: case OCM_VSCROLL: case OCM_CTLCOLORBTN: case OCM_CTLCOLORDLG: case OCM_CTLCOLOREDIT: case OCM_CTLCOLORLISTBOX: case OCM_CTLCOLORMSGBOX: case OCM_CTLCOLORSCROLLBAR: case OCM_CTLCOLORSTATIC: lResult = ::DefWindowProc(hWnd, uMsg - OCM__BASE, wParam, lParam); return TRUE; default: break; } return FALSE; } |
看到这里,如果想添加DEFAULT_REFLECTION_HANDLER宏,控件类就要由CWindowImpl派生。为了测试把死马当活马医的想法,我们把类的定义改为如下这样:
class CComboBoxEx:public CWindowImpl< CComboBoxEx, CComboBox> |
于是,添加DEFAULT_REFLECTION_HANDLER宏得操作通过了编译,但是事实证明,不合逻辑的想法很难带来正确的结果,不仅重画事件没有被触发,修改后,在控件类析构时碰到了ATL的断言。
错误提示是,类在窗体句柄销毁之前被析构。
这个错误到让我们想到原书中提到的一个WTL特性,WTL不会自动销毁窗体句柄,需要自己手工Detach()窗体句柄。既然这样,我们又添加了下面的代码:
~CComboBoxEx() { Detach(); } |
虽然,没有Attach()的Detach()感觉有点怪,可是毕竟ATL的断言不会出现了。但是,问题并没有解决,重画事件还是没有被触发。难道是CMainDlg没有反射事件回来?看看用来反射事件的REFLECT_NOTIFICATIONS宏的代码:
#define REFLECT_NOTIFICATIONS() \ { \ bHandled = TRUE; \ lResult = ReflectNotifications(uMsg, wParam, lParam, bHandled); \ if(bHandled) \ return TRUE; \ } |
参看ATL文件<atlwin.h>
REFLECT_NOTIFICATIONS宏调用的是函数CWindowImplRoot::ReflectNotifications。这个函数通过参数取得发送事件控件的窗体句柄,并通过该句柄将事件发还给控件,代码如下:
template <class TBase> LRESULT CWindowImplRoot< TBase >::ReflectNotifications(UINT uMsg, WPARAM wParam, LPARAM lParam, BOOL& bHandled) { HWND hWndChild = NULL; switch(uMsg) { case WM_COMMAND: if(lParam != NULL) // not from a menu hWndChild = (HWND)lParam; break; case WM_NOTIFY: hWndChild = ((LPNMHDR)lParam)->hwndFrom; break; case WM_PARENTNOTIFY: switch(LOWORD(wParam)) { case WM_CREATE: case WM_DESTROY: hWndChild = (HWND)lParam; break; default: hWndChild = GetDlgItem(HIWORD(wParam)); break; } break; case WM_DRAWITEM: if(wParam) // not from a menu hWndChild = ((LPDRAWITEMSTRUCT)lParam)->hwndItem; break; case WM_MEASUREITEM: if(wParam) // not from a menu hWndChild = GetDlgItem(((LPMEASUREITEMSTRUCT)lParam)->CtlID); break; case WM_COMPAREITEM: if(wParam) // not from a menu hWndChild = GetDlgItem(((LPCOMPAREITEMSTRUCT)lParam)->CtlID); break; case WM_DELETEITEM: if(wParam) // not from a menu hWndChild = GetDlgItem(((LPDELETEITEMSTRUCT)lParam)->CtlID); break; case WM_VKEYTOITEM: case WM_CHARTOITEM: case WM_HSCROLL: case WM_VSCROLL: hWndChild = (HWND)lParam; break; case WM_CTLCOLORBTN: case WM_CTLCOLORDLG: case WM_CTLCOLOREDIT: case WM_CTLCOLORLISTBOX: case WM_CTLCOLORMSGBOX: case WM_CTLCOLORSCROLLBAR: case WM_CTLCOLORSTATIC: hWndChild = (HWND)lParam; break; default: break; } if(hWndChild == NULL) { bHandled = FALSE; return 1; } ATLASSERT(::IsWindow(hWndChild)); return ::SendMessage(hWndChild, OCM__BASE + uMsg, wParam, lParam); } |
我 们感兴趣的是最后一句,控件接收到的是ID = OCM__BASE + WM_DRAWITEM的消息,那么我们可以让控件直接接收消息(OCM__BASE + WM_DRAWITEM),用于取代使用不起作用的MSG_OCM_DRAWITEM。于是有了下面的代码:
MESSAGE_HANDLER_EX(OCM__BASE + WM_DRAWITEM, OnDrawItem)
但是结果还是一样 - 重画事件没有被触发。
幸 亏我们有了新的发现,否则有可能就没由信心解决这个问题了。我们在CMainDlg中添加了WM_DRAWITEM事件,结果捕抓到了 CComboBoxEx的重画事件,这说明CComBoxEx的重画事件发出了,但不知什么原因没有反射回控件。于是我们在CMainDlg:: OnDrawItem()中添加了
SendMessage(m_cmbEx.m_hWnd, OCM__BASE + WM_DRAWITEM, 0, 0)
以取代REFLECT_NOTIFICATIONS宏所做的自动反射,结果发现,事件还是没有收到。难道WTL事件处理出了问题?我们又为CComboBoxEx添加了非反射的事件WM_PAINT,结果发现WM_PAINT事件也没有被触发!!!
CComboBoxEx根本无法收到任何事件!!!!!
我们重新回到起点,来看看那里出了错。
仔细地研读代码以后发现,事件是怎么传递到MSG_MAP的呢?难道我们通过赋值将一个窗体句柄传进来,我们在这个类中定义的MSG_MAP就能自动的连接到这个句柄上吗?这显然是真的不可能。
那么没有将MSG_MAP连接到窗体句柄很可能是控件类无法收到任何事件的原因。那么如何将MSG_MAP连接到窗体句柄上呢?原书中提到一个重要的函数,CWindowImpl::SubclassWindow()。我们再次更改我们的控件类:
CComboBoxEx& operator =(HWND hWnd) { CWindowImpl< CComboBoxEx, CComboBox>::SubclassWindow(hWnd); return *this; } |
一测之下,大吃一惊。不仅重画事件被正确触发,连析构函数中的没有Attach的Detach这个怪用法也可以删除了。为什么会这样呢?探究这个问 题之前,让我们先看看原书使用的DDX_CONTROL宏 - 它只针对CWindowImpl的派生类起作用 - 是怎么回事。原码如下:
#define DDX_CONTROL(nID, obj) \ if(nCtlID == (UINT)-1 || nCtlID == nID) \ DDX_Control(nID, obj, bSaveAndValidate); // Full control subclassing (for CWindowImpl derived controls) template <class TControl> void DDX_Control(UINT nID, TControl& ctrl, BOOL bSave) { if(!bSave && ctrl.m_hWnd == NULL) { T* pT = static_cast<T*>(this); ctrl.SubclassWindow(pT->GetDlgItem(nID)); } } |
从原码可以看到,DDX_CONTROL宏和DDX_CONTROL_HANDLER宏实现的区别只是,前者使用SubclassWindow,而
后者使用操作符“=”。如果把我们上面的代码联系起来,在操作符“=”的处理函数中调用SubclassWindow,其实就等于是明着使用
DDX_CONTROL_HANDLER宏,暗地里却把DDX_CONTROL宏实现了。原来想出门,结果先绕着后院跑了3圈,这真是一个大笑话。
为什么会这样呢?不使用DDX_CONTROL宏是因为CComboBox没有SubclassWindow函数,而是用CComboBox是因为
在MFC中CComboBoxEx就是从CComboBox派生,移植的时候当然倾向于选择同名的类,而不是CWindowImpl<
CComboBoxEx, CComboBox>这样怪怪的声明方法。
可是这里忽视了一个基本的WTL特性,由于WTL基于ATL,而设计ATL就是为了将接口和实现分开,所以在WTL中所有不带Impl字样的类都不 是实现类,像CWindow,CButton,CComboBox等等,他们只是包含一个句柄,没有自己的事件,他们只是负责中转,封装控件事件等等。像 CComboBox的操作符“=”就只是一个赋值语句而已。而DDX_CONTROL_HANDLER正是为这些类服务的,当然如果我们注意到这个宏得注 释,也许早就发现这个问题了,还记得吗?在这里重温一下吧:
// Full control subclassing (for CWindowImpl derived controls) template <class TControl> void DDX_Control(UINT nID, TControl& ctrl, BOOL bSave) // Simple control attaching (for HWND wrapper controls) template <class TControl> void DDX_Control_Handle(UINT nID, TControl& ctrl, BOOL bSave) |
好了,在环游地球一周以后,我们又回到了起点,虽然费了不少的力气,但也搞清楚不少的东西,下面大概地总结一下:
- WTL的类包含接口类(只包含窗体句柄和事件的封装)和实现类(可以拥有自己的事件),要根据具体情况有选择的使用。
- WTL不会自动销毁窗体句柄(当然是指接口类),所以Attach操作以后要记着Detach
- 注意包含有HANDLE的宏,类,函数,它们往往是接口类或为接口类服务的,如上面所说的DDX_Control_Handle,以及CDCHandle等等。
- DDX是通过宏定义重载CWinDataExchange::DoDataExchange()函数实现的
- 消息反射是在取道发送消息的窗体句柄后,通过像它回发相应的消息来实现的。
- 当你想说这不可能的时候,往往是你在调用的方法上出现了错误。
- 多看看代码,你会了解得更多