MFC消息机制
使用MFC编程时,消息发送和处理的本质与Win相同,但是,它对消息处理进行了封装,简化了程序员编程时消息处理的复杂性,它通过消息映射机制来处理消息,程序员不必去设计和实现自己的窗口过程。
消息响应的方式:
基类中针对每种消息做一个虚函数,当子类对消息响应时候,只要在子类中重写这个虚函数即可。缺点:MFC类派生层次很多,如果在基类对每个消息进行虚函数处理,那么从基类派生的每个子类都将背负一个庞大的虚表,这样浪费内存,故MFC没有采取这种方式而采取消息映射方式。
MFC在后台维护了一个句柄和C++对象指针对照表,当收到一个消息后,通过消息结构里的资源句柄(查对照表)就可找到与它对应的一个C++对象指针,然后把这个指针传给基类,基类利用这个指针调用WindowProc()函数对消息进行处理,WindowProc()函数中调用OnWndMsg()函数,真正的消息路由及处理是由OnWndMsg()函数完成的。由于WindowProc()和OnWndMsg()都是虚函数,而且是用派生类对象指针调用的,由多态性知最总终调用子类的。在OnWndMsg()函数处理的时候,根据消息种类去查找消息映射,判断所发的消息有没有响应函数,具体方式是到相关的头文件和源文件中寻找消息响应函数声明(从注释宏//{{AFX_MSG(CDrawView)...//}}AFX_MSG之间寻找),消息映射(从宏BEGIN_MESSAGE_MAP(...)....END_MESSAGE_MAP()之间寻找),最终找到对应的消息处理函数。当然,如果子类中没有对消息进行处理,则消息交由基类处理。
一般作为基类使用的CWnd类为Windows消息定义了大量窗口消息的缺省处理函数,这些函数大部分只是简单地调用了Windows的缺省过程,可以在派生类中对其进行重载。但是MFC应用程序框架却并没有象使用普通虚函数那样使用Windows消息处理函数,而是通过宏将指定的消息映射到派生类的成员函数。如果MFC仍象普通虚函数一样对消息响应函数进行处理,那么CWnd类就要为这上百个消息声明虚函数。而C++将为在程序中使用的每一 个派生类都提供一个被称作vtable的虚拟函数分配表,这个分配表需要为每一个虚函数提供一个4字节的入口,而不管这些函数在派生类中是否真正被重载, 这将不能有效利用存储空间。而且对于每一个不同类型的窗口或控件,应用程序都要为其提供一个超过400字节的虚拟函数分配表来实现对消息的响应。而采用 MFC的用宏将Windows消息映射到C++成员函数的方式则可避免产生庞大的虚拟函数分配表,其消耗的内存是同它所包含的消息入口数量成正比的。
1 MFC消息映射
说白了,MFC中的消息映射机制实质是一张巨大的消息及其处理函数对应表。
消息映射基本上分为两大部分:
在头文件(.h)中有一个宏DECLARE_MESSAGE_MAP(),它放在类的末尾,这里主要进行消息映射和消息处理函数的声明,是一个public属性的;与之对应的是在实现部分(.cpp)增加了一个消息映射表,内容如下
BEGIN_MASSAGE_MAP(theClass,baseClass) //消息的入口项 : //}}AFX_MSG_MAP |
但是仅是这两项还不足以完成一条消息,一个完整的MFC消息映射包括对消息处理函数的原型声明、实现以及存在于消息映射中的消息入口。要是一个消息工作,必须还有以下3个部分去协作:
1、在类的定义中加入相应的函数声明;
2、在类的消息映射表中加入相应的消息映射入口项;
3、在类的实现中加入相应的函数体;
1.1 MFC消息映射宏(参考)
除了BEGIN_MESSAGE_MAP、END_MESSAGE_MAP 和头文件中的DECLARE_MESSAGE_MAP三个用于消息映射的宏,MFC还提供了其他一些用于消息映射的宏:
宏名 | 说明 |
DECLARE_MESSAGE_MAP | 在头文件声明源文件中所含有的消息映射 |
BEGIN_MESSAGE_MAP | 标记源文件消息映射的开始 |
END_MESSAGE_MAP | 标记源文件消息映射的结束 |
ON_COMMAND | 将特定命令的处理委派给类的一个成员函数 |
ON_COMMAND_RANGE | 把一定范围内的command IDs 映射到相应的函数上 |
ON_CONTROL | 映射一个函数到一个定制控制通知消息。其中,定制控制通知消息是从一个控制发送到其父窗口的消息。 |
ON_CONTROL_RANGE | 将一个控制ID的范围映射到一个消息处理函数 |
ON_CONTROL_REFLECT | 映射一个由父窗口反射回控制的通知消息 |
ON_MESSAGE | 将一个用户自定义消息映射到一消息处理函数 |
ON_NOTIFY | 映射一个控制消息到一个函数 |
ON_NOTIFY_RANGE | 映射一个控制ID范围内的控制消息到一个函数 |
ON_NOTIFY_EX | 映射一个控制消息到一个函数,该成员函数返回FALSE或TRUE来表明通知是否应被传送到下一个对象以进行其他反应。 |
ON_NOTIFY_EX_RANGE | 映射一个控制ID范围内的控制消息到一个函数,该成员函数返回FALSE或TRUE来表明通知是否应被传送到下一个对象以进行其他反应 |
ON_NOTIFY_REFLECT | 映射一个控制消息到一个函数。该消息将会被控制的父窗口反射回来。 |
ON_REGISTERED_MESSAGE | 映射一个唯一的消息到一个将要处理该注册消息的函数上。该消息是由RegisterWindowMessage()函数注册的。 |
ON_UPDATE_COMMAND_UI | 映射一个函数来处理一个用户接口更新命令消息 |
ON_UPDATE_COMMAND_UI_RANGE | 映射一个命令ID的范围到一个更新消息处理函数 |
这里主要讨论消息映射宏,常用的分为以下几类。
1.用于 Windows消息的宏,前缀为“ON_WM_”。
这样的宏不带参数,因为它对应的消息和消息处理函数的函数名称、函数原型是确定的。 MFC提供了这类消息处理函数的定义和缺省实现。每个这样的宏处理不同的Windows消息。
例如:宏 ON_WM_CREATE()把消息WM_CREATE映射到OnCreate函数,消息映射条目的第一个成员nMessage指定为要处理的Windows消息的ID,第二个成员nCode指定为0。
2.用于命令消息的宏 ON_COMMAND
这类宏带有参数,需要通过参数指定命令 ID和消息处理函数。这些消息都映射到WM_COMMAND上,也就是将消息映射条目的第一个成员nMessage指定为WM_COMMAND,第二个成员nCode指定为CN_COMMAND(即0)。消息处理函数的原型是void (void),不带参数,不返回值。
除了单条命令消息的映射,还有把一定范围的命令消息映射到一个消息处理函数的映射宏 ON_COMMAND_RANGE。这类宏带有参数,需要指定命令ID的范围和消息处理函数。这些消息都映射到WM_COMMAND上,也就是将消息映射条目的第一个成员nMessage指定为WM_COMMAND,第二个成员nCode指定为CN_COMMAND(即0),第三个成员nID和第四个成员nLastID指定了映射消息的起止范围。消息处理函数的原型是void (UINT),有一个UINT类型的参数,表示要处理的命令消息ID,不返回值。
3.用于控制通知消息的宏
这类宏可能带有三个参数,如 ON_CONTROL,就需要指定控制窗口ID,通知码和消息处理函数;也可能带有两个参数,如具体处理特定通知消息的宏ON_BN_CLICKED、ON_LBN_DBLCLK、ON_CBN_EDITCHANGE等,需要指定控制窗口ID和消息处理函数。
控制通知消息也被映射到 WM_COMMAND上,也就是将消息映射条目的第一个成员的nMessage指定为WM_COMMAND,但是第二个成员nCode是特定的通知码,第三个成员nID是控制子窗口的ID,第四个成员nLastID等于第三个成员的值。消息处理函数的原型是void (void),没有参数,不返回值。
还有一类宏处理通知消息 ON_NOTIFY,它类似于ON_CONTROL,但是控制通知消息被映射到WM_NOTIFY。消息映射条目的第一个成员的nMessage被指定为WM_NOTIFY,第二个成员nCode是特定的通知码,第三个成员nID是控制子窗口的ID,第四个成员nLastID等于第三个成员的值。消息处理函数的原型是void (NMHDR*, LRESULT*),参数1是NMHDR指针,参数2是LRESULT指针,用于返回结果,但函数不返回值。
对应地,还有把一定范围的控制子窗口的某个通知消息映射到一个消息处理函数的映射宏,这类宏包括 ON__CONTROL_RANGE和ON_NOTIFY_RANGE。这类宏带有参数,需要指定控制子窗口ID的范围和通知消息,以及消息处理函数。
对于 ON__CONTROL_RANGE,是将消息映射条目的第一个成员的nMessage指定为WM_COMMAND,但是第二个成员nCode是特定的通知码,第三个成员nID和第四个成员nLastID等于指定了控制窗口ID的范围。消息处理函数的原型是void (UINT),参数表示要处理的通知消息是哪个ID的控制子窗口发送的,函数不返回值。
对于 ON__NOTIFY_RANGE,消息映射条目的第一个成员的nMessage被指定为WM_NOTIFY,第二个成员nCode是特定的通知码,第三个成员nID和第四个成员nLastID指定了控制窗口ID的范围。消息处理函数的原型是void (UINT, NMHDR*, LRESULT*),参数1表示要处理的通知消息是哪个ID的控制子窗口发送的,参数2是NMHDR指针,参数3是LRESULT指针,用于返回结果,但函数不返回值。
4.用于用户界面接口状态更新的ON_UPDATE_COMMAND_UI宏
这类宏被映射到消息 WM_COMMND上,带有两个参数,需要指定用户接口对象ID和消息处理函数。消息映射条目的第一个成员nMessage被指定为WM_COMMAND,第二个成员nCode被指定为-1,第三个成员nID和第四个成员nLastID都指定为用户接口对象ID。消息处理函数的原型是 void (CCmdUI*),参数指向一个CCmdUI对象,不返回值。
对应地,有更新一定 ID范围的用户接口对象的宏ON_UPDATE_COMMAND_UI_RANGE,此宏带有三个参数,用于指定用户接口对象ID的范围和消息处理函数。消息映射条目的第一个成员nMessage被指定为WM_COMMAND,第二个成员nCode被指定为-1,第三个成员nID和第四个成员nLastID用于指定用户接口对象ID的范围。消息处理函数的原型是 void (CCmdUI*),参数指向一个CCmdUI对象,函数不返回值。之所以不用当前用户接口对象ID作为参数,是因为CCmdUI对象包含了有关信息。
5.用于其他消息的宏
例如用于用户定义消息的 ON_MESSAGE。这类宏带有参数,需要指定消息ID和消息处理函数。消息映射条目的第一个成员nMessage被指定为消息ID,第二个成员nCode被指定为0,第三个成员nID和第四个成员也是0。消息处理的原型是LRESULT (WPARAM, LPARAM),参数1和参数2是消息参数wParam和lParam,返回LRESULT类型的值。
2 MFC消息映射实现机制分析
第一个宏:DECLARE_MESSAGE_MAP()
作用:为一个消息响应类声明必需的成员变量和成员函数。
我们在窗口类、应用程序类、文档类、视图类、以及这些类的子类的定义中,都能看到DECLARE_MESSAGE_MAP()宏,通常被自动化工具声明在类的最后部分,如:
// 生成的消息映射函数
protected:
DECLARE_MESSAGE_MAP()
};
DECLARE_MESSAGE_MAP()
宏定义如下(在DLL类型和WINDOWS程序类型下,定义会有不同,本文只分析非DLL类型,下同):
#define DECLARE_MESSAGE_MAP()
private:
static const AFX_MSGMAP_ENTRY _messageEntries[];
protected:
static const AFX_MSGMAP messageMap;
virtual const AFX_MSGMAP* GetMessageMap() const;
可以看到,宏DECLARE_MESSAGE_MAP()定义了两个静态成员变量,并重载了一个虚函数。下面分析一下这三个成员:
_messageEntries被定义为一个AFX_MSGMAP_ENTRY类型的数组。结构体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_PTR nSig; // signature type (action) or pointer to message #
AFX_PMSG pfn; // routine to call (or special value)
};
通过查看源代码中的注释,可以看出AFX_MSGMAP_ENTRY定义了一个消息入口,或者说定义了一个消息到函数的映射关系。nMessage和nCode确定一条消息的内容,nID和nLastID确定了一条消息的来源,而nSig和pfn确定了消息的响应函数和调用方式。
通过对消息响应过程的源码分析可知,nSig事实上是一系列编码,每一种编码代表一种响应函数的类型,包括返回值、参数信息等。在响应消息的时候,将把pfn指向的函数指针强制类型转换为nSig代表的函数类型,然后再调用。
pfn的类型AFX_PMSG定义如下,意为CCmdTarget的成员函数:
typedef void (AFX_MSG_CALL CCmdTarget::*AFX_PMSG)(void);
由此我们可以得出:静态成员_messageEntries为是一个消息到函数的映射表,或叫消息入口表。通过查找此表,可以找到消息的响应函数。
DECLARE_MESSAGE_MAP()宏声明的另一个静态成员变量messageMap被定义为AFX_MSGMAP类型。AFX_MSGMAP定义如下:
struct AFX_MSGMAP
{
#ifdef _AFXDLL
const AFX_MSGMAP* (PASCAL* pfnGetBaseMap)();
#else
const AFX_MSGMAP* pBaseMap;
#endif
const AFX_MSGMAP_ENTRY* lpEntries;
};
过滤掉_AFXDLL(当MFC工程是以动态链接库为目标代码编译时,使用_AFXDLL宏)的影响,可以简化为如下:
struct AFX_MSGMAP
{
const AFX_MSGMAP* pBaseMap;
const AFX_MSGMAP_ENTRY* lpEntries;
};
可见结构体AFX_MSGMAP中定义了两个指针,pBaseMap指向另一个AFX_MSGMAP,lpEntries指向一个消息入口表。可以推想,在响应消息时,一定是在lpEntries指向的消息入口表中寻找响应函数,也可能会在pBaseMap指向的结构体中做同样的响应函数寻找操作。
至于DECLARE_MESSAGE_MAP()宏重载的虚函数GetMessageMap,可以猜测只是用来返回成员messageMap的地址而已。因为GetMessageMap是虚函数,所以系统只要通过调用消息响应类的基类CCmdTarget类的GetMessageMap函数,便可以找到最后一级子类的消息映射信息。我们将在接下来对其它几个宏的分析中得到相同的结论。
第二个宏:BEGIN_MESSAGE_MAP()
作用:定义DECLARE_MESSAGE_MAP宏声明的静态变量。
BEGIN_MESSAGE_MAP定义的源代码如下:
#define BEGIN_MESSAGE_MAP(theClass, baseClass)
const AFX_MSGMAP* theClass::GetMessageMap() const
{ return &theClass::messageMap; }
AFX_COMDAT const AFX_MSGMAP theClass::messageMap =
{ &baseClass::messageMap, &theClass::_messageEntries[0] };
AFX_COMDAT const AFX_MSGMAP_ENTRY theClass::_messageEntries[] =
{
BEGIN_MESSAGE_MAP宏有两个参数,theClass表示为当前类,bassClass为当前类的父类。
BEGIN_MESSAGE_MAP宏首先定义了函数GetMessageMap的函数体,如前文所述,直接返回当前类的成员变量messageMap的地址。
const AFX_MSGMAP* theClass::GetMessageMap() const
{ return &theClass::messageMap; }
然后初始化了当前类的成员变量messageMap。messageMap的pBaseMap指针指向其父类的messageMap成员,lpEntries指针指向当前类的_messageEntries数组的首地址。
AFX_COMDAT const AFX_MSGMAP theClass::messageMap =
{ &baseClass::messageMap, &theClass::_messageEntries[0] };
最后,定义了_messageEntries数组初始化代码的开始部分。
AFX_COMDAT const AFX_MSGMAP_ENTRY theClass::_messageEntries[] =
{
第三个宏:END_MESSAGE_MAP()
作用:定义_messageEntries数组初始化代码的结束部分。
#define END_MESSAGE_MAP()
{0, 0, 0, 0, AfxSig_end, (AFX_PMSG)0 }
};
在DECLARE_MESSAGE_MAP和END_MESSAGE_MAP之间还有一些宏,如ON_COMMAND、ON_WM_CREATE等,这些宏最终都会被生成一条AFX_MSGMAP_ENTRY结构体数据,并成为_messageEntries消息映射表数据的一个元素。我们以常见的ON_COMMAND宏为例。ON_COMMAND宏的源代码为:
#define ON_COMMAND(id, memberFxn)
{ WM_COMMAND, CN_COMMAND, (WORD)id, (WORD)id, AfxSigCmd_v,
static_cast<AFX_PMSG> (memberFxn) }
通过以上分析,我们可以得到一个链表式的数据结构,子类的messageMap成员为链表的头节点。链表的每个节点都包含一个消息入口表。MFC的消息系统的标准备消息处理函数CCmdTarget::OnCmdMsg正是通过这样一个链表查找到消息的响应函数,并调用该函数来响应消息。
MFC 调用关系:
AfxWndProc->AfxCallWndProc->WindowProc->OnWndMsg。
Qt消息机制
Windows消息:
消息循环:
消息处理:
Qt消息:
Qt消息机制相比于MFC,做了较好的封装,开发者无须关注比较底层的消息处理机制,使用较简单。
程序主体由 QApplication app(argc,argv)开始,按顺序初始化程序,在 return app.exec();处进入消息循环,app.exec()函数监听事件,有event()函数分发事件。
Qt的事件处理过程:QApplication的事件循环体(main event loop)从事件队列中读取本地窗口系统事件或其他事件,翻译成QEvent(),并传递给QObject::event(),最后分发给QWidget::event()分别处理具体的事件。