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得到父类的消息映射表,继续查找。以此类推。

  • 1
    点赞
  • 5
    收藏
    觉得还不错? 一键收藏
  • 1
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值