原文:http://blog.csdn.net/hw_henry2008/article/details/6453730
上面说完了ATL的消息分发方式,下面继续MFC的实现机制。
二、MFC消息分发的实现方式:
首先来看注册窗口时:
还是以对话框为例子,调用DoModal 后,进入:
- INT_PTR CDialog::DoModal()
- {
- //···进行资源的准备
- // disable parent (before creating dialog)
- HWND hWndParent = PreModal();//得到父窗口的句柄,并且调用AfxHookWindowCreate,这是关键
- AfxUnhookWindowCreate();
- //···
- AfxHookWindowCreate(this);
- if (CreateDlgIndirect(lpDialogTemplate,
- CWnd::FromHandle(hWndParent), hInst))
- {
- //···
- VERIFY(RunModalLoop(dwFlags) == m_nModalResult);
- //···
- }
- //···
- }
- HWND CDialog::PreModal()
- {
- //····
- HWND hWnd = CWnd::GetSafeOwner_(m_pParentWnd->GetSafeHwnd(), &m_hWndTop);
- //下面的函数看名字也能知道,是WINDOWS的钩子函数,下面进去看看就知道它干的什么事了
- AfxHookWindowCreate(this);
- return hWnd;// return window to use as parent for dialog
- }
其实PreModal没干啥事,就是调用了下面的AfxHookWindowCreate函数,以及得到父窗口句柄
- void AFXAPI AfxHookWindowCreate(CWnd* pWnd)
- {
- _AFX_THREAD_STATE* pThreadState = _afxThreadState.GetData();//看下面的说明
- if (pThreadState->m_pWndInit == pWnd)
- return;
- if (pThreadState->m_hHookOldCbtFilter == NULL)
- {//可以看出,这if之后进来一次,可以调用多次。
- pThreadState->m_hHookOldCbtFilter = ::SetWindowsHookEx(WH_CBT,
- _AfxCbtFilterHook, NULL, ::GetCurrentThreadId());
- if (pThreadState->m_hHookOldCbtFilter == NULL)
- AfxThrowMemoryException();
- }
- //···
- pThreadState->m_pWndInit = pWnd;
- }
AfxHookWindowCreate在MFC中起着很奇特的作用,也很有意思。先看第一行的 _afxThreadState.GetData();
字面意思可以看出是得到属于线程的一些状态信息,不过如果大家看其实现的话就会发现有些字眼很熟悉,比如:(CThreadData*)TlsGetValue(m_tlsIndex); 对,TLS,线程局部存储,它借助window操作系统提供的线程局部存储来保存与线程相关的一些变量数据,因为我们知道,消息队列是基于线程的,也就是每个线程可以有一个消息队列。
下面看重头戏:SetWindowsHookEx,::SetWindowsHookEx(WH_CBT,_AfxCbtFilterHook, NULL, ::GetCurrentThreadId())也即给当前线程安装一个线程钩子。
类型是WH_CBT,钩子函数为:_AfxCbtFilterHook。那么WH_CBT是说明意思呢?MSDN:
WH_CBT :Installs a hook procedure that receives notifications useful to a computer-based training (CBT)
application. For more information, see the CBTProc hook procedure.
_AfxCbtFilterHook也就是这个所谓的CBTProc钩子函数类型。
The CBTProc hook procedure is an application-defined or library-defined callback function used with the SetWindowsHookEx function. The system calls this function before activating, creating, destroying, minimizing, maximizing, moving, or sizing a window; before completing a system command; before removing a mouse or keyboard event from the system message queue; before setting the keyboard focus; or before synchronizing with the system message queue. A computer-based training (CBT) application uses this hook procedure to receive useful notifications from the system.
其实意思就是:给线程安装一个WH_CBT钩子,当激活,创建,销毁··一个窗口的时候回先调用这个钩子函数通知我们。什么意思,也就是在线程创建窗口时,接收第一个消息之前会调研我们这个函数。
下面先别急着看钩子过程的处理方式,因为我们知道,现在它是不会被调研的,它一定会在creating窗口的时候在系统内部被调用。咱们继续回到DoModal的代码,HW_CBT钩子安装好了,下面也该创建窗口了吧?
进入CreateDlgIndirect,很长的函数···
- BOOL CWnd::CreateDlgIndirect(LPCDLGTEMPLATE lpDialogTemplate,
- CWnd* pParentWnd, HINSTANCE hInst)
- {
- //···
- AfxHookWindowCreate(this);//这个函数刚才见过,只是这回的调用没啥实际用途了。
- //笔者纳闷为何他调用了2次,虽然不会出问题,但是···总之不爽
- hWnd = ::CreateDialogIndirect(hInst, lpDialogTemplate,
- pParentWnd->GetSafeHwnd(), AfxDlgProc);
- //···此处省略很多行,跟我们这无关的代码
- }
恩,原来这样,CreateDialogIndirect创建一个对话框,提供的窗口过程是AfxDlgProc ! 不过也许我们不会忘记,AfxHookWindowCreate安装了一个WH_CBT钩子,会在创建窗口的时候同步调用我们的钩子函数。顺便说一下SendMessage和PostMessage的区别。前者是同步的,实际上也就是一般的调用,知道调用的函数完成后才返回;
后者是把消息放到线程的消息队列里面,然后马上返回。这里操作系统会在创建窗口的过程中,发送WM_CREATE,WM_NCCREATE 消息之前调用钩子过程。因此保证了第一条消息的正确处理!!
- 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;
- BOOL bContextIsDLL = afxContextIsDLL;
- if (pWndInit != NULL || (!(lpcs->style & WS_CHILD) && !bContextIsDLL))
- {
- // ···
- if (pWndInit != NULL) {
- //···
- ASSERT(CWnd::FromHandlePermanent(hWnd) == NULL);
- pWndInit->Attach(hWnd);
- WNDPROC afxWndProc = AfxGetAfxWndProc();//实际返回:AfxWndProcBase
- oldWndProc = (WNDPROC)SetWindowLongPtr(hWnd, GWLP_WNDPROC,
- (DWORD_PTR)afxWndProc);//重新设置窗口句柄。
- //····
- pThreadState->m_pWndInit = NULL;//准备下一个窗口
- }
- //···
- }
- lCallNextHook:
- LRESULT lResult = CallNextHookEx(pThreadState->m_hHookOldCbtFilter, code, wParam, lParam);
- return lResult;
- }
首先我们感兴趣的是FromHandlePermanent,其接收一个HWND句柄,然后返回一个窗口类的指针!!!这不就是我们想要了解的:
MFC如何将窗口句柄和窗口实例联系起来的?
- CWnd* PASCAL CWnd::FromHandlePermanent(HWND hWnd) {
- CHandleMap* pMap = afxMapHWND();//该函数得到当前线程的m_pmapHWND数据成员。
- CWnd* pWnd = NULL;
- if (pMap != NULL) {
- // only look in the permanent map - does no allocations
- pWnd = (CWnd*)pMap->LookupPermanent(hWnd);//lookup,应该是查表了,对,就是查表。有点失望···
- ASSERT(pWnd == NULL || pWnd->m_hWnd == hWnd);
- }
- return pWnd;//返回查得的指针
- }
- inline CObject* CHandleMap::LookupPermanent(HANDLE h)//还好,是inline的,不然就真的太郁闷了,查表···不慢吗??
- { return (CObject*)m_permanentMap.GetValueAt((LPVOID)h); }
看到这基本有点预感了,MFC对应窗口句柄和对应窗口类实例直接的映射是通过查表来实现的。那么,问题来了,这些映射什么时候加入的呢???
明显的感觉:在创建窗口,还没有接收到WM_CREATE消息之前。不然如果没有映射怎么分发这条消息呀?发给哪个类?不知道。这个关键的建立映射的时期不正是钩子WH_CBT的钩子过程里吗??回到_AfxCbtFilterHook钩子函数中。pWndInit->Attach(hWnd)语句其实就是将当前句柄加入到当前线程的映射中。
- BOOL CWnd::Attach(HWND hWndNew)
- {
- ASSERT(FromHandlePermanent(hWndNew) == NULL);//查表?正常情况下当然不可能已经建立映射了,即返回空。
- //···
- CHandleMap* pMap = afxMapHWND(TRUE); // 返回这个映射表
- ASSERT(pMap != NULL);
- pMap->SetPermanent(m_hWnd = hWndNew, this);//建立一个:<HWND,CWnd *>即<窗口句柄,窗口类实例的this指针>映射!!
- //···
- return TRUE;
- }
看到这基本大功告成了,反正我是相当失望了,强大的复杂的MFC竟然用查表的方式建立映射,不过还好,是哈希表。基础数据结构是CMapPtrToPtr,关于CHandleMap里面的实现这里就不多说啦,读者可以自行查看。其用哈希表实现,所以效率还算差强人意。不过相对ATL的thunk技术就要逊色写了,个人觉得。
再补充一下_AfxCbtFilterHook钩子过程中关于窗口过程句柄的设置。AfxGetAfxWndProc函数返回的是AfxWndProcBase,后者的工作其实也就是转掉AfxWndProc函数。
- WNDPROC AFXAPI AfxGetAfxWndProc()
- {
- #ifdef _AFXDLL //一般定义了_AFXDLL
- return AfxGetModuleState()->m_pfnAfxWndProc;
- #else
- return &AfxWndProc;
- #endif
- }
- LRESULT CALLBACK
- AfxWndProcBase(HWND hWnd, UINT nMsg, WPARAM wParam, LPARAM lParam)
- {
- AFX_MANAGE_STATE(_afxBaseModuleState.GetData());
- return AfxWndProc(hWnd, nMsg, wParam, lParam);//殊途同归了又
- }
所以之后的窗口过程回调函数实际上就是AfxWndProcBase或AfxWndProc。在AfxWndProc中完成消息的分发。下面就简单了
- LRESULT CALLBACK
- AfxWndProc(HWND hWnd, UINT nMsg, WPARAM wParam, LPARAM lParam)
- {
- //···
- CWnd* pWnd = CWnd::FromHandlePermanent(hWnd);//这个熟悉的函数见过几次了,查找哈希表,返回指定窗口句柄的实例指针
- //···
- return AfxCallWndProc(pWnd, hWnd, nMsg, wParam, lParam);//传入窗口实例的this指针
- }
- LRESULT AFXAPI AfxCallWndProc(CWnd* pWnd, HWND hWnd, UINT nMsg,
- WPARAM wParam = 0, LPARAM lParam = 0)
- {
- //···
- //真正调用每个实例的窗口过程,终于找到对应的东家啦@!!从此就可以有私有数据了。
- lResult = pWnd->WindowProc(nMsg, wParam, lParam);
- //···
- return lResult;
- }
到此MFC的消息映射终于算是完成了。此后的事情就是在各种类继承层次中进行那漫长而又复杂的消息路由了。
也许会有疑问:为什么必须得安装一个WH_CBT钩子?其实很简单,HWND到this的映射必须在WM_CREATE等消息到达窗口过程之前设置好,而且我们的应用程序得到窗口过程是在CreateWindow,CreateDialogIndirect系列函数返回时当做返回值传给应用程序的,可是API告诉我们,此函数返回之前会发送WM_CREATE等函数!毫无疑问,等这个函数返回时去设置映射已经晚了,唯一的方法是安装钩子。
总结一下:
因为window操作系统对窗口过程的要求必须是静态或者全局的CALLBACK调用约定的函数,传入HWND参数标志对应的窗口。
而ATL/MFC 力求用面向对象封装繁琐的WIN32面向过程的window应用程序开发方式。但是又只能给操作系统提供统一的窗口过程。所以势必得再HWND和窗口类实例的this指针直接建立映射。在统一的窗口回调过程中再把消息“分发”到对应的窗口类的成员函数中去。
1. 对于ATL来说,其使用thunk技术巧妙的实现了这个目标。
2. MFC则使用哈希表来建立映射。
个人感觉这次ATL和MFC对弈的结果是ATL优于MFC,尽管后者哈希表可能能达到线性时间速度,但是thunk的技术还是要好点的。呵呵···
说的不对的地方还请大家指教一下,谢谢了!欢迎一起讨论!
///
要赶在WM_CREATE之前拦截并不是使用钩子的必然原因,ATL的StartWindowProc也能干同样的事。MFC使用WH_CBT的好处在于只设了与自己线程相关的钩子,窗口类的指针也是每个线程各自保存的。而ATL里的StartWindowProc和AtlWinModuleExtractCreateWndData是所有线程公用的。这样在多线程同时创建窗口的时候,理论上MFC的效率更高一些