上篇已经描述了消息映射表的原理,下面主要说说MFC如何利用这张表来进行消息映射。
在MFC中,分为三种消息,三种消息的传递路线是不同的,可以参看一下代码:
BOOL CWnd::OnWndMsg(UINT message, WPARAM wParam, LPARAM lParam, LRESULT* pResult)
{
...
if (message == WM_COMMAND)
{
if (OnCommand(wParam, lParam));
}
...
if (message == WM_NOTIFY)
{
if (pNMHDR->hwndFrom != NULL && OnNotify(wParam, lParam, &lResult))
}
//以下是标准消息的处理
...
}
以上是摘自wincore.cpp中,OnWndMsg是用来分辨并处理消息的专职机构。
如果是命令消息,就交给OnCommand处理,如果是通知消息,就交给OnNotify处理。 标准消息则严格按照继承的路线走。
标准消息是按照消息映射表的线路来走的,举个例子:
CMyView->CView->CWnd->CCmdTarget,这是一个继承关系,标准消息按照这个路线,从子类走到父类,中途一旦搜索到有相应的消息处理函数,即处理。
而命令消息则要拐个弯,上面已经看到,如果是命令消息,就交给OnCommand处理。
因为命令消息只有CWnd及其派生类才能接收到,所以,这里的OnCommand不一定是CWnd的OnCommand,也有可能是其子类的OnCommand,根据多态性原理,如果其子类改写了OnCommand,则调用其子类的OnCommand。
在MFC中,以下数个类都改写了OnCommand虚函数:
CWnd;CFrameWnd;CMDIFrameWnd;CSplitterWnd;CPropertySheet;COlePropertyPage;
举个具体的示例吧,假设消息从CFrameWnd进来:
//in winfrm.cpp
BOOL CFrameWnd::OnCommand(WPARAM wParam, LPARAM lParam)
{
....
return CWnd::OnCommand(wParam, lParam);
}
很明显,调用了CWnd::OnCommand:
//in wincore.cpp
BOOL CWnd::OnCommand(WPARAM wParam, LPARAM lParam)
{
....
return OnCmdMsg(nID, nCode, NULL, NULL);
}
这里调用的OnCmdMsg不一定是CWnd的,根据多态性原理,此处很明显是调用了CFrameWnd::OnCmdMsg:
BOOL CFrameWnd::OnCmdMsg(UINT nID, int nCode, void* pExtra,AFX_CMDHANDLERINFO* pHandlerInfo)
{
....
// pump through current view FIRST
CView* pView = GetActiveView();
if (pView != NULL && pView->OnCmdMsg(nID, nCode, pExtra, pHandlerInfo))
return TRUE;
// then pump through frame
if (CWnd::OnCmdMsg(nID, nCode, pExtra, pHandlerInfo))
return TRUE;
// last but not least, pump through app
CWinApp* pApp = AfxGetApp();
if (pApp != NULL && pApp->OnCmdMsg(nID, nCode, pExtra, pHandlerInfo))
return TRUE;
}
在这里,有三个分支,很明显,传递的路线应该是:
View->Frame窗口本身->CWinApp对象。
我们再细化一下:
//in viewcore.cpp
BOOL CView::OnCmdMsg(UINT nID, int nCode, void* pExtra, AFX_CMDHANDLERINFO* pHandlerInfo)
{
// first pump through pane
if (CWnd::OnCmdMsg(nID, nCode, pExtra, pHandlerInfo))
return TRUE;
// then pump through document
if (m_pDocument != NULL)
{
// special state for saving view before routing to document
CPushRoutingView push(this);
return m_pDocument->OnCmdMsg(nID, nCode, pExtra, pHandlerInfo);
}
return FALSE;
}
很明显,到了view之后,先调用CWnd::OnCmdMsg,这里因为CWnd没有改写OnCmdMsg,其实就是调用了,CCmdTarget::OnCmdMsg:
//cmdtarg.cpp
BOOL CCmdTarget::OnCmdMsg(UINT nID, int nCode, void* pExtra,AFX_CMDHANDLERINFO* pHandlerInfo)
{
for (pMessageMap = GetMessageMap(); pMessageMap->pfnGetBaseMap != NULL;pMessageMap = (*pMessageMap->pfnGetBaseMap)())
{
....
lpEntry = AfxFindMessageEntry(pMessageMap->lpEntries, nMsg, nCode, nID);
if (lpEntry != NULL)
{
....
return _AfxDispatchCmdMsg(this, nID, nCode,lpEntry->pfn, pExtra, lpEntry->nSig, pHandlerInfo);
}
}
return FALSE;
}
可以看到,这个时候开始比较CCmdTarget的消息映射表,如果没有吻合者,则返回FALSE。好了,可以回到CView::OnCmdMsg中继续执行了,在CView中,下面是调用m_pDocument->OnCmdMsg:
BOOL CDocument::OnCmdMsg(UINT nID, int nCode, void* pExtra, AFX_CMDHANDLERINFO* pHandlerInfo)
{
if (CCmdTarget::OnCmdMsg(nID, nCode, pExtra, pHandlerInfo))
return TRUE;
// otherwise check template
if (m_pDocTemplate != NULL && m_pDocTemplate->OnCmdMsg(nID, nCode, pExtra, pHandlerInfo))
return TRUE;
return FALSE;
}
到了CDocument,也是先调用CCmdTarget::OnCmdMsg,如找到相应的消息函数,则传回FALSE。接下来调用m_pDocTemplate->OnCmdMsg,若未找到相应的处理函数,则返回FALSE。
好了,这时候,CView::OnCmdMsg返回FALSE,回到CFrameWnd::OnCmdMsg继续往下执行。
总结一下:
OnCmdMsg函数,是专门用来对付命令消息的函数,每一个“可接受命令消息的对象”在处理消息时,都应该调用另一个目标类的OnCmdMsg,这样才能将消息传递下去。
总的传递路径就是:
Frame窗口接收到消息->传递给view(未处理)->传递给document(未处理)->传递给document template(未处理)->返回给Frame窗口(未处理)->CWinApp对象(调用::DefWindowProc)
下面举一个具体的例子:
假设现在有一个sdi风格的程序,我们在菜单上选择了某项,ID为IDM_TEST,其相应程序在doc类下。消息产生了,路线如下:
1、CMyFrameWnd主窗口收到命令消息
2、CMyFrameWnd给它自己的子窗口CMyView一个机会,结果CView检查自己的messagemap,发现自己没有相应的消息处理函数。
3、CMyView只能将消息传递给CMyDocument,CMyDocument在自己的messagemap中发现了一个吻合项,于是调用响应函数,消息流动终止了。
4、假设CMyDocument没有这个消息的处理函数,则消息继续传递,CMyDocument会把消息传递到Document Template对象去。
5、若还是没被处理,则消息回到CMyView。
6、CMyView未处理,则又回到CMyFrameWnd主窗口。
7、CMyFrameWnd传递消息给CWinApp,最终调用::DefWindowProc处理无主消息。
到此为止,还有一个未弄明白的地方。那就是消息映射网中的_messageEntries[]的数组内容。为什么消息经由映射,可以找到它的处理函数呢?
上一章提到,messagemap中,每一笔记录是这样的:
struct AFX_MSGMAP_ENTRY
{
UINT nMessage; // windows message
UINT nCode; // control code or WM_NOTIFY code
UINT nID; // control ID (or 0 for windows messages)
UINT nLastID; // used for entries specifying a range of control id's
UINT_PTR nSig; // signature type (action) or pointer to message #
AFX_PMSG pfn; // routine to call (or special value)
};
而用用于实例化的ON_宏,是这样的:
#define ON_WM_CREATE() /
{ WM_CREATE, 0, 0, 0, AfxSig_is, /
(AFX_PMSG) (AFX_PMSGW) /
(static_cast< int (AFX_MSG_CALL CWnd::*)(LPCREATESTRUCT) > ( &ThisClass :: OnCreate)) },
用黑体字标示的是参数AFX_PMSG pfn,一个可怕的类型转换,这么处理时为了保证类型安全(typed-safe),实际上是一个指向消息响应函数的指针。
有一个很奇怪的东西, AfxSig,这个东西的作用是向消息响应函数中,传递参数。来看看它的实现。
以下代码可以在CWnd::OnWndMsg中找到(wincore.cpp)
union MessageMapFunctions mmf;//共用体类型
mmf.pfn = lpEntry->pfn;
switch (lpEntry->nSig)
{
...
case AfxSig_i_v_s:
lResult = (this->*mmf.pfn_i_s)(reinterpret_cast<LPTSTR>(lParam));
break;
...
}
注意:这里显示的是AfxSig_i_v_s而不是AfxSig_is,这是因为在vs2008中,AfxSig_is被视为旧的版本,MFC9.0中已经用AfxSig_i_v_s替代,可以在afxmsg_.h中找到以下代码:
// Old
...
AfxSig_is = AfxSig_i_v_s, // int (LPTSTR)
其中MessageMapFunctions的定义在afximpl.h中
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);
int (AFX_MSG_CALL CWnd::*pfn_i_s)(LPTSTR);
...
}
其实真正的函数只有一个pfn,但通过union,他拥有了许多不同的形象,如pfn_i_s代表了“返回值为int,参数为LPRSTR字符串”。相当繁琐的一个过程,但尤为精妙。
好了,最后总结一下:
1、MFC中消息分为命令消息、标准消息、控件通知消息。
2、消息传递是通过消息映射表格,此表格的声明利用BEGIN_MESSAGE_MAP/ON_**/END_MESSAGE_MAP三个宏。
3、标准消息的流动路线按照继承线路走。命令消息则是按照特殊路线走。