VC++/MFC消息映射机制(3):MFC消息路由的源码分析
本文为原创文章,转载请注明出处,或注明转载自“黄邦勇帅(原名:黄勇)
《C++语法详解》网盘地址:https://pan.baidu.com/s/1dIxLMN5b91zpJN2sZv1MNg
若对C++语法不熟悉,建议参阅本人所著《C++语法详解》一书,电子工业出版社出版,该书语法示例短小精悍,对查阅C++知识点相当方便,并对语法原理进行了透彻、深入详细的讲解,可确保读者彻底弄懂C++的原理,彻底解惑C++,使其知其然更知其所以然。此书是一本全面了解C++不可多得的案头必备图书。
注意:本文最好结合本人所作前两篇与消息映射机制有关的文章一起阅读。
注意:消息路由包含了消息映射功能
1、MFC源码的消息映射宏,把模仿程序中的消息映射数组ss和消息映射表msgmp封装在了名为GetThisMessageMap()的函数内部。但其基本原理是相同的,请读者自行理解。
2、基本原理:在MFC源码中,消息路由的功能是在重新设置的过程函数::AfxWndProc()内部间接完成的,因此在MFC源码中的第一步就是使用钩子拦截消息,并重新设置过程函数。
3、首先使用钩子函数拦截消息,以实现消息映射。在MFC源码中是使用AfxHookWindowCreate()函数间接调用::SetWindowsHookEx(…)函数,设置钩子函数的。AfxHookWindowCreate是在CWnd::CreateEx函数中的PreCreateWindow函数(完成窗口注册)和::CreateWindowEx函数(完成窗口创建)之间调用的。
4、然后通过钩子函数间接的使用SetWindowLongPtr()函数,重新设置程序的过程函数为::AfxWndProc()。
5、过程函数AfxWndProc并不直接处理消息,而是通过调用CWnd::WindowProc函数处理由钩子拦截到的消息。消息路由的功能就是在该函数内通过调用OnWndMsg()间接完成的。
6、CWnd::WindowProc根据消息的不同情形,分别使用CWnd::OnWndMsg()函数和CWnd::DefWindowProc处理消息。其中CWnd::OnWndMsg()是主要的处理消息的函数,即消息映射和消息路由的功能都在该函数内直接或间接的实现。
7、CWnd::OnWndMsg()函数,根据接收到的消息类型的不同,而对消息进行不同的路由,若消息是命令消息WM_COMMAND,则调用OnCommand()虚函数处理。若消息是通知消息WM_NOTIFY则调用OnNotify虚函数进行处理,若消息是一般消息,则在CWnd::OnWndMsg()函数内部直接进行处理,对于一般消息,使用的是直线路由模式,因此CWnd::OnWndMsg内部实现了直线路由。
8、对于OnCommand虚函数,因为处理的是命令消息,而命令消息可能会拐弯,因此在OnCommand函数内部实现了拐弯路由。
一、使用钩子函数重新设置过程函数(以下程序全部在wincore.cpp文件内实现)
注:钩子函数的原理见后文附注讲解
以下程序全部来自wincore.cpp文件内部的MFC源码程序
BOOL CWnd::CreateEx(......){ //节选源码
......
if (!PreCreateWindow(cs)){PostNcDestroy();return FALSE;}
AfxHookWindowCreate(this); //❶、由该函数间接设置钩子函数。
HWND hWnd = CreateWindowEx(...); }
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()); //❷、粗体为设置的钩子函数
if (pThreadState->m_hHookOldCbtFilter == NULL) AfxThrowMemoryException();
}
//以下为一些发生错误时的处理代码。
ASSERT(pThreadState->m_hHookOldCbtFilter != NULL);
ASSERT(pWnd != NULL);
ASSERT(pWnd->m_hWnd == NULL); // only do once
ASSERT(pThreadState->m_pWndInit == NULL); // hook not already in progress
pThreadState->m_pWndInit = pWnd;}
LRESULT CALLBACK _AfxCbtFilterHook(int code, WPARAM wParam, LPARAM lParam){//节选源码
......
if (pWndInit != NULL || (!(lpcs->style & WS_CHILD) && !bContextIsDLL)){
......
if (pWndInit != NULL){
......
WNDPROC afxWndProc = AfxGetAfxWndProc(); //❸、粗体为重新设置的过程函数
oldWndProc = (WNDPROC)SetWindowLongPtr( //重新设置过程函数为❸的反回值。
hWnd, GWLP_WNDPROC,(DWORD_PTR)afxWndProc);
......}
WNDPROC AFXAPI AfxGetAfxWndProc(){//完整源码
#ifdef _AFXDLL
return AfxGetModuleState()->m_pfnAfxWndProc;
#else
return &AfxWndProc; //❹、粗体为重新设置的过程函数,至此过程函数重新设置完毕。
#endif
}
LRESULT CALLBACK AfxWndProc(HWND hWnd, UINT nMsg, WPARAM wParam, LPARAM lParam)//完整源码
{
// special message which identifies the window as using AfxWndProc
if (nMsg == WM_QUERYAFXWNDPROC) return 1;
// all other messages route through message map
CWnd* pWnd = CWnd::FromHandlePermanent(hWnd); //表示获取窗口句柄hWnd所关联的类对象。
ASSERT(pWnd != NULL);
ASSERT(pWnd==NULL || pWnd->m_hWnd == hWnd);
if (pWnd==NULL||pWnd->m_hWnd!=hWnd) return ::DefWindowProc(hWnd,nMsg,wParam,lParam);
return AfxCallWndProc(pWnd,hWnd,nMsg,wParam,lParam);}//❺、调用此函数完成过程函数的功能
LRESULT AfxCallWndProc(CWnd* pWnd, HWND hWnd, UINT nMsg,WPARAM wParam = 0, LPARAM lParam = 0)
......
if (nMsg == WM_INITDIALOG)
_AfxPreInitDialog(pWnd, &rectOld, &dwStyle);
lResult=pWnd->WindowProc(nMsg,wParam,lParam); //❻、调用父类的该函数完成过程函数的功能。
...... }
LRESULT CWnd::WindowProc(UINT message, WPARAM wParam, LPARAM lParam){//完整源码
// OnWndMsg does most of the work, except for DefWindowProc call
LRESULT lResult = 0;
if (!OnWndMsg(message, wParam, lParam, &lResult)) //❼、消息路由从此成员函数开始
lResult = DefWindowProc(message, wParam, lParam);
return lResult;
}
二、MFC源码实现直线路由(CWnd::OnWndMsg源码解析wincore.cpp)
1、基本原理:直线路由的实现原理请参阅模仿直线路由的讲解(示例3.10)。在MFC源码中是使用CWnd::OnWndMsg实现直线路由和拐弯路由的,其中直线路由由该函数内部直接实现,而拐弯路由通过调用另一个虚拟成员函数CWnd::OnCommand间接实现,另外OnWndMsg还对不同类型的消息,进行了不同的处理,详见源码。
2、存放<消息,处理函数>对的结构体(afxwin.h)
struct AFX_MSGMAP_ENTRY { UINT nMessage; UINT nCode; UINT nID;
UINT nLastID; UINT_PTR nSig; AFX_PMSG pfn; };
3、消息映射表(afxwin.h)
struct AFX_MSGMAP
{const AFX_MSGMAP* (PASCAL* pfnGetBaseMap)(); const AFX_MSGMAP_ENTRY* lpEntries;};
4、用于遍历消息映射数组的函数
const AFX_MSGMAP_ENTRY* AFXAPI
AfxFindMessageEntry(const AFX_MSGMAP_ENTRY* lpEntry,UINT nMsg, UINT nCode, UINT nID){
#if defined(_M_IX86)
...... //在这之中是一系列的汇编代码,用于加快处理速度。
#else
while (lpEntry->nSig != AfxSig_end) /*⓬、此while用于遍历消息映射数组_messageEntries(该数组在宏BEGIN_MESSAGE_MAP之中声明和定义中的所有<消息,处理函数>对。其原理与模仿程序示例3.10中的while语句相同。*/
{if (lpEntry->nMessage == nMsg /*⓭、比对从窗口接收到的消息是否与消息映射数组_messageEntries中<消息,处理函数>对里的消息相等*/
&& lpEntry->nCode == nCode &&nID >= lpEntry->nID && nID <= lpEntry->nLastID)
{ return lpEntry; }
lpEntry++;} //while结束
return NULL; // not found
#endif }
5、实现消息路由的共用体
union MessageMapFunctions{
BOOL (AFX_MSG_CALL CWnd::*pfn_b_D_v)(CDC*);
BOOL (AFX_MSG_CALL CWnd::*pfn_b_b_v)(BOOL);
BOOL (AFX_MSG_CALL CWnd::*pfn_b_W_uu)(CWnd*,UNIT,UNIT);
BOOL (AFX_MSG_CALL CWnd::*pfn_l_w_l)(WPARAM,LPARAM);
...... } //大约有一百多行类似的代码
6、需要用到的枚举
enum AfxSig //位于文件afxmsg_.h
{ AfxSig_end = 0, // [marks end of message map]
AfxSig_b_D_v, // BOOL (CDC*)
AfxSig_b_b_v, // BOOL (BOOL)
AfxSig_b_u_v, // BOOL (UINT)
AfxSig_b_h_v, // BOOL (HANDLE)
AfxSig_b_W_uu, // BOOL (CWnd*, UINT, UINT)
...... } //大约有一百多行类似的代码
7、Cwnd::OnWndMsg源码(wincore.cpp)
Cwnd::OnWndMsg源码中会用到由消息映射宏DECLARE_MESSAGE_MAP()、BEGIN_MESSAGE_MAP、END_MESSAGE_MAP、ON_MESSAGE展开后的代码,比如其中的GetMessageMap()函数。
BOOL CWnd::OnWndMsg(UINT message, WPARAM wParam, LPARAM lParam, LRESULT* pResult)
{//绝大部分源码
LRESULT lResult = 0;
union MessageMapFunctions mmf; //❶、共用体的使用(原理见示例3.5),其定义见后文。
mmf.pfn = 0;
CInternalGlobalLock winMsgLock;
// special case for commands
if (message == WM_COMMAND) //❷、判断消息是否是命令消息,若是,则按以下方式处理。
{ if (OnCommand(wParam, lParam)) //❸、使用OnCommand实现拐弯路由
{lResult = 1; goto LReturnTrue;}
return FALSE; }
……
if (message == WM_NOTIFY) //判断消息是否是通知消息WM_NOTIFY
{NMHDR* pNMHDR = (NMHDR*)lParam;
if (pNMHDR->hwndFrom != NULL && OnNotify(wParam, lParam, &lResult))goto LReturnTrue;
return FALSE; }
if (message == WM_ACTIVATE) //判断是否是WM_ACTIVATE消息
_AfxHandleActivate(this, wParam, CWnd::FromHandle((HWND)lParam));
if (message == WM_SETCURSOR &&
_AfxHandleSetCursor(this, (short)LOWORD(lParam), HIWORD(lParam)))
{lResult = 1;
goto LReturnTrue;}
// special case for windows that contain windowless ActiveX controls
BOOL bHandled;
bHandled = FALSE;
if ((m_pCtrlCont != NULL) && (m_pCtrlCont->m_nWindowlessControls > 0))
{ if (((message >= WM_MOUSEFIRST) && (message <= AFX_WM_MOUSELAST)) ||
((message >= WM_KEYFIRST) && (message <= WM_IME_KEYLAST)) ||
((message >= WM_IME_SETCONTEXT) && (message <= WM_IME_KEYUP)))
{ bHandled = m_pCtrlCont->HandleWindowlessMessage(message, wParam, lParam, &lResult); }
}
if (bHandled) { goto LReturnTrue; }
……
const AFX_MSGMAP* pMessageMap;
pMessageMap = GetMessageMap();
const AFX_MSGMAP_ENTRY* lpEntry;
UINT iHash; iHash = (LOWORD((DWORD_PTR)pMessageMap) ^ message) & (iHashMax-1);
winMsgLock.Lock(CRIT_WINMSGCACHE);
AFX_MSG_CACHE* pMsgCache; pMsgCache = &_afxMsgCache[iHash];
if (message == pMsgCache->nMsg && //检查消息是否处于cache之中
pMessageMap == pMsgCache->pMessageMap)
{ // cache hit
lpEntry = pMsgCache->lpEntry;
winMsgLock.Unlock();
if (lpEntry == NULL) return FALSE;
// cache hit, and it needs to be handled
if (message < 0xC000) goto LDispatch;
else goto LDispatchRegistered; }
else //❺、从此处开始实现消息的直线路由
{ pMsgCache->nMsg = message;
pMsgCache->pMessageMap = pMessageMap;
/*❻、for循环的原理与直线路由模仿程序示例3.10相同。但要注意的是在MFC之中,把消息映射(模仿示例中的ss,在MFC中为_messageEntries)和消息映射表(模仿示例中的msgmp,在MFC中为messageMap)封装在了一个名为GetThisMessageMap的函数之中,但其基本原理是一样的。*/
for (;pMessageMap->pfnGetBaseMap != NULL;
pMessageMap = (*pMessageMap->pfnGetBaseMap)() )
{ ASSERT(pMessageMap != (*pMessageMap->pfnGetBaseMap)());
if (message < 0xC000) /*小于0xC000的消息为一般消息,否则为用户注册消息。*/
{
/*❼、函数AfxFindMessageEntry的主要作用是用于循环查询消息映射(即_messageEntries)中的所有<消息,处理函数>对,该函数类似于模仿程序示例3.10中的while语句除去其中的swtich后的样子。其定义见后文。*/
if ((lpEntry = AfxFindMessageEntry(pMessageMap->lpEntries,
message, 0, 0)) != NULL)
{ pMsgCache->lpEntry = lpEntry;
winMsgLock.Unlock();
goto LDispatch; /*❽、LDispatch后的语句,主要用于比对应该调用共用体中的哪一个函数。类似于模仿程序示例3.10中的swtich语句。*/
} }
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);
if (*pnID == message)
{ pMsgCache->lpEntry = lpEntry;
winMsgLock.Unlock();
goto LDispatchRegistered;
}
lpEntry++; // keep looking past this one
}} } //for结束
pMsgCache->lpEntry = NULL;
winMsgLock.Unlock();
return FALSE; } //if结束
LDispatch:
ASSERT(message < 0xC000);
mmf.pfn = lpEntry->pfn; //❾、初始化共用体
switch (lpEntry->nSig) /*❿、根据消息处理函数原型的不同,而使用共用体中不同的成员间接调用消处理函数,以下调用的原则是,共用体mmf中指向函数的指针成员的原型必须与消息处理函数的原型相同。*/
{ default: ASSERT(FALSE); break;
case AfxSig_l_p: /*⓫、AfxSig_XXXX是枚举类型AfxSig中定义的枚举值,其定义见后文,该枚举是与MessageMapFunctions共用体相对应的。*/
{CPoint point(lParam);lResult=(this->*mmf.pfn_l_p)(point); break;}
case AfxSig_b_D_v:
lResult = (this->*mmf.pfn_b_D)(CDC::FromHandle(reinterpret_cast<HDC>(wParam)));break;
...... //此处大约有100多个类似的语句。
}
LDispatchRegistered: // for registered windows messages
ASSERT(message >= 0xC000);
ASSERT(sizeof(mmf) == sizeof(mmf.pfn));
mmf.pfn = lpEntry->pfn;
lResult = (this->*mmf.pfn_l_w_l)(wParam, lParam);
LReturnTrue:
if (pResult != NULL)
*pResult = lResult;
return TRUE; } //OnWndMsg结束
三、MFC源码实现拐弯路由(CWnd::OnCommand源码解析wincore.cpp)
1、拐弯路由牵扯到几个问题,第一是消息是从哪个类传入进去的,第二个是拐弯路由时的函数调用,第三个是继承结构问题,因此在讲解源码的消息拐弯路由时需作一些假设。
2、因为在CWnd::OnWndMsg中调用的OnCommand是个虚函数,因此究竟是调用的哪个类中的OnCommand虚函数,需要视this指针指向的类型而定。以下类都重写了该虚函数CWnd,CFrameWnd,CSplitterWnd,CPropertySheet,CMDIFrameWnd,COlePropertyPage
3、OnCommand函数并不会执行消息的匹配和查找工作,该函数的主要作用是实现消息的拐弯,消息的匹配和查找工作由另一个虚函数OnCmdMsg函数完成,该虚函数主要用于实现消息在继承子树中的直线路由,因此程序最终都会调用OnCmdMsg函数,以实现该类所在继承子树的直线路由,而该函数的最终实现代码(即直线路由的代码)位于CCmdTarget::OnCmdMsg之中,因此源码中的其他子类,最终都会调用CCmdTarget::OnCmdMsg函数。
4、以下类重写了OnCmdMsg虚函数:CCmdTarget, CFrameWnd, CView, CDocument, CMDIFrameWnd, CPropertySheet, CDialog, COleDocument,注意CWnd类没有重写该虚函数。
5、源码中的OnCommand相当于模仿程序示例3.11中的g1函数,而OncmdMsg相当于是g函数。
6、假设消息从CFrameWnd进入,即假设CWnd::OnWndMsg函数中的this指针指向的类型是CFrameWnd,则在其中调用的OnCommand,就是CFrameWnd::OnCommand。现在从此处开始列出所有有关的源代码。
BOOL CWnd::OnWndMsg(UINT message, WPARAM wParam, LPARAM lParam, LRESULT* pResult)
{ ......
if (message == WM_COMMAND)
{ if (OnCommand(wParam, lParam)) //❶、假设this指针指向的类型是CFrameWnd
......}
......}
BOOL CFrameWnd::OnCommand(WPARAM wParam, LPARAM lParam) //winfrm.cpp
{ ......
return CWnd::OnCommand(wParam, lParam); }
BOOL CWnd::OnCommand(WPARAM wParam, LPARAM lParam) (wincore.cpp)
{ ......
return OnCmdMsg(nID,nCode,NULL,NULL);} //❷、此处再次假设this指针指向的类型是CFrameWnd
/*❸、OnCmdMsg用于处理消息在继承子树中的直线路由,该函数的具体代码(此处指用于实现直线路由的代码)位于顶级父类CCmdTarget之中,因此其他各子类最终都会调用CCmdTarget::OnCmdMsg函数。*/
BOOL CFrameWnd::OnCmdMsg(UINT nID, int nCode, void* pExtra, //winfrm.cpp
AFX_CMDHANDLERINFO* pHandlerInfo)
{ CPushRoutingFrame push(this);
/*❹、 CFrameWnd类中,消息横向流动的三条路径,这三条路径最终都会调用CCmdTarget::OnCmdMsg函数,以便处理消息在各自的继承子树中的直线路由,因为分路径比较多,本示例只追踪部分分路径源码,其余分路径原理都是类似的,请读者自行追踪,但最终都会调用CCmdTarget::OnCmdMsg。
注意:CWnd未重写OnCmdMsg函数,因此CWnd::OnCmdMsg就是直接调用CCmdTarget::OnCmdMsg。*/
CView* pView = GetActiveView();
if (pView != NULL && pView->OnCmdMsg(nID, nCode, pExtra, pHandlerInfo)) //路径1
return TRUE;
if (CWnd::OnCmdMsg(nID,nCode,pExtra,pHandlerInfo))//路径2,直接调用CCmdTarget::OnCmdMsg
return TRUE;
CWinApp* pApp = AfxGetApp();
if (pApp != NULL && pApp->OnCmdMsg(nID, nCode, pExtra, pHandlerInfo)) //路径3
return TRUE;
return FALSE; }
//❺、CFrameWnd路径1:CView::OnCmdMsg源码(viewcore.cpp),本路径又产生两个分路径1---1和1---2
BOOL CView::OnCmdMsg(UINT nID, int nCode, void* pExtra, AFX_CMDHANDLERINFO* pHandlerInfo)
{ //分路径1---1,直接调用CCmdTarget::OnCmdMsg
if(CWnd::OnCmdMsg(nID,nCode,pExtra,pHandlerInfo))return TRUE;
if (m_pDocument != NULL){ CPushRoutingView push(this);
return m_pDocument->OnCmdMsg(nID,nCode,pExtra, pHandlerInfo);} //分路径1----2
return FALSE; }
//❻、CFrameWnd分路径1----2:CDocument::OnCmdMsg源码(doccore.cpp)
//本路径再次产生两个分路径1----2-----1和1----2------2
BOOL CDocument::OnCmdMsg(UINT nID,int nCode,void* pExtra,AFX_CMDHANDLERINFO* pHandlerInfo)
{ if (CCmdTarget::OnCmdMsg(nID, nCode, pExtra, pHandlerInfo)) //分路径1----2-----1
return TRUE;
if (m_pDocTemplate != NULL &&
m_pDocTemplate->OnCmdMsg(nID, nCode, pExtra, pHandlerInfo))//分路径1----2-----2
return TRUE;
return FALSE;}
//❼、CCmdTarget::OnCmdMsg源码,用于处理消息在各自的继承子树中的直线路由,该程序中的执行原理与CWnd::OnWndMsg是类似的,详见直线路由模仿程序示例3.10。
BOOL CCmdTarget::OnCmdMsg(UINT nID, int nCode, void* pExtra, //cmdtarg.cpp
AFX_CMDHANDLERINFO* pHandlerInfo)
{ ......
const AFX_MSGMAP* pMessageMap;
const AFX_MSGMAP_ENTRY* lpEntry;
......
if (nCode != CN_UPDATE_COMMAND_UI){ nMsg = HIWORD(nCode); nCode = LOWORD(nCode); }
// for backward compatibility HIWORD(nCode)==0 is WM_COMMAND
if (nMsg == 0) nMsg = WM_COMMAND;
// look through message map to see if it applies to us
//❽、以下for语句与MFC源码的直线路由代码的原理是相同的,详见直线路由模仿程序示例3.10。
for (pMessageMap = GetMessageMap(); pMessageMap->pfnGetBaseMap != NULL;
pMessageMap = (*pMessageMap->pfnGetBaseMap)())
{ // Note: catches BEGIN_MESSAGE_MAP(CMyClass, CMyClass)!
ASSERT(pMessageMap != (*pMessageMap->pfnGetBaseMap)());
/*❾、AfxFindMessageEntry函数的源码详见MFC源码直线路由,该函数的功能是用于查询消息映射中的所有<消息,处理函数>对。类似于模仿程序的while循环。*/
lpEntry = AfxFindMessageEntry(pMessageMap->lpEntries, nMsg, nCode, nID);
if (lpEntry != NULL)
{ ......
return _AfxDispatchCmdMsg( /*❿、此函数的功能就是MFC源码直线路由goto语句跳转到的类似swtich的功能,主要用于对比应该使用哪一个函数原型调用消息处理函数。*/
this, nID, nCode, lpEntry->pfn, pExtra, lpEntry->nSig, pHandlerInfo);
} }
return FALSE; } // not handled
AFX_STATIC BOOL AFXAPI _AfxDispatchCmdMsg(CCmdTarget* pTarget,UINT nID, int nCode, //cmdtarg.cpp
AFX_PMSG pfn, void* pExtra, UINT_PTR nSig, AFX_CMDHANDLERINFO* pHandlerInfo)
{ ENSURE_VALID(pTarget);
UNUSED(nCode); // unused in release builds
union MessageMapFunctions mmf;
mmf.pfn = pfn;
BOOL bResult = TRUE; // default is ok
if (pHandlerInfo != NULL)
{ pHandlerInfo->pTarget = pTarget; pHandlerInfo->pmf = mmf.pfn; return TRUE; }
switch (nSig)
{default: // illegal
ASSERT(FALSE);
return 0;
break;
case AfxSigCmd_v:
// normal command or control notification
ASSERT(CN_COMMAND == 0); // CN_COMMAND same as BN_CLICKED
ASSERT(pExtra == NULL);
(pTarget->*mmf.pfnCmd_v_v)();
break;
......} //一系列类似的case子句。
本文作者:黄邦勇帅(原名:黄勇)