欢迎您访问365答案网,请分享给你的朋友!
生活常识 学习资料

MFC中消息映射与命令传递机制初探

时间:2023-05-31

本文主要根据侯捷《深入浅出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(wParam)));break;case AfxSig_l_D_u:lResult = (this->*mmf.pfn_l_D_u)(CDC::FromHandle(reinterpret_cast(wParam)), (UINT)lParam);break;case AfxSig_b_b_v:lResult = (this->*mmf.pfn_b_b)(static_cast(wParam));break;case AfxSig_b_u_v:lResult = (this->*mmf.pfn_b_u)(static_cast(wParam));break;case AfxSig_b_h_v:lResult = (this->*mmf.pfn_b_h)(reinterpret_cast(wParam));break;..、..、}goto LReturnTrue;LDispatchRegistered: // for registered windows messagesASSERT(message >= 0xC000);ASSERT(sizeof(mmf) == sizeof(mmf.pfn));mmf.pfn = lpEntry->pfn;lResult = (this->*mmf.pfn_l_w_l)(wParam, lParam);LReturnTrue:if (pResult != NULL)*pResult = lResult;return TRUE;}const AFX_MSGMAP_ENTRY* AFXAPIAfxFindMessageEntry(const AFX_MSGMAP_ENTRY* lpEntry,UINT nMsg, UINT nCode, UINT nID){..、..、//以汇编语言处理,加快速度// C version of search routinewhile (lpEntry->nSig != AfxSig_end){if (lpEntry->nMessage == nMsg && lpEntry->nCode == nCode &&nID >= lpEntry->nID && nID <= lpEntry->nLastID){return lpEntry;}lpEntry++;}return NULL; // not found}

通过该函数可以看出,如果是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指针实际指向的是子类对象。最终的效果就好像是基类指针调用一个虚函数。
以下代码模式了这个过程。

#includeusing namespace std;class Cbase{public:void basePrintMsg() {cout << "In base class" << endl;}};class CDerive :public Cbase{public:CDerive() :m_iDerive(3) {}int GetInt(int m) {return m_iDerive * m;}double GetDouble(int m, double d) {return m * d;}private:int m_iDerive;};typedef void (Cbase::* pbaseVoidFun)();union UMapFuns{//请注意,以下三个成员都是基类的成员函数的函数指针pbaseVoidFun pfn;double (Cbase::*pfn_double)(int m, double d);int (Cbase::*pfn_int)(int m);};int main(){UMapFuns uMapFun;//向MFC学习,定义一个联合体变量,表示函数指针uMapFun.pfn = (pbaseVoidFun)&CDerive::GetInt;//子类方法转为基类方法赋给联合体CDerive cDeriveObj;Cbase* pbase = &cDeriveObj;//调用成功,关键要理解uMapFun.pfn_int的地址实际上是子类成员函数的地址,子类对象//调用解除引用运算符时,不过是将子类对象的地址压栈,让子类成员函数能够根据这个//地址找到类的其他成员。int i = (cDeriveObj.*(uMapFun.pfn_int))(3);//调用成功,用基类指针调用子类函数。关键要理解uMapFun.pfn_int的地址实际上是子类//成员函数的地址,基类指针指向子类对象的地址,该地址压栈后,//子类成员函数根据这个地址找到的类的数据成员都是正确的。int i2 = (pbase->*(uMapFun.pfn_int))(4);uMapFun.pfn = (pbaseVoidFun)&CDerive::GetDouble;double d = (pbase->*(uMapFun.pfn_double))(3, 4.0);//调用成功,用基类指针调用子类函数return 0;}

That is all!

Copyright © 2016-2020 www.365daan.com All Rights Reserved. 365答案网 版权所有 备案号:

部分内容来自互联网,版权归原作者所有,如有冒犯请联系我们,我们将在三个工作时内妥善处理。