如果在看完上一篇文章后觉得有点晕,不要害怕。本节我们就不用这些宏,而是用其中的内容重新完成开头那个程序,进而探究MFC消息映射的本来面目。
MFC消息映射机制初探
还我本来面目
class CMyWnd : public CFrameWnd
{
public:
//原有代码
void OnLButtonDown(UINT nFlags, CPoint point)
{
MessageBox(TEXT("In CMyWnd::OnLButtonDown"), TEXT("In CMyWnd::OnLButtonDown"), MB_OK);
}
static const AFX_MSGMAP* __stdcall GetThisMessageMap();
virtual const AFX_MSGMAP* GetMessageMap() const;
};
const AFX_MSGMAP* CMyWnd::GetMessageMap() const
{
return GetThisMessageMap();
}
const AFX_MSGMAP* __stdcall CMyWnd::GetThisMessageMap()
{
typedef CMyWnd ThisClass;
typedef CFrameWnd TheBaseClass;
static const AFX_MSGMAP_ENTRY _messageEntries[] =
{
{ WM_LBUTTONDOWN, 0, 0, 0, AfxSig_vwp, (AFX_PMSG)&ThisClass::OnLButtonDown },
{ 0, 0, 0, 0, AfxSig_end, (AFX_PMSG)0 }
};
static const AFX_MSGMAP messageMap = { &TheBaseClass::GetThisMessageMap, &_messageEntries[0] };
return &messageMap;
}
编译后,程序功能与之前用宏定义的无异。我们再通过手工添加代码的方式让程序响应按键消息。
在CMyWnd类中增加OnChar函数:
void OnChar(UINT nChar, UINT nRepCnt, UINT nFlags)
{
static TCHAR buff[32];
wsprintf(buff, TEXT("%c键被按下"), nChar);
MessageBox(buff, buff, MB_OK);
}
在CMyWnd::GetThisMessage函数中的_messageEntries[]结构数组如下:
static const AFX_MSGMAP_ENTRY _messageEntries[] =
{
{ WM_LBUTTONDOWN, 0, 0, 0, AfxSig_vwp, (AFX_PMSG)&ThisClass::OnLButtonDown },
{ WM_CHAR, 0, 0, 0, AfxSig_vwww, (AFX_PMSG)&ThisClass::OnChar },
{ 0, 0, 0, 0, AfxSig_end, (AFX_PMSG)0 }
};
编译后,就能处理按键消息了。
消息路由
下面来看看,消息是怎样到达我们定义的CMyWnd处理函数的。在GetMessageMap和GetThisMessageMap上下断点,并在CMyWnd::CMyWnd的Create函数上下断点,调试运行程序。
待在Create函数中断时,查看CMyWnd对象的地址:
可见其地址为0x0079cda0;恢复运行后,程序断在了CMyWnd::GetMessageMap上
我们一项项的来看:
AfxWndProc应该是MFC默认的窗口过程:
在该函数中,通过CWnd::FromHandlePermanent(hWnd)函数,由窗口句柄hWnd找到了CWnd类地址。经过比对,确系CMyWnd类对象的地址。
接着在AfxCallWndProc中,调用了WindowProc:
而在CWnd::WindowProc中又调用了OnWndMsg
在CWnd::OnWndMsg是普通窗口消息处理的关键所在,在这里,还发现了MFC用的全局锁以及对特殊消息的特俗处理,这里先按下不表。在该函数中调用了GetMessageMap
由于GetMessageMap为虚函数,所以调到的是CMyWnd::GetMessageMap:
GetMessageMap获得了AFX_MSGMAP指针以后,先去Cache表里查找(这部分属于优化内容,先省略),找不到就按常规方式,从子类的消息映射表开始,一项项匹配,子类找完找父类,父类找完找爷爷,一直找到祖宗十八代(pMessageMap->pfnGetBaseMap == NULL 为边界条件)
for (; pMessageMap->pfnGetBaseMap != NULL; pMessageMap = (*pMessageMap->pfnGetBaseMap)())
{
// Note: catch not so common but fatal mistake!!
// BEGIN_MESSAGE_MAP(CMyWnd, CMyWnd)
ASSERT(pMessageMap != (*pMessageMap->pfnGetBaseMap)());
if (message < 0xC000)
{
// constant window message
if ((lpEntry = AfxFindMessageEntry(pMessageMap->lpEntries, message, 0, 0)) != NULL)
{
pMsgCache->lpEntry = lpEntry;
winMsgLock.Unlock();
goto LDispatch;
}
}
else
{
// registered windows message
lpEntry = pMessageMap->lpEntries;
while ((lpEntry = AfxFindMessageEntry(lpEntry, 0xC000, 0, 0)) != NULL)
{
UINT* pnID = (UINT*)(lpEntry->nSig);
ASSERT(*pnID >= 0xC000 || *pnID == 0);
// must be successfully registered
if (*pnID == message)
{
pMsgCache->lpEntry = lpEntry;
winMsgLock.Unlock();
goto LDispatchRegistered;
}
lpEntry++; // keep looking past this one
}
}
可以看出,for循环的逻辑是先在CMyWnd的消息映射表中找目标消息处理函数,如果没找到,再去其父类的消息映射表中找。具体查找的算法在AfxFindMessageEntry中,我们来看看,居然内联了一段汇编!翻译成C语言代码差不多这样:
while (lpEntry->nSig != AfxSig_end) //末尾元素全0,用nSig字段加以判断
{
if (lpEntry->nMessage == nMsg && lpEntry->nCode == nCode && nID >= lpEntry->nID && nID <= lpEntry->nLastID)
{
return lpEntry;
}
lpEntry++; // 检查下一条目
}
return NULL; // 未找到,返回NULL
当从家族的消息映射表(不管是子类的、父类的还是各祖宗类的消息映射表)中找到目标消息的处理函数后,就准备调用了,这部分也挺有意思:
LDispatch:
ASSERT(message < 0xC000);
mmf.pfn = lpEntry->pfn;
switch (lpEntry->nSig)
{
default:
ASSERT(FALSE);
break;
//若干case
case AfxSig_v_u_p: //OnLButtonDown的函数原型
{
CPoint point(lParam);
(this->*mmf.pfn_v_u_p)(static_cast<UINT>(wParam), point);
break;
}
//一堆case
case AfxSig_v_u_uu: //OnChar的函数原型
(this->*mmf.pfn_v_u_u_u)(static_cast<UINT>(wParam), LOWORD(lParam), HIWORD(lParam));
break;
//一堆case
break;
}
即根据消息映射表中登记的函数原型,构造好相应的参数,并调用相应的消息处理函数。这下知道消息映射表中那一堆Sig宏的作用了吧!
最后,明明传入的是mmf.pfn的地址,怎么调用时候出来了一堆千奇百怪的诸如mmf.pfn_v_u_u_u、mmf.pfn_v_u_p之类的呢?so easy,想必就是用了C语言中的联合体嘛,验证看看:
union MessageMapFunctions
{
AFX_PMSG pfn; // generic member function pointer
BOOL (AFX_MSG_CALL CCmdTarget::*pfn_b_D)(CDC*);
BOOL (AFX_MSG_CALL CCmdTarget::*pfn_b_b)(BOOL);
BOOL (AFX_MSG_CALL CCmdTarget::*pfn_b_u)(UINT);
//
void (AFX_MSG_CALL CWnd::*pfn_v_u_u_u)(UINT, UINT, UINT);
//
void (AFX_MSG_CALL CWnd::*pfn_v_u_p)(UINT, CPoint);
//...
}
union MessageMapFunctions mmf;
其实就是一个DWORD,只不过为了在C语言中让其优雅,定义了一大堆。
小结
其实借助上面的循环遍历查找代码,可以把消息映射表抽象为一个单项链表,子类的消息映射表是首节点,如果在首节点中没有找到,再调用pfnGetBaseMap得到父类的消息映射表,继续查找。以此类推。