MFC原始碼實戰分析(三)——訊息對映原理與訊息路由機制初探
如果在看完上一篇文章後覺得有點暈,不要害怕。本節我們就不用這些巨集,而是用其中的內容重新完成開頭那個程式,進而探究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得到父類的訊息對映表,繼續查詢。以此類推。