1. 程式人生 > >MFC原始碼實戰分析(三)——訊息對映原理與訊息路由機制初探

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得到父類的訊息對映表,繼續查詢。以此類推。