MFC源码实战分析(四)——hWnd与CWnd之千里情缘一线牵

MFC源码解析:CWnd与hWnd的关联建立
本文深入探讨MFC的消息响应机制,重点分析如何建立起CWnd对象与窗口句柄hWnd之间的联系。通过CWnd::FromHandlePermanent函数,MFC使用CHandleMap类的CMapPtrToPtr映射实现从hWnd到CWnd的查找。在窗口创建过程中,CBT钩子被用来在本线程内存储hWnd-CWnd*对,并将窗口过程替换为AfxWndProc,以便处理消息。

深入MFC的消息响应机制

对于Win32下的应用程序框架来说,如何建立起窗口句柄和窗口类间的对应关系是其在封装窗口类时必须考虑的,而这个过程和男女谈恋爱很类似:从CWnd找到对应的hWnd是相对容易的(m_hWnd就是CWnd的成员变量),就好像“女追男,隔层纱”;而要从hWnd找到对应的CWnd就难了,正所谓“男追女,隔座山”。因此,为了建立起hWnd到CWnd的联系,各大框架的设计者也是绞尽脑汁,就像追女孩一样,可谓“八仙过海,各显神通”。正好今天心情不太好,咱们就一起顺着MFC前辈当年的足迹“爬爬山”。

建立起CWnd*与HWND的联系

通过前文的分析,我们知道MFC有一个全局的窗口过程AfxWndProc供User32.dll回调,在其中通过CWnd::FromHandlePermanent(hWnd)函数,由窗口句柄hWnd找到了CWnd类地址。那么CWnd::FromHandlePermanent(HWND)是怎样由hWnd找到CWnd的指针的呢?我们来看看它的源码。
这里写图片描述
首先调用全局函数afxMapHWND获取到了一个CHandleMap类型对象的指针,并调用其LookupPermanent函数,传入hWnd值,由该函数返回CWnd指针。从其名字中的Map我们猜测,这应该是一个map容器。下面继续跟踪其类定义:

class CHandleMap
{
private:    // implementation
    CFixedAllocNoSync m_alloc;
    void (PASCAL* m_pfnConstructObject)(CObject* pObject);
    void (PASCAL* m_pfnDestructObject)(CObject* pObject);
    CMapPtrToPtr m_permanentMap;
    CMapPtrToPtr m_temporaryMap;
    CRuntimeClass* m_pClass;
    size_t m_nOffset;       // offset of handles in the object
    int m_nHandles;         // 1 or 2 (for CDC)

public:
    CHandleMap(CRuntimeClass* pClass, 
        void (PASCAL* pfnConstructObject)(CObject* pObject),
        void (PASCAL* pfnDestructObject)(CObject* pObject),
        size_t nOffset, int nHandles = 1);
    virtual ~CHandleMap() { DeleteTemp(); }

// Operations
public:
    CObject* FromHandle(HANDLE h);
    void DeleteTemp();
    void SetPermanent(HANDLE h, CObject* permOb);
    void RemoveHandle(HANDLE h);
    CObject* LookupPermanent(HANDLE h);
    CObject* LookupTemporary(HANDLE h);
    friend class CWinThread;
};

inline void CHandleMap::SetPermanent(HANDLE h, CObject* permOb)
    { m_permanentMap[(LPVOID)h] = permOb; }

inline void CHandleMap::RemoveHandle(HANDLE h)
{
    m_permanentMap.RemoveKey((LPVOID)h);
}

inline CObject* CHandleMap::LookupPermanent(HANDLE h)
    { return (CObject*)m_permanentMap.GetValueAt((LPVOID)h); }
inline CObject* CHandleMap::LookupTemporary(HANDLE h)
    { return (CObject*)m_temporaryMap.GetValueAt((LPVOID)h); }

从源码分析,这个类基本就是一个adapter,对CMapPtrToPtr进行了包装。关于CMapPtrToPtr,这里不再继续跟踪,否则就偏离我们的主线了,更多详细信息请自行分析源码,或参考MSDN https://docs.microsoft.com/en-us/cpp/mfc/reference/cmapptrtoptr-class

为什么要跟到CHandleMap,却不再关注其中CMapPtrToPtr呢?因为我们的目的已经达到了,从CHandleMap的类定义中,我们找到了与LookupPermanent相对应的函数SetPermanent,下面,看看HWND-CWND*组合是何时被传入的。

跟踪建立过程

我们在CMyWnd::CMyWnd上下断点,调试运行程序。当CMyWnd::CMyWnd被断下时,记住其实例地址。
这里写图片描述
由上图得CMyWnd类实例的地址为0x01404CD8,此时我们再对CHandleMap::SetPermanent下条件断点:
这里写图片描述
继续运行程序,断点被命中,查看调用栈:
这里写图片描述
我的天,我没看错吧?!user32.dll!DispatchHookW发起的调用,居然是在钩子处理函数中调用的!先不管那么多,找到向map容器中存入key-value对的地方:
这里写图片描述
再倒回去看看那个钩子函数:

LRESULT CALLBACK
_AfxCbtFilterHook(int code, WPARAM wParam, LPARAM lParam)
{
    _AFX_THREAD_STATE* pThreadState = _afxThreadState.GetData();
    if (code != HCBT_CREATEWND) return CallNextHookEx(pThreadState->m_hHookOldCbtFilter, code, wParam, lParam);

    CWnd* pWndInit = pThreadState->m_pWndInit;
    if (pWndInit != NULL || (!(lpcs->style & WS_CHILD)))
    {
        HWND hWnd = (HWND)wParam;
        WNDPROC oldWndProc;
        if (pWndInit != NULL)
        {
            // 将HWND-PCWnd对存入map中
            pWndInit->Attach(hWnd);
            // 在替换消息处理函数前调用此函数,目前为空函数(所谓子类化就是换掉窗口过程)
            pWndInit->PreSubclassWindow(); 
            // 获取AfxWndProc并替换窗口过程
            WNDPROC afxWndProc = AfxGetAfxWndProc(); //填入AfxWndProc
            oldWndProc = (WNDPROC)SetWindowLongPtr(hWnd, GWLP_WNDPROC, (DWORD_PTR)afxWndProc);
        }
        //省略else部分
    }
    LRESULT lResult = CallNextHookEx(pThreadState->m_hHookOldCbtFilter, code,
        wParam, lParam);
    return lResult;
}

可以看出这是一个CBT类型的消息钩子,在这里的主要目的是截获窗口创建消息,并将hWnd-CWnd*值存入map容器,然后将窗口过程替换掉。那就说明原先注册窗口类时,安装的窗口过程函数不是AfxWndProc。下面我们来看看窗口创建时的景象。

初始消息处理函数、设置CBT钩子

我们现在CMyWnd::CMyWnd处下断点,调试运行程序后待断下,然后在RegisterClassA、RegisterClassW、RegisterClassExA、RegisterClassExW处下断点。运行后程序被断下:
此时查看调用堆栈:
这里写图片描述
关键函数是CWnd::CreateEx

BOOL CWnd::CreateEx(DWORD dwExStyle, LPCTSTR lpszClassName,
    LPCTSTR lpszWindowName, DWORD dwStyle,
    int x, int y, int nWidth, int nHeight,
    HWND hWndParent, HMENU nIDorHMenu, LPVOID lpParam)
{
    // allow modification of several common create parameters
    CREATESTRUCT cs;
    cs.dwExStyle = dwExStyle;
    cs.lpszClass = lpszClassName;
    cs.lpszName = lpszWindowName;
    cs.style = dwStyle;
    cs.x = x;
    cs.y = y;
    cs.cx = nWidth;
    cs.cy = nHeight;
    cs.hwndParent = hWndParent;
    cs.hMenu = nIDorHMenu;
    cs.hInstance = AfxGetInstanceHandle();
    cs.lpCreateParams = lpParam;

    if (!PreCreateWindow(cs))
    {
        PostNcDestroy();
        return FALSE;
    }

    AfxHookWindowCreate(this);
    HWND hWnd = CreateWindowEx(cs.dwExStyle, cs.lpszClass,
            cs.lpszName, cs.style, cs.x, cs.y, cs.cx, cs.cy,
            cs.hwndParent, cs.hMenu, cs.hInstance, cs.lpCreateParams);

    if (!AfxUnhookWindowCreate())
        PostNcDestroy();        // cleanup if CreateWindowEx fails too soon

    if (hWnd == NULL)
        return FALSE;
    ASSERT(hWnd == m_hWnd); // should have been set in send msg hook
    return TRUE;
}

其中,在PreCreateWindow中用传入的CREATESTRUCT,由其调用AfxEndDeferRegisterClass负责注册窗口类,在该函数中,将窗口类的窗口过程回调函数设为了DefWindowProc。这里PreCreateWindow是虚函数,被CFrameWnd类override了。
接下来就调用AfxHookWindowCreate了,代码如下:

void AFXAPI AfxHookWindowCreate(CWnd* pWnd)
{
    _AFX_THREAD_STATE* pThreadState = _afxThreadState.GetData();
    if (pThreadState->m_pWndInit == pWnd)
        return;

    if (pThreadState->m_hHookOldCbtFilter == NULL)
    {
        pThreadState->m_hHookOldCbtFilter = ::SetWindowsHookEx(WH_CBT,
            _AfxCbtFilterHook, NULL, ::GetCurrentThreadId());    //设置本线程的CBT钩子处理函数
        if (pThreadState->m_hHookOldCbtFilter == NULL)
            AfxThrowMemoryException();
    }

    pThreadState->m_pWndInit = pWnd;     //保存窗口类指针
}

可以看出,该CBT钩子的作用范围仅限于本线程内。最后,再看看注册时传入的DefWndProc:
这里写图片描述

这里写图片描述
厉害了,就是Win32SDK编程中缺省的那个。为什么MFC不直接将AfxWndProc作为默认窗口处理函数呢?搜索的答案是为了支持3D控件。

小结

  1. 在CMyWnd::CMyWnd调用虚函数Create时,由于CFrameWnd类override了此函数,所以调到的是CFrameWnd::Create
  2. CFrameWnd::Create中先加载了菜单之类的,然后调用虚函数CreateEx,由于CFrameWnd没有override此函数,因此调到了CWnd::CreateEx
  3. CWnd::CreateEx中,首先构造了一个CREATESTRUCT,填写了大部分信息(回调函数留空),引用传递给了虚函数PreCreateWindow,CFrameWnd重载了此函数,因此调用了CFrameWnd::PreCreateWindow
  4. CFrameWnd::PreCreateWindow中调用了全局函数AfxEndDeferRegisterClass,该函数根据要求设置了CREATESTRUCT中其余字段并将窗口过程地址为DefWindowProc并调用Win32API:RegisterClass注册了窗口类后返回
  5. AfxEndDeferRegisterClass、CFrameWnd::PreCreateWindow先后返回,在CWnd::CreateEx中调用全局函数AfxHookWindowCreate(this)将this存入线程存储空间并设置可线程级的CBT钩子。
  6. CWnd::CreateEx中继续调用Win32API:CreateEx创建窗口
  7. 窗口创建消息被线程级CBT钩子_AfxCbtFilterHook截获,在其中进行了两项重要操作:1.将hWnd-CWnd*存入了map容器中;2.替换窗口过程为AfxWndProc
  8. Win32API CreateEx完成后,CWnd::CreateEx调用AfxUnhookWindowCreate()卸载掉CBT钩子,然后返回。
评论
成就一亿技术人!
拼手气红包6.0元
还能输入1000个字符
 
红包 添加红包
表情包 插入表情
 条评论被折叠 查看
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值