本文主要根据侯捷《深入浅出MFC》整理而成,主要讲述MFC消息映射与传递机制。
一 如何形成消息映射网 1 在源文件加入实现消息映射表格代码首先你必须在头文件中(.H)声明消息映射表格
class CScribbleDoc : public Cdocument{ ... DECLARE_MESSAGE_MAP()};
然后在实现文件中(.CPP)实现此表格:
BEGIN_MESSAGE_MAP(CScribbleDoc, Cdocument) //{{AFX_MSG_MAP(CScribbleDoc) ON_COMMAND(ID_EDIT_CLEAR_ALL, OnEditClearAll) ON_COMMAND(ID_PEN_THICK_OR_THIN, OnPenThickOrThin) ... //}}AFX_MSG_MAPEND_MESSAGE_MAP()
这其中出现三个宏。第一个宏BEGIN_MESSAGE_MAP 有两个参数,分别是拥有此消息映射表之类,及其父类。第二个宏是ON_COMMAND,指定消息处理函数名称。第三个宏END_MESSAGE_MAP 作为结尾记号。至于夹在BEGIN_ 和END_ 之中奇奇怪怪的说明符号//}} 和//{{,是ClassWizard 产生的,也是用来给它自己看的。
2 消息映射表格的构建 消息映射的本质其实是一个巨大的数据结构,用来为诸如WM_PAINT 这样的标准消息决定流动路线,使它得以流到父类去;也用来为WM_COMMAND 这个特殊消息决定流动路线,使它能够七拐八弯地流到类继承结构的旁支去。我们看看在头文件和源文件中这些宏的定义是什么。
首先,头文件中的DECLARE_MESSAGE_MAP宏定义为:
#define DECLARE_MESSAGE_MAP() protected: static const AFX_MSGMAP* PASCAL GetThisMessageMap(); virtual const AFX_MSGMAP* GetMessageMap() const;
然后,在实现文件中的宏定义为:
#define BEGIN_MESSAGE_MAP(theClass, baseClass) const AFX_MSGMAP* theClass::GetMessageMap() const { return GetThisMessageMap(); } const AFX_MSGMAP* PASCAL theClass::GetThisMessageMap() { typedef theClass ThisClass;typedef baseClass ThebaseClass;static const AFX_MSGMAP_ENTRY _messageEntries[] = {#define END_MESSAGE_MAP() {0, 0, 0, 0, AfxSig_end, (AFX_PMSG)0 } }; static const AFX_MSGMAP messageMap = { &ThebaseClass::GetThisMessageMap, &_messageEntries[0] };return &messageMap; }
其中AFX_MSGMAP结构表示本类和基类的消息映射表地址信息,定义如下:
struct AFX_MSGMAP{ const AFX_MSGMAP* pbaseMap;//基类的消息处理映射表地址 const AFX_MSGMAP_ENTRY* lpEntries;//本类的消息映射表地址,是个数组};
AFX_MSGMAP_ENTRY(代表消息映射表的每个条目)是这样的形式
struct AFX_MSGMAP_ENTRY{UINT nMessage; // windows messageUINT nCode; // control code or WM_NOTIFY codeUINT nID; // control ID (or 0 for windows messages)UINT nLastID; // used for entries specifying a range of control id'sUINT_PTR nSig; // signature type (action) or pointer to message #AFX_PMSG pfn; // routine to call (or special value)};
任何一个ON_宏会把这六个项目初始化起来。例如:
#define ON_COMMAND(id, memberFxn) { WM_COMMAND, CN_COMMAND, (WORD)id, (WORD)id, AfxSig_vv, (AFX_PMSG)memberFxn },#define ON_WM_CREATE() { WM_CREATE, 0, 0, 0, AfxSig_is, (AFX_PMSG)(AFX_PMSGW)(int (AFX_MSG_CALL CWnd::*)(LPCREATESTRUCT))onCreate },#define ON_WM_DESTROY() { WM_DESTROY, 0, 0, 0, AfxSig_vv, (AFX_PMSG)(AFX_PMSGW)(void (AFX_MSG_CALL CWnd::*)(void))onDestroy },#define ON_WM_MOVE() { WM_MOVE, 0, 0, 0, AfxSig_vvii, (AFX_PMSG)(AFX_PMSGW)(void (AFX_MSG_CALL CWnd::*)(int, int))onMove },#define ON_WM_SIZE() { WM_SIZE, 0, 0, 0, AfxSig_vwii, (AFX_PMSG)(AFX_PMSGW)(void (AFX_MSG_CALL CWnd::*)(UINT, int, int))onSize },#define ON_WM_ACTIVATE() { WM_ACTIVATE, 0, 0, 0, AfxSig_vwWb, (AFX_PMSG)(AFX_PMSGW)(void (AFX_MSG_CALL CWnd::*)(UINT, CWnd*,BOOL))onActivate },#define ON_WM_SETFOCUS() { WM_SETFOCUS, 0, 0, 0, AfxSig_vW, (AFX_PMSG)(AFX_PMSGW)(void (AFX_MSG_CALL CWnd::*)(CWnd*))onSetFocus },#define ON_WM_PAINT() { WM_PAINT, 0, 0, 0, AfxSig_vv, (AFX_PMSG)(AFX_PMSGW)(void (AFX_MSG_CALL CWnd::*)(void))onPaint },#define ON_WM_CLOSE() { WM_CLOSE, 0, 0, 0, AfxSig_vv, (AFX_PMSG)(AFX_PMSGW)(void (AFX_MSG_CALL CWnd::*)(void))onClose },...
从以上宏的定义可以看出,在定义消息映射表的时候,实际上在类中定义了一个静态的数组,这个数组中每一项是AFX_MSGMAP_ENTRY类型的数据,而AFX_MSGMAP_ENTRY包含了不同消息的消息号、ID值、通知消息代码、签名记号以及消息处理函数;
另外,通过GetMessageMap方法,可以获取AFX_MSGMAP结构,而这个结构体可以获取本类以及基类的消息映射表地址。
至此,我们的消息映射网初步形成,可以想见,我们新建每一个窗口类,都可以有自己的消息映射表格,也可以找到基类的消息映射表格,如此一直上溯,肯定可以找到默认消息处理函数。
我们的消息映射网已经搭建好,那么窗口如何根据消息选择相应的处理函数呢?
首先要说,窗口接收到消息后,操作系统通过回调函数,最终调用的是窗口的CWnd::OnWndMsg函数。该函数实现如下:
BOOL CWnd::OnWndMsg(UINT message, WPARAM wParam, LPARAM lParam, LRESULT* pResult){LRESULT lResult = 0;union MessageMapFunctions mmf;mmf.pfn = 0;CInternalGlobalLock winMsgLock;// Windows消息单独处理if (message == WM_COMMAND){if (OnCommand(wParam, lParam)){lResult = 1;goto LReturnTrue;}return FALSE;}........// 通告消息单独处理if (message == WM_NOTIFY){NMHDR* pNMHDR = (NMHDR*)lParam;if (pNMHDR->hwndFrom != NULL && OnNotify(wParam, lParam, &lResult))goto LReturnTrue;return FALSE;}........//下面这些就是标准的Windows消息const AFX_MSGMAP* pMessageMap; pMessageMap = GetMessageMap();//找到最底层子类的消息映射表UINT iHash; iHash = (LOWORD((DWORD_PTR)pMessageMap) ^ message) & (iHashMax-1);winMsgLock.Lock(CRIT_WINMSGCACHE);AFX_MSG_CACHE* pMsgCache; pMsgCache = &_afxMsgCache[iHash];const AFX_MSGMAP_ENTRY* lpEntry;if (........) //检查是否在cache之中{........}else{pMsgCache->nMsg = message;pMsgCache->pMessageMap = pMessageMap;//以下循环请仔细查看,其代码逻辑就是直线上溯,一层一层的找详细映射表,//直到找到为止。for (; pMessageMap->pfnGetbaseMap != NULL;pMessageMap = (*pMessageMap->pfnGetbaseMap)()){if (message < 0xC000){// 开始查找符合条件的消息if ((lpEntry = AfxFindMessageEntry(pMessageMap->lpEntries,message, 0, 0)) != NULL){pMsgCache->lpEntry = lpEntry;winMsgLock.Unlock();goto LDispatch;}}else{// registered windows messagelpEntry = pMessageMap->lpEntries;while ((lpEntry = AfxFindMessageEntry(lpEntry, 0xC000, 0, 0)) != NULL){UINT* pnID = (UINT*)(lpEntry->nSig);ASSERT(*pnID >= 0xC000 || *pnID == 0);// must be successfully registeredif (*pnID == message){pMsgCache->lpEntry = lpEntry;winMsgLock.Unlock();goto LDispatchRegistered;}lpEntry++; // keep looking past this one}}}pMsgCache->lpEntry = NULL;winMsgLock.Unlock();return FALSE;}LDispatch:mmf.pfn = lpEntry->pfn;//这个神秘的联合体待会详解,你只需知道这个变量代表一个函数指针switch (lpEntry->nSig)//通过AFX_MSGMAP_ENTRY条目中的nSig变量,确定真正的函数形式{default:ASSERT(FALSE);break;case AfxSig_l_p:{CPoint point(lParam);lResult = (this->*mmf.pfn_l_p)(point);break;}case AfxSig_b_D_v:lResult = (this->*mmf.pfn_b_D)(CDC::FromHandle(reinterpret_cast
通过该函数可以看出,如果是Windows消息的话,采取的是直线上溯的方式,一一比较消息和消息映射表中的条目是否相符。查找函数的实现函数为AfxFindMessageEntry,这个函数其实就是迭代本类中的消息映射表,尝试找到nMessage、nCode、nID都相同的条目。如果在这个子类中没有找到符合的条目,就到这个上一层父类中去找,直到找到为止。
那么,找到会怎么做?怎么调用相关的响应函数?这里的关键是AFX_MSGMAP_ENTRY条目中的nSig变量。
nSig变量可能的值如下:
enum AfxSig{ AfxSig_end = 0, // [marks end of message map] AfxSig_bD, // BOOL (CDC*) AfxSig_bb, // BOOL (BOOL) AfxSig_bWww, // BOOL (CWnd*, UINT, UINT) AfxSig_hDWw, // HBRUSH (CDC*, CWnd*, UINT) AfxSig_hDw, // HBRUSH (CDC*, UINT) AfxSig_iwWw, // int (UINT, CWnd*, UINT) AfxSig_iww, // int (UINT, UINT) AfxSig_iWww, // int (CWnd*, UINT, UINT) AfxSig_is, // int (LPTSTR) AfxSig_lwl, // LRESULT (WPARAM, LPARAM) AfxSig_lwwM, // LRESULT (UINT, UINT, CMenu*) AfxSig_vv, // void (void) AfxSig_vw, // void (UINT) AfxSig_vww, // void (UINT, UINT) AfxSig_vvii, // void (int, int) // wParam is ignored AfxSig_vwww, // void (UINT, UINT, UINT) AfxSig_vwii, // void (UINT, int, int) AfxSig_vwl, // void (UINT, LPARAM) AfxSig_vbWW, // void (BOOL, CWnd*, CWnd*)..、...};
AfxSig枚举类型最后几位代表函数类型,例如 AfxSig_bWww代表函数返回值为bool型,函数参数依次为CWnd*, UINT和 UINT。
程序通过查看AFX_MSGMAP_ENTRY条目中的nSig变量值,就可以知道条目中消息响应函数的函数类型。刚刚我们说过了,消息响应函数表中所存储的都是AFX_PMSG 类型的函数指针,怎么转为正确的函数指针呢?
MFC将所有可能的响应函数形式定义为了一个联合体变量。
union MessageMapFunctions{ AFX_PMSG pfn; // generic member function pointer // specific type safe variants BOOL (AFX_MSG_CALL CWnd::*pfn_bD)(CDC*); BOOL (AFX_MSG_CALL CWnd::*pfn_bb)(BOOL); BOOL (AFX_MSG_CALL CWnd::*pfn_bWww)(CWnd*, UINT, UINT); BOOL (AFX_MSG_CALL CWnd::*pfn_bHELPINFO)(HELPINFO*); HBRUSH (AFX_MSG_CALL CWnd::*pfn_hDWw)(CDC*, CWnd*, UINT); HBRUSH (AFX_MSG_CALL CWnd::*pfn_hDw)(CDC*, UINT); int (AFX_MSG_CALL CWnd::*pfn_iwWw)(UINT, CWnd*, UINT); int (AFX_MSG_CALL CWnd::*pfn_iww)(UINT, UINT); int (AFX_MSG_CALL CWnd::*pfn_iWww)(CWnd*, UINT, UINT); int (AFX_MSG_CALL CWnd::*pfn_is)(LPTSTR); LRESULT (AFX_MSG_CALL CWnd::*pfn_lwl)(WPARAM, LPARAM); LRESULT (AFX_MSG_CALL CWnd::*pfn_lwwM)(UINT, UINT, CMenu*); void (AFX_MSG_CALL CWnd::*pfn_vv)(void); void (AFX_MSG_CALL CWnd::*pfn_vw)(UINT); void (AFX_MSG_CALL CWnd::*pfn_vww)(UINT, UINT); void (AFX_MSG_CALL CWnd::*pfn_vvii)(int, int); void (AFX_MSG_CALL CWnd::*pfn_vwww)(UINT, UINT, UINT); void (AFX_MSG_CALL CWnd::*pfn_vwii)(UINT, int, int); ..、...};
然后就非常容易了,如果说在消息映射表中知道了相符的条目,就可以知道消息对应的消息响应函数的地址什么(就是函数的名称)。然后,根据AFX_MSGMAP_ENTRY条目中的nSig变量值,选择联合体中合适的成员就好了。以下是一个例子。
static BOOL DispatchCmdMsg(CCmdTarget* pTarget, UINT nID, int nCode, AFX_PMSG pfn, void* pExtra, UINT nSig, AFX_CMDHANDLERINFO* pHandlerInfo) // return TRUE to stop routing{union MessageMapFunctions mmf;//定义一个联合体变量,这个变量实际是某个函数指针mmf.pfn = pfn;BOOL bResult = TRUE; // default is ok..、... //以下根据AFX_MSGMAP_ENTRY条目中的nSig变量值,调用函数正确的形式和参数。//注意:函数的参数实际最初是由消息体中的wParam和lParam传递进来的switch (nSig){default: // illegalASSERT(FALSE);return 0;break;case AfxSigCmd_v:// normal command or control notificationASSERT(CN_COMMAND == 0); // CN_COMMAND same as BN_CLICKEDASSERT(pExtra == NULL);(pTarget->*mmf.pfnCmd_v_v)();break;case AfxSigCmd_b:// normal command or control notificationASSERT(CN_COMMAND == 0); // CN_COMMAND same as BN_CLICKEDASSERT(pExtra == NULL);bResult = (pTarget->*mmf.pfnCmd_b_v)();break;case AfxSigCmd_RANGE:// normal command or control notification in a rangeASSERT(CN_COMMAND == 0); // CN_COMMAND same as BN_CLICKEDASSERT(pExtra == NULL);(pTarget->*mmf.pfnCmd_v_u)(nID);break;case AfxSigCmd_EX:// extended command (passed ID, returns bContinue)ASSERT(pExtra == NULL);bResult = (pTarget->*mmf.pfnCmd_b_u)(nID);break;..、...}
这是MFC动人的一幕,不同的消息,参数不一样,返回值也不一样,而且在定义的时候只是一个指针,可是在调用的时候却有各种各样的方式。用了一个union变量,就将所有不同形态函数统一为一个,这太牛了。
可是为什么要这样做呢?为了降低程序的空间复杂度。如果我们使用C++常用的虚函数来实现多态,需要在类中维护一个虚函数表,从而实现基类指针调用子类方法的效果。由于Windows消息众多,如果在每个类中维护一个虚函数表,大大增加了程序了空间复杂度。
还剩下最后一个问题,比如上面函数中的这条语句
case AfxSigCmd_v:(pTarget->*mmf.pfnCmd_v_v)();break;
pTarget是一个CCmdTarge指针,mmf.pfnCmd_v_v指针类型如下:
void (AFX_MSG_CALL CCmdTarget::*pfnCmd_v_v)();
可以看出,这分别是一个基类指针,调用了一个基类方法,怎么就最终调用成子类的方法了呢?这里关键是要理解成员解除引用(->*)运算符。基类指针使用成员解除引用运算符调用某个子类的成员函数时,基类指针指向的地址(实际指向子类对象)会被当做this指针压栈,然后程序直接转到这个被调用的函数的地址(实际上这个地址是子类成员函数的地址),然后程序根据this指针获取类中数据成员,而this指针实际指向的是子类对象。最终的效果就好像是基类指针调用一个虚函数。
以下代码模式了这个过程。
#include
That is all!