本文主要根据侯捷《深入浅出MFC》整理而成,主要讲述MFC消息映射与传递机制。
一 如何形成消息映射网
1 在源文件加入实现消息映射表格代码
首先你必须在头文件中(.H)声明消息映射表格
class CScribbleDoc : public CDocument
{
...
DECLARE_MESSAGE_MAP()
};
然后在实现文件中(.CPP)实现此表格:
BEGIN_MESSAGE_MAP(CScribbleDoc, CDocument)
ON_COMMAND(ID_EDIT_CLEAR_ALL, OnEditClearAll)
ON_COMMAND(ID_PEN_THICK_OR_THIN, OnPenThickOrThin)
...
END_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;
UINT nCode;
UINT nID;
UINT nLastID;
UINT_PTR nSig;
AFX_PMSG pfn;
};
任何一个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;
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;
}
........
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 (........)
{
........
}
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
{
lpEntry = pMessageMap->lpEntries;
while ((lpEntry = AfxFindMessageEntry(lpEntry, 0xC000, 0, 0)) != NULL)
{
UINT* pnID = (UINT*)(lpEntry->nSig);
ASSERT(*pnID >= 0xC000 || *pnID == 0);
if (*pnID == message)
{
pMsgCache->lpEntry = lpEntry;
winMsgLock.Unlock();
goto LDispatchRegistered;
}
lpEntry++;
}
}
}
pMsgCache->lpEntry = NULL;
winMsgLock.Unlock();
return FALSE;
}
LDispatch:
mmf.pfn = lpEntry->pfn;
switch (lpEntry->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<HDC>(wParam)));
break;
case AfxSig_l_D_u:
lResult = (this->*mmf.pfn_l_D_u)(CDC::FromHandle(reinterpret_cast<HDC>(wParam)), (UINT)lParam);
break;
case AfxSig_b_b_v:
lResult = (this->*mmf.pfn_b_b)(static_cast<BOOL>(wParam));
break;
case AfxSig_b_u_v:
lResult = (this->*mmf.pfn_b_u)(static_cast<UINT>(wParam));
break;
case AfxSig_b_h_v:
lResult = (this->*mmf.pfn_b_h)(reinterpret_cast<HANDLE>(wParam));
break;
... ...
}
goto LReturnTrue;
LDispatchRegistered:
ASSERT(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* AFXAPI
AfxFindMessageEntry(const AFX_MSGMAP_ENTRY* lpEntry,
UINT nMsg, UINT nCode, UINT nID)
{
... ...
while (lpEntry->nSig != AfxSig_end)
{
if (lpEntry->nMessage == nMsg && lpEntry->nCode == nCode &&
nID >= lpEntry->nID && nID <= lpEntry->nLastID)
{
return lpEntry;
}
lpEntry++;
}
return NULL;
}
通过该函数可以看出,如果是Windows消息的话,采取的是直线上溯的方式,一一比较消息和消息映射表中的条目是否相符。查找函数的实现函数为AfxFindMessageEntry,这个函数其实就是迭代本类中的消息映射表,尝试找到nMessage、nCode、nID都相同的条目。如果在这个子类中没有找到符合的条目,就到这个上一层父类中去找,直到找到为止。 那么,找到会怎么做?怎么调用相关的响应函数?这里的关键是AFX_MSGMAP_ENTRY条目中的nSig变量。 nSig变量可能的值如下:
enum AfxSig
{
AfxSig_end = 0,
AfxSig_bD,
AfxSig_bb,
AfxSig_bWww,
AfxSig_hDWw,
AfxSig_hDw,
AfxSig_iwWw,
AfxSig_iww,
AfxSig_iWww,
AfxSig_is,
AfxSig_lwl,
AfxSig_lwwM,
AfxSig_vv,
AfxSig_vw,
AfxSig_vww,
AfxSig_vvii,
AfxSig_vwww,
AfxSig_vwii,
AfxSig_vwl,
AfxSig_vbWW,
... ...
};
AfxSig枚举类型最后几位代表函数类型,例如 AfxSig_bWww代表函数返回值为bool型,函数参数依次为CWnd*, UINT和 UINT。 程序通过查看AFX_MSGMAP_ENTRY条目中的nSig变量值,就可以知道条目中消息响应函数的函数类型。刚刚我们说过了,消息响应函数表中所存储的都是AFX_PMSG 类型的函数指针,怎么转为正确的函数指针呢? MFC将所有可能的响应函数形式定义为了一个联合体变量。
union MessageMapFunctions
{
AFX_PMSG pfn;
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)
{
union MessageMapFunctions mmf;
mmf.pfn = pfn;
BOOL bResult = TRUE;
... ...
switch (nSig)
{
default:
ASSERT(FALSE);
return 0;
break;
case AfxSigCmd_v:
ASSERT(CN_COMMAND == 0);
ASSERT(pExtra == NULL);
(pTarget->*mmf.pfnCmd_v_v)();
break;
case AfxSigCmd_b:
ASSERT(CN_COMMAND == 0);
ASSERT(pExtra == NULL);
bResult = (pTarget->*mmf.pfnCmd_b_v)();
break;
case AfxSigCmd_RANGE:
ASSERT(CN_COMMAND == 0);
ASSERT(pExtra == NULL);
(pTarget->*mmf.pfnCmd_v_u)(nID);
break;
case AfxSigCmd_EX:
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<iostream>
using 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;
uMapFun.pfn = (pBaseVoidFun)&CDerive::GetInt;
CDerive cDeriveObj;
CBase* pBase = &cDeriveObj;
int i = (cDeriveObj.*(uMapFun.pfn_int))(3);
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!
|