转自 http://blog.csdn.net/sunjd2001/archive/2007/05/14/1609158.aspx
一. MFC 消息响应机制分析
---- MFC 是 Windows 下程序设计的最流行的一个类库,但是该类库比较庞杂,尤其是它的消息映射机制,更是涉及到很多低层的东西,我们在这里,对它的整个消息映射机制进行了系统的分析,可以帮助程序开发人员对 MFC 的消息映射机制有一个比较透彻的了解。
---- 关键词:面向对象 消息映射 MFC 程序设计
1 .引言
---- VC++ 的 MFC 类库实际上是 Windows 下 C++ 编程的一套最为流行的类库。 MFC 的框架结构大大方便了程序员的编程工作,但是为了更加有效、灵活的使用 MFC 编程,了解 MFC 的体系结构往往可以使编程工作事半功倍。它合理的封装了 WIN32 API 函数,并设计了一套方便的消息映射机制。但这套机制本身比较庞大和复杂,对它的分析和了解无疑有助于我们写出更为合理的高效的程序。这里我们简单的分析 MFC 的消息响应机制,以了解 MFC 是如何对 Windows 的消息加以封装,方便用户的开发。
2. SDK 下的消息机制实现
---- 这里简单的回顾一下 SDK 下我们是如何进行 Windows 的程序开发的。一般来说, Windows 的消息都是和线程相对应的。即 Windows 会把消息发送给和该消息相对应的线程。在 SDK 的模式下,程序是通过 GetMessage 函数从和某个线程相对应的消息队列里面把消息取出来并放到一个特殊的结构里面,一个消息的结构是一个如下的 STRUCTURE 。
typedef struct tagMSG {
HWND hwnd;
UINT message;
WPARAM wParam;
LPARAM lParam;
DWORD time;
POINT pt;
}MSG;
---- 其中 hwnd 表示和窗口过程相关的窗口的句柄, message 表示消息的 ID 号, wParam 和 lParam 表示和消息相关的参数, time 表示消息发送的时间, pt 表示消息发送时的鼠标的位置。
---- 然后 TranslateMessage 函数用来把虚键消息翻译成字符消息并放到响应的消息队列里面,最后 DispatchMessage 函数把消息分发到相关的窗口过程。然后窗口过程根据消息的类型对不同的消息进行相关的处理。在 SDK 编程过程中,用户需要在窗口过程中分析消息的类型和跟消息一起的参数的含义,做不同的处理,相对比较麻烦,而 MFC 把消息调用的过程给封装起来,使用户能够通过 ClassWizard 方便的使用和处理 Windows 的各种消息。
3 . MFC 的消息实现机制
---- 我们可以看到,在 MFC 的框架结构下,可以进行消息处理的类的头文件里面都会含有 DECLARE_MESSAGE_MAP() 宏 , 这里主要进行消息映射和消息处理函数的声明。可以进行消息处理的类的实现文件里一般都含有如下的结构。
BEGIN_MESSAGE_MAP(CInheritClass, CBaseClass)
//{{AFX_MSG_MAP(CInheritClass)
//}}AFX_MSG_MAP
END_MESSAGE_MAP()
---- 这里主要进行消息映射的实现和消息处理函数的实现。
---- 所有能够进行消息处理的类都是基于 CCmdTarget 类的,也就是说 CCmdTarget 类是所有可以进行消息处理类的父类。 CCmdTarget 类是 MFC 处理命令消息的基础和核心。
---- 同时 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
{
#ifdef _AFXDLL
const AFX_MSGMAP* (PASCAL* pfnGetBaseMap)();
#else
const AFX_MSGMAP* pBaseMap;
#endif
const AFX_MSGMAP_ENTRY* lpEntries;
};
其中 AFX_MSGMAP_ENTRY 结构包含了
一个消息的所有相关信息 , 其中
nMessage 为 Windows 消息的 ID 号
nCode 为控制消息的通知码
nID 为 Windows 控制消息的 ID
nLastID 表示如果是一个指定范围的消息被映射的话,
nLastID 用来表示它的范围。
nSig 表示消息的动作标识
AFX_PMSG pfn 它实际上是一个指向
和该消息相应的执行函数的指针。
---- 而 AFX_MSGMAP 主要作用是两个,一:用来得到基类的消息映射入口地址。二:得到本身的消息映射入口地址。
---- 实际上, MFC 把所有的消息一条条填入到 AFX_MSGMAP_ENTRY 结构中去,形成一个数组,该数组存放了所有的消息和与它们相关的参数。同时通过 AFX_MSGMAP 能得到该数组的首地址,同时得到基类的消息映射入口地址,这是为了当本身对该消息不响应的时候,就调用其基类的消息响应。
---- 现在我们来分析 MFC 是如何让窗口过程来处理消息的,实际上所有 MFC 的窗口类都通过钩子函数 _AfxCbtFilterHook 截获消息,并且在钩子函数 _AfxCbtFilterHook 中把窗口过程设定为 AfxWndProc 。原来的窗口过程保存在成员变量 m_pfnSuper 中。
---- 所以在 MFC 框架下,一般一个消息的处理过程是这样的。
函数 AfxWndProc 接收 Windows 操作系统发送的消息。
函数 AfxWndProc 调用函数 AfxCallWndProc 进行消息处理,这里一个进步是把对句柄的操作转换成对 CWnd 对象的操作。
函数 AfxCallWndProc 调用 CWnd 类的方法 WindowProc 进行消息处理。注意 AfxWndProc 和 AfxCallWndProc 都是 AFX 的 API 函数。而 WindowProc 已经是 CWnd 的一个方法。所以可以注意到在 WindowProc 中已经没有关于句柄或者是 CWnd 的参数了。
方法 WindowProc 调用方法 OnWndMsg 进行正式的消息处理,即把消息派送到相关的方法中去处理。消息是如何派送的呢?实际上在 CWnd 类中都保存了一个 AFX_MSGMAP 的结构,而在 AFX_MSGMAP 结构中保存有所有我们用 ClassWizard 生成的消息的数组的入口,我们把传给 OnWndMsg 的 message 和数组中的所有的 message 进行比较,找到匹配的那一个消息。实际上系统是通过函数 AfxFindMessageEntry 来实现的。找到了那个 message ,实际上我们就得到一个 AFX_MSGMAP_ENTRY 结构,而我们在上面已经提到 AFX_MSGMAP_ENTRY 保存了和该消息相关的所有信息,其中主要的是消息的动作标识和跟消息相关的执行函数。然后我们就可以根据消息的动作标识调用相关的执行函数,而这个执行函数实际上就是通过 ClassWizard 在类实现中定义的一个方法。这样就把消息的处理转化到类中的一个方法的实现上。举一个简单的例子,比如在 View 中对 WM_LButtonDown 消息的处理就转化成对如下一个方法的操作。
void CInheritView::OnLButtonDown
(UINT nFlags, CPoint point)
{
// TODO: Add your message
handler code here and/or call default
CView::OnLButtonDown(nFlags, point);
}
注意这里 CView::OnLButtonDown(nFlags, point) 实际上就是调用 CWnd 的 Default() 方法。 而 Default() 方法所做的工作就是调用 DefWindowProc 对消息进行处理。这实际上是调用原来的窗口过程进行缺省的消息处理。
如果 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( 表示键值 ) 由小写字母的值该为大写字母的值就实现了这个功能。
---- 继续上面的例子,根据我们对 MFC 消息机制的分析,我们很容易得到除了上面的方法,我们至少还可以在另外两个地方进行操作。
---- 一:在消息的处理方法里面即 OnChar 中,当然最后我们不再调用 CEdit::OnChar(nChar, nRepCnt, nFlags) ,而是直接调用 DefWindowProc(WM_CHAR,nChar,MAKELPARAM (nRepCnt,nFlags)) 。因为从我们上面的分析可以知道 CEdit::OnChar(nChar, nRepCnt, nFlags) 实际上也就是对 DefWindowProc 方法的调用。
---- 二:我们可以直接重载 DefWindowProc 方法 , 对 message 类型等于 WM_CHAR 的,直接修改 nChar 的值即可。
4 .小结
---- 通过对 MFC 类库的分析和了解,不仅能够使我们更好的使用 MFC 类库,同时,对于我们自己设计和实现框架和类,无疑也有相当大的帮助。
二. MFC 的消息映射机制
MFC 的设计者们在设计 MFC 时,紧紧把握一个目标,那就是尽可能使得 MFC 的代码要小,速度尽可能快。为了这个目标,他们使用了许多技巧,其中很多技巧体现在宏的运用上,实现 MFC 的消息映射的机制就是其中之一。
同 MFC 消息映射机制有关的宏有下面几个:
DECLARE_MESSAGE_MAP() 宏
BEGIN_MESSAGE_MAP(theClass, baseClass) 和 END_MESSAGE_MAP() 宏
弄懂 MFC 消息映射机制的最好办法是将找出一个具体的实例,将这些宏展开,并找出相关的数据结构。
DECLARE_MESSAGE_MAP()
DECLARE_MESSAGE_MAP() 宏的定义如下:
#define DECLARE_MESSAGE_MAP() /
private: /
static const AFX_MSGMAP_ENTRY _messageEntries[]; /
protected: /
static AFX_DATA const AFX_MSGMAP messageMap; /
virtual const AFX_MSGMAP* GetMessageMap() const; /
从上面的定义可以看出, DECLARE_MESSAGE_MAP() 作下面三件事:
定义一个长度不定的静态数组变量 _messageEntries[] ;
定义一个静态变量 messageMap ;
定义一个虚拟函数 GetMessageMap() ;
在 DECLARE_MESSAGE_MAP() 宏中,涉及到 MFC 中两个对外不公开的数据结构
AFX_MSGMAP_ENTRY 和 AFX_MSGMAP 。为了弄清楚消息映射,有必要考察一下这两个数据结构的定义。
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_ENTRY 结构实际上定义了消息和处理此消息的动作之间的映射关系。因此静态数组变量 _messageEntries[] 实际上定义了一张表,表中的每一项指定了相应的对象所要处理的消息和处理此消息的函数的对应关系,因而这张表也称为消息映射表。再看看 AFX_MSGMAP 的定义。
( 2 ) AFX_MSGMAP 的定义
struct AFX_MSGMAP
{
const AFX_MSGMAP* pBaseMap;
const AFX_MSGMAP_ENTRY* lpEntries;
};
不难看出, AFX_MSGMAP 定义了一单向链表,链表中每一项的值是一指向消息映射表的指针(实际上就是 _messageEntries 的值)。通过这个链表,使得在某个类中调用基类的的消息处理函数很容易,因此, “ 父类的消息处理函数是子类的缺省消息处理函数 ” 就 “ 顺理成章 ” 了。在后面的 “MFC 窗口的消息处理 ” 一节中会对此作详细的讲解。
由上述可见,在类的头文件中主要定义了两个数据结构:消息映射表和单向链表。 (孙建东总结)
BEGIN_MESSAGE_MAP() 和 END_MESSAGE_MAP()
它们的定义如下:
#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[] = /
{ /
#define END_MESSAGE_MAP() /
{0, 0, 0, 0, AfxSig_end, (AFX_PMSG)0 } /
}; /
对应 BEGIN_MESSAGE_MAP() 的定义可能不是一下子就看得明白,不过不要紧,举一例子就很清楚了。对于 BEGIN_MESSAGE_MAP(CView, CWnd) , VC 预编译器将其展开成下面的形式:
const AFX_MSGMAP* CView::GetMessageMap() const
{
return &CView::messageMap;
}
AFX_COMDAT AFX_DATADEF const AFX_MSGMAP CView::messageMap =
{
&CWnd::messageMap,
&CView::_messageEntries[0]
};
AFX_COMDAT const AFX_MSGMAP_ENTRY CView::_messageEntries[] =
{
至于 END_MESSAGE_MAP() 则不过定义了一个表示映射表结束的标志项,我想大家对于这种简单的技巧应该是很熟悉的,无需多述。
到此为止,我想大家也已经想到了象 ON_COMMAND 这样的宏的具体作用了,不错它们只不过定义了一种类型的消息映射项,看看 ON_COMMAND 的定义:
#define ON_COMMAND(id, memberFxn) /
{ WM_COMMAND, CN_COMMAND, (WORD)id, (WORD)id, AfxSig_vv, (AFX_PMSG)&memberFxn },
根据上面的定义, ON_COMMAND(ID_FILE_NEW, OnFileNew) 将被 VC 预编译器展开
如下:
{WM_COMMAND, CN_COMMAND, (WORD)id, (WORD)id, AfxSig_vv,
(AFX_PMSG)&OnFileNew},
到此, MFC 的消息映射机制已经清楚了,现在提出并解答两个问题以作为对这一节的小结。
为什么不直接使用虚拟函数实现消息处理函数呢?这是一个 GOOD QUESTION 。前面已经说过, MFC 的设计者们在设计 MFC 时有一个很明确的目标,就是使得 “MFC 的代码尽可能小,速度尽可能快 ” ,如果采用虚拟函数,那么对于所有的窗口消息,都必须有一个与之对应的虚拟函数,因而对每一个从 CWnd 派生的类而言,都会有一张很大的虚拟函数表 vtbl 。但是在实际应用中,一般只对少数的消息进行处理,大部分都交给系统缺省处理,所以表中的大部分项都是无用项,这样做就浪费了很多内存资源,这同 MFC 设计者们的设计目标是相违背的。当然, MFC 所使用的方法只是解决这类问题的方式之一,不排除还有其他的解决方式,但就我个人观点而言,这是一种最好的解决方式,体现了很高的技巧性,值得我们学习。
至于这第二个问题,是由上面的问题引申出来的。如果在子类和父类中出现了相同的消息出来函数, VC 编译器会怎么处理这个问题呢? VC 不会将它们看作错误,而会象对待虚拟函数类似的方式去处理,但对于消息处理函数 ( 带 afx_msg 前缀 ) ,则不会生成虚拟函数表 vtbl 。