消息映射初见
我们新建一个Draw单文档工程后,通过在DrawView类中添加一个“鼠标左键按下的消息”进行消息映射机制的分析,在分析之前,先
完成“WM_LButtonDown”消息响应函数的添加。
添加步骤:
1.在ClassView中找到DrawView类,点击鼠标右键,点击“属性”按钮,这时会弹出DrawView类的“属性配置界面”。
2.在“属性配置界面中”,点击“消息图标”->找到“WM_LButtonDown”->下拉选项->点击"添加OnLButtonDown",这时在工程中会自动增加三处代码。
(1)消息响应函数原型
在CDraw类的头文件中,能找到OnLButtonDown的函数声明,其中afx_msg是限定符,表明是消息响应函数。
class CDrawView : public CView { protected: // 仅从序列化创建 CDrawView(); DECLARE_DYNCREATE(CDrawView) ..... protected: // 生成的消息映射函数 protected: DECLARE_MESSAGE_MAP() public: afx_msg void OnLButtonDown(UINT nFlags, CPoint point);//新增增加代码 };
(2)ON_WM_LBUTTONDOWN消息映射宏
在DrawView.cpp的文件开头能找到ON_WM_LBUTTONDOWN这样的宏,代码如下:
BEGIN_MESSAGE_MAP(CDrawView, CView) // 标准打印命令 ON_COMMAND(ID_FILE_PRINT, &CView::OnFilePrint) ON_COMMAND(ID_FILE_PRINT_DIRECT, &CView::OnFilePrint) ON_COMMAND(ID_FILE_PRINT_PREVIEW, &CView::OnFilePrintPreview) ON_WM_LBUTTONDOWN()//新增加代码 END_MESSAGE_MAP()
(3)消息响应函数的定义
在CDrawView.cpp源文件中,可以看到OnLButtonDown函数的定义,如下代码段。
void CDrawView::OnLButtonDown(UINT nFlags, CPoint point) { // TODO: 在此添加消息处理程序代码和/或调用默认值 CView::OnLButtonDown(nFlags, point); }
经过以上分析,可以知道,一个MFC消息响应函数在程序中有三处相关信息:消息响应函数原型、消息响应函数定义以及用来关系
消息和消息响应函数的宏。
消息路由实现方式
实际上,消息路由可以有多重方式,其中一种可能的方式就是,在基类中针对每种消息定义一个虚函数。当子类需要对某个消息进
行处理,则重写基类相应的虚函数即可。根据多态性原理,如果子类重写了该函数,就调用子类的这个函数,否则调用父类的相应
函数,从原理是可行的,但从编程角度看是不可以取的。
为什么不可取呢?因为虚函数必须由一个“虚函数表”来实现。在应用程序中使用的每个派生类,系统都需要为它们分配一个
vtable(相关内容见“c++虚函数表解析”)。不管基类中虚函数释是否被重写,每个派生类都要背负一个很大的虚函数表。我们知道
MFC的派生层次很深,并且消息种类也很多,如果采用虚函数的实现方式,这是对内存资源的也很大的损耗,这么多的虚函数表,
查找对应的响应函数,效率也很低。因此MFC采用更加高级的“消息映射机制”。
消息映射机制的实现
在win32 SDK程序中,通过消息循环完成消息获取、通过窗口过程函数完成消息的响应,在窗口过程函数中通switch-case的方式,
完成对每种消息具体操作。在MFC程序中,只要添加如上三处有关消息的步骤之后,就可以实现消息的响应,这种机制称之为:
MFC消息映射机制。
MFC消息映射机制的具体实现方法是:在每个能接收和处理消息的类中,定义一个消息和消息函数静态对照表,即消息映射表,在
消息映射表中,消息和消息处理函数指针是成对出现的。某个类能处理的所有消息及其对应的消息处理函数的地址都列在这个类的
静态表中。当有消息需要处理时,程序只需要搜索该静态表,查看表中是否包含该消息,就可以知道该类能否处理该消息了。
BEGIN_MESSAGE_MAP(CDrawView, CView) // 标准打印命令 ON_COMMAND(ID_FILE_PRINT, &CView::OnFilePrint) ON_COMMAND(ID_FILE_PRINT_DIRECT, &CView::OnFilePrint) ON_COMMAND(ID_FILE_PRINT_PREVIEW, &CView::OnFilePrintPreview) ON_WM_LBUTTONDOWN() END_MESSAGE_MAP()
这段代码就形成了消息映射表的实现,现在分析其实现过程。微软通过宏的形式,使得我们消息映射编码更加的便捷,我们将这些
宏展开便可以见其原貌,宏展开内容如下:
//开始标志
#define BEGIN_MESSAGE_MAP(theClass, baseClass) PTM_WARNING_DISABLE const AFX_MSGMAP* theClass::GetMessageMap() const { return GetThisMessageMap(); } const AFX_MSGMAP* PASCAL theClass::GetThisMessageMap() { typedef theClass ThisClass; typedef baseClass TheBaseClass; static const AFX_MSGMAP_ENTRY _messageEntries[] = {//消息和消息响应函数映射宏,这些宏展开都是AFX_MSGMAP_ENTRY 结构,具体代码如下:
#define ON_COMMAND(id, memberFxn) { WM_COMMAND, CN_COMMAND, (WORD)id, (WORD)id, AfxSigCmd_v, static_cast<AFX_PMSG> (memberFxn) }, // ON_COMMAND(id, OnBar) is the same as // ON_CONTROL(0, id, OnBar) or ON_BN_CLICKED(0, id, OnBar) #define ON_COMMAND_RANGE(id, idLast, memberFxn) { WM_COMMAND, CN_COMMAND, (WORD)id, (WORD)idLast, AfxSigCmd_RANGE, (AFX_PMSG) (static_cast< void (AFX_MSG_CALL CCmdTarget::*)(UINT) > (memberFxn)) }, #define ON_WM_LBUTTONDOWN() { WM_LBUTTONDOWN, 0, 0, 0, AfxSig_vwp, (AFX_PMSG)(AFX_PMSGW) (static_cast< void (AFX_MSG_CALL CWnd::*)(UINT, CPoint) > ( &ThisClass :: OnLButtonDown)) }, struct AFX_MSGMAP_ENTRY//结束标志
#define END_MESSAGE_MAP() {0, 0, 0, 0, AfxSig_end, (AFX_PMSG)0 } }; static const AFX_MSGMAP messageMap = { &TheBaseClass::GetThisMessageMap, &_messageEntries[0] }; return &messageMap; }
其中static const AFX_MSGMAP_ENTRY _messageEntries[] ={}就是这个类的静态表,{0, 0, 0, 0, AfxSig_end, (AFX_PMSG)0 }这个是静态表的结束标志。ON_COMMAND(id, memberFxn)、ON_WM_LBUTTONDOWN()等展开后都是AFX_MSGMAP_ENTRY类型,都是_messageEntries的成员。因此想要添加其他的消息和消息响应函数只要按照三部曲“消息响应原型声明、消息响应函数定义、消息和消息响应函数义”操作即可完成MFC的消息映射。
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) }; typedef void (AFX_MSG_CALL CCmdTarget::*AFX_PMSG)(void);
消息响应函数会被统一转换成无参数、无返回值的AFX_PMSG类型储存起来。原有的返回值、参数信息通过标识符标唯一保存,即
nSig参数。而后通过标识符再被还原回去,可能的标识符由enum AfxSig给出,被定义在afxmsg_.h中。
消息路由过程
在WinCore.cpp文件中定义了一个WindowProc函数,该函数会调用一个OnWndMsg函数,这个函数完成了MFC的消息路由功能,即
消息映射,相关代码如下:
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; }
BOOL CWnd::OnWndMsg(UINT message, WPARAM wParam, LPARAM lParam, LRESULT* pResult) { LRESULT lResult = 0; union MessageMapFunctions mmf; mmf.pfn = 0; CInternalGlobalLock winMsgLock; // special case for commands if (message == WM_COMMAND) { if (OnCommand(wParam, lParam)) { lResult = 1; goto LReturnTrue; } return FALSE; } // special case for notifies if (message == WM_NOTIFY) { NMHDR* pNMHDR = (NMHDR*)lParam; if (pNMHDR->hwndFrom != NULL && OnNotify(wParam, lParam, &lResult)) goto LReturnTrue; return FALSE; } ... LDispatch: ASSERT(message < 0xC000); mmf.pfn = lpEntry->pfn; switch (lpEntry->nSig) { default: ASSERT(FALSE); break; case AfxSig_l_p://消息响应函数还原 { CPoint point(lParam); lResult = (this->*mmf.pfn_l_p)(point); break; }
OnWndMsg函数处理过程是:
1.判断消息是否有消息响应函数.判断方法是在相应的窗口类中查找所需要的消息响应函数,首先判断是否有相应的函数声明,其次
在BEGIN_MESSAGE_MAP和END_MESSAGE_MAP这个两个宏之间查找是否有相应的消息映射宏。
2.通过上述步骤,如果找到了这个消息响应函数,那么就调用该响应函数,对消息进行处理。
3.如果在子类中没有找到消息响应函数,那么就交个基类进行处理。
通过以上步骤,MFC就实现了具体的消息映射,完成对消息的响应。