一个完整的MFC消息映射包括对消息处理函数的原型声明、实现以及存在于消息映射中的消息入口。这几部分分别存在与类的头文件和实现文件中。一般情况下除了对自定义消息的响应外,对于标准Windows 消息的映射处理可以借助ClassWizard向导来完成。
在选定了待处理的Windows 消息后,向导将会根据消息的不同而生成具有相应函数参数和返回值的消息处理代码框架。下面这段代码给出了一个完成的MFC消息映射过程:
// 在.h文件中的声明 //{{AFX_MSG(CMessageMapView) afx_msg void On //}}AFX_MSG DECLARE_MESSAGE_MAP() …… // 在.cpp文件中的实现 BEGIN_MESSAGE_MAP(CMessageMapView, CView) //{{AFX_MSG_MAP(CMessageMapView) ON_WM_MOVE() //}}AFX_MSG_MAP END_MESSAGE_MAP() …… void CMessageMapView::On { CView::On // TODO: Add your message handler co } |
这里对Windows标准消息WM_MOVE做了消息映射,其中用到的BEGIN_MESSAGE_MAP、END_MESSAGE_MAP和头文件中的DECLARE_MESSAGE_MAP等均是用于消息映射的宏。这些宏声明了在应用程序框架中可用于在系统中浏览所有对象映射的成员变量和函数。除了以上三个比较常见的宏之外,MFC还提供了其他一些用于消息映射的宏,详情可参见下表:
一般作为基类使用的CWnd类为Windows消息定义了大量窗口消息的缺省处理函数,这些函数大部分只是简单地调用了Windows的缺省过程,可以在派生类中对其进行重载。但是MFC应用程序框架却并没有象使用普通虚函数那样使用Windows消息处理函数,而是通过宏将指定的消息映射到派生类的成员函数。如果MFC仍象普通虚函数一样对消息响应函数进行处理,那么CWnd类就要为这上百个消息声明虚函数。而C++将为在程序中使用的每一个派生类都提供一个被称作vtable的虚拟函数分配表,这个分配表需要为每一个虚函数提供一个4字节的入口,而不管这些函数在派生类中是否真正被重载,这将不能有效利用存储空间。而且对于每一个不同类型的窗口或控件,应用程序都要为其提供一个超过400字节的虚拟函数分配表来实现对消息的响应。而采用MFC的用宏将Windows消息映射到C++成员函数的方式则可避免产生庞大的虚拟函数分配表,其消耗的内存是同它所包含的消息入口数量成正比的。
消息映射的工作原理
前面给出了消息映射的一般形式,下面就对消息映射的工作原理做更深入的分析。任何使用了MFC应用程序框架的Windows程序都含有一个从CWinApp派生的应用程序类对象,成员函数Run()将被隐含调用,其调用的CWinThread类成员函数Run()将通过对GetMessage()、TranslateMessage()和DispatchMessage()等函数的调用完成同WinMain()类似的消息循环。在消息处理中,几乎所有的窗口对象都使用AfxWndProc()窗口处理函数,并通过一个包含了窗口句柄和对象指针等信息的列表而获取到一个指向对象的指针,由此可以调用CWnd的虚函数WindowProc()。WindowProc()函数调用了CWnd的另一个成员函数On
#ifdef _AFXDLL #define DECLARE_MESSAGE_MAP() \ private: \ static const AFX_MSGMAP_ENTRY _messageEntries[]; \ protected: \ static AFX_DA static const AFX_MSGMAP* PASCAL _GetBaseMessageMap(); \ virtual const AFX_MSGMAP* GetMessageMap() const; \ #else #define DECLARE_MESSAGE_MAP() \ private: \ static const AFX_MSGMAP_ENTRY _messageEntries[]; \ protected: \ static AFX_DA virtual const AFX_MSGMAP* GetMessageMap() const; \ #endif #ifdef _AFXDLL #define BEGIN_MESSAGE_MAP(theClass, baseClass) \ const AFX_MSGMAP* PASCAL theClass::_GetBaseMessageMap() \ { return &baseClass::messageMap; } \ const AFX_MSGMAP* theClass::GetMessageMap() const \ { return &theClass::messageMap; } \ AFX_COMDAT AFX_DATADEF const AFX_MSGMAP theClass::messageMap = \ { &theClass::_GetBaseMessageMap, &theClass::_messageEntries[0] }; \ AFX_COMDAT const AFX_MSGMAP_ENTRY theClass::_messageEntries[] = \ { \ #else #define BEGIN_MESSAGE_MAP(theClass, baseClass) \ const AFX_MSGMAP* theClass::GetMessageMap() const \ { return &theClass::messageMap; } \ AFX_COMDAT AFX_DATADEF const AFX_MSGMAP theClass::messageMap = \ { &baseClass::messageMap, &theClass::_messageEntries[0] }; \ AFX_COMDAT const AFX_MSGMAP_ENTRY theClass::_messageEntries[] = \ { \ #endif #define END_MESSAGE_MAP() \ {0, 0, 0, 0, AfxSig_end, (AFX_PMSG)0 } \ }; \ |
图1展示了消息映射处理的过程示意。搜寻过程是从CMainWindow的消息入口开始的,DECLARE_MESSAGE_MAP,BEGIN_MESSAGE_MAP和END_MESSAGE_MAP等消息映射宏通过搜索派生类消息映射的函数允许访问积累消息映射的入口。如果由CFrameWnd类派生的类CMainWindow没有捕获通常由CFrameWnd捕获的消息,那么消息将由相同的由派生类所继承的CFrameWnd类函数捕获。同样,如果CFrameWnd类仍没有捕获通常由其父类CWnd捕获的消息,则将继续上溯下去。这种消息映射的继承性与C++的继承是一致的。
另外,消息映射函数入口可以在在消息到达时为那些被隐含消息循环所调用的函数从中查看,并决定哪一个对象以及对象中的哪一个成员函数应该负责此消息的处理。虽然消息映射的内部工作原理比较复杂,但MFC通过预定义宏等手段将其完整的封装了起来,展现给开发人员的只是简单明了的MFC消息映射。
图1 消息映射处理过程示意
命令和通知
命令和通知实际都是一种特殊的消息类型。在SDK编程中,菜单和控件的动作均会产生一个WM_COMMAND命令消息,通过对消息参数wParam的区分可以识别出具体是哪个控件或菜单发出的命令。在MFC应用程序框架下,菜单和控件产生的消息将有所区分,选取菜单产生的消息被称作命令,而点击控件所产生的消息则被称作通知。由于命令和通知的本质仍是一种消息,因此在基本原理上仍是同消息一致的,即也是通过消息循环进入On
图2 命令/通知传递流程
这里CWnd::On
// 头文件 //{{AFX_MSG(CDIP_SystemView) afx_msg void On afx_msg void On //}}AFX_MSG DECLARE_MESSAGE_MAP() …… // 源文件 BEGIN_MESSAGE_MAP(CDIP_SystemView, CScrollView) /{{AFX_MSG_MAP(CDIP_SystemView) ON_COMMAND(IDM_EMBOSS, On ON_UPDATE_COMMAND_UI(IDM_EMBOSS, On //}}AFX_MSG_MAP END_MESSAGE_MAP() …… void CDIP_SystemView::On { return; } …… void CDIP_SystemView::On { pCmdUI->Enable(m_bCanUse); } |
这里ON_COMMAND宏将特定命令的处理同一个类成员函数建立了关联。而宏ON_UPDATE_COMMAND_UI则负责对命令的更新,即通过CCmdUI对象控制菜单/控件的是否可用或其他一些状态变化的更新。对命令的更新也可以将其理解为存在一个含有每个菜单入口的大表,各菜单入口含有菜单是否可用的标志。在显示菜单时通过快速检查该表而做出其所对应的每一个菜单项是否可用的决定。如果可用标志发生了变化,该表也将得到及时的更新。