MFC消息响应机制分析
1 引言
微软公司提供的MFC基本类库(Microsoft Foundation Classes),是进行可视化编程时使用最为流行的一个类
库。MFC封装了大部分Windows API函数和Windows控件,使得程序的开发变得简单,极大的缩短了程序的开发
周期。MFC独创的Document/View框架结构,能够将管理数据的代码和显示数据的程序代码分开,并且设计了
一套方便的消息映射和命令传递机制,方便程序员的开发使用。其中消息映射机制本身比较庞大和复杂,对
它的分析和了解无疑有助于我们写出更为合理的高效的程序。这里我们分析一下MFC的消息映射机制,以了解
MFC是如何对Windows的消息加以封装,方便用户的开发。
2 SDK下的消息机制实现
首先,简单回顾一下SDK下我们是如何进行Windows的程序开发的。Windows程序的运行是依靠外部发生的事件
来驱动的,事件由操作系统捕捉,以消息的形式进入消息队列,然后通过消息循环从队列中不断取出消息,
送到对应的窗口过程里处理。相对于DOS程序,Windows是以WinMain作为程序的入口点,以下就是一个简化的
Win32程序的主体,通过while语句实现消息循环:
WinMain(…)
{
   MSG msg;
   RegisterClass(…);           // 注册窗口类
   CreateWindow(…);            // 创建窗口
   ShowWindow(…);              // 显示窗口
   UpdateWindow(…);
   While(GetMessage(&msg,…)){   // 消息循环
TranslateMessage(…);
DispatchMessage(…);
}
return msg.wParam;
}
其中,msg代表消息,程序是通过GetMessage函数从和某个线程相对应的消息队列里面把消息取出来并放到消
息变量msg里面。然后TranslateMessage函数用来把键盘消息转化并放到响应的消息队列里面,最后
DispatchMessage函数把消息分发到相关的窗口过程去处理。窗口过程根据消息的类型对不同的消息进行相关
的处理。在SDK编程过程中,用户需要在窗口过程中分析消息的类型及其参数的含义,然后做不同的处理,相
对比较麻烦;而MFC把消息调用的过程给封装起来,使用户能够通过ClassWizard方便的使用和处理Windows的
各种消息。
3 MFC中的消息映射机制
在MFC的框架结构下,“消息映射”是通过巧妙的宏定义,形成一张消息映射表格来进行的。这样一旦消息发
生,Framework就可以根据消息映射表格来进行消息映射和命令传递。
首先在需要进行消息处理的类的头文件(.H)里,都会含有DECLARE_MESSAGE_MAP()宏,声明该类拥有消息映
射表格:
class CscribbleDoc:public Cdocument
{
    …
DECLARE_MESSAGE_MAP()
};
然后在类应用程序文件(.CPP)实现这一表格
BEGIN_MESSAGE_MAP(CInheritClass, CBaseClass)
          //{{AFX_MSG_MAP(CInheritClass)
         ON_COMMAND(ID_EDIT_COPY,OnEditCopy)
         ………
          //}}AFX_MSG_MAP
END_MESSAGE_MAP()
----这里主要进行消息映射的实现,把它和消息处理函数联系在一起。其中出现三个宏,第一个宏是
BEGIN_MESSAGE_MAP有两个参数,分别是拥有消息表格的类,及其父类。第二个宏是ON_COMMAND,指定命令消
息的处理函数名称。第三个宏是END_MESSAGE_MAP()作为结尾符号。中间的奇怪符号//}}和//{{,是
ClassWizard产生的,对程序无影响。
观察DECLARE_MESSAGE_MAP的定义:
#define DECLARE_MESSAGE_MAP () \
private: \
static const AFX_MESSAGE_ENTRY _messageEntries[]; \
protected: \
static AFX_DATA const AFX_MSGMAP messageMap; \
virtual const AFX_MSGMAP* GetMessageMap() const; \
里面又包含了MFC新定义的两个数据结构,如下:
AFX_MSGMAP_ENTRY
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 nSig;       // signature type (action) or pointer to message #
          AFX_PMSG pfn;    // routine to call (or special value)
};
和AFX_MSGMAP
struct AFX_MSGMAP
{
          const AFX_MSGMAP* pBaseMap;
          const AFX_MSGMAP_ENTRY* lpEntries;
};
其中AFX_MSGMAP_ENTRY结构包含了一个消息的所有相关信息,而AFX_MSGMAP主要作用有两个,一是用来得到基
类的消息映射入口地址。二是得到本身的消息映射入口地址。
实际上,MFC把所有的消息一条条填入到AFX_MSGMAP_ENTRY结构中去,形成一个数组,该数组存放了所有的消
息和与它们相关的参数。同时通过AFX_MSGMAP能得到该数组的首地址,同时得到基类的消息映射入口地址。
当本身对该消息不响应的时候,就可以上溯到基类的消息映射表寻找对应的消息响应。
现在我们来分析MFC是如何让窗口过程来处理消息的,实际上所有MFC的窗口类都通过钩子函数
_AfxCbtFilterHook截获消息,并且在钩子函数_AfxCbtFilterHook中把窗口过程设定为AfxWndProc。原来的
窗口过程保存在成员变量m_pfnSuper中。
在MFC框架下,一般一个消息的处理过程是这样的。
(1)函数AfxWndProc接收Windows操作系统发送的消息。
(2)函数AfxWndProc调用函数AfxCallWndProc进行消息处理,这里一个进步是把对句柄的操作转换成对CWnd对
象的操作。
(3)函数AfxCallWndProc调用CWnd类的方法WindowProc进行消息处理。
(4)WindowProc调用OnWndMsg进行正式的消息处理,即把消息派送到相关的方法中去处理。在CWnd类中都保存
了一个AFX_MSGMAP的结构,而在AFX_MSGMAP结构中保存有所有我们用ClassWizard生成的消息的数组的入口,
我们把传给OnWndMsg的message和数组中的所有的message进行比较,找到匹配的那一个消息。实际上系统是
通过函数AfxFindMessageEntry来实现的。找到了那个message,实际上我们就得到一个AFX_MSGMAP_ENTRY结
构,而我们在上面已经提到AFX_MSGMAP_ENTRY保存了和该消息相关的所有信息,其中主要是消息的动作标识
和相关的执行函数。然后我们就可以根据消息的动作标识调用相关的执行函数,而这个执行函数实际上就是
通过ClassWizard在类实现中定义的一个方法。这样就把消息的处理转化到类中的一个方法的实现上。
(5)如果OnWndMsg方法没有对消息进行处理的话,就调用DefWindowProc对消息进行处理。这是实际上是调用
原来的窗口过程进行缺省的消息处理。 所以如果正常的消息处理的话,MFC窗口类是完全脱离了原来的窗口
过程,用自己的一套体系结构实现消息的映射和处理。即先调用MFC窗口类挂上去的窗口过程,再调用原先的
窗口过程。用户面对的消息参数将不再是固定的wParam和lParam,而是和消息类型具体相关的参数。比如和消
息WM_LButtonDown相对应的方法OnLButtonDown的两个参数是nFlags和point。nFlags表示在按下鼠标左键的
时候是否有其他虚拟键按下,point更简单,就是表示鼠标的位置。同时MFC窗口类消息传递中还提供了两个
函数,分别为WalkPreTranslateTree和PreTranslateMessage。我们知道利用MFC框架生成的程序,都是从
CWinApp开始执行的,而CWinapp实际继承了CWinThread类。在CWinThread的运行过程中会调用窗口类中的
WalkPreTranslateTree方法。而WalkPreTranslateTree方法实际上就是从当前窗口开始查找愿意进行消息翻
译的类,直到找到窗口没有父类为止。在WalkPreTranslateTree方法中调用了PreTranslateMessage方法。实
际上PreTranslateMessage最大的好处是我们在消息处理前可以在这个方法里面先做一些事情。举一个简单的
例子,比如我们希望在一个CEdit对象里,把所有的输入的字母都以大写的形式出现。我们只需要在
PreTranslateMessage方法中判断message是否为WM_CHAR,如果是的话,把wParam(表示键值)由小写字母的值
该为大写字母的值就实现了这个功能。
4 小结
MFC通过巧妙的宏定义把消息调用的过程给封装起来,使用户能够通过ClassWizard方便的使用和处理Windows
的各种消息。通过对MFC消息映射机制的分析,不仅能够使我们更好的使用MFC类库,同时,对于我们自己设
计程序框架和类,无疑也有相当大的帮助。