1、MFC 之消息映射的实现(1) 分类: Window 编程 2012-01-09 23:38 60 人阅读 评论(0) 收藏 举报 Windows 应用程序的输入由 Windows 系统以消息的形式发送给应用程序的窗口。这些窗口通过窗口过程来接收和处理消息,然后把控制返还给 Windows。 1.消息的分类1. 队列消息和非队列消息从消息的发送途径上看,消息分两种:队列消息和非队列消息。队列消息送到系统消息队列,然后到线程消息队列;非队列消息直接送给目的窗口过程。这里,对消息队列阐述如下:Windows 维护一个系统消息队列(System message queue),每个 GUI 线程有一个
2、线程消息队列(Thread message queue)。鼠标、键盘事件由鼠标或键盘驱动程序转换成输入消息并把消息放进系统消息队列,例如 WM_MOUSEMOVE、WM_LBUTTONUP 、WM_KEYDOWN 、WM_CHAR 等等。Windows 每次从系统消息队列移走一个消息,确定它是送给哪个窗口的和这个窗口是由哪个线程创建的,然后,把它放进窗口创建线程的线程消息队列。线程消息队列接收送给该线程所创建窗口的消息。线程从消息队列取出消息,通过 Windows 把它送给适当的窗口过程来处理。除了键盘、鼠标消息以外,队列消息还有 WM_PAINT、WM_TIMER 和WM_QUIT。这些队列
3、消息以外的绝大多数消息是非队列消息。2. 系统消息和应用程序消息从消息的来源来看,可以分为:系统定义的消息和应用程序定义的消息。系统消息 ID 的范围是从 0 到 WM_USER-1,或 0X80000 到 0XBFFFF;应用程序消息从 WM_USER(0X0400 )到 0X7FFF,或 0XC000 到 0XFFFF;WM_USER 到 0X7FFF 范围的消息由应用程序自己使用;0XC000 到 0XFFFF 范围的消息用来和其他应用程序通信,为了 ID 的唯一性,使用:RegisterWindowMessage 来得到该范围的消息 ID。2.消息结构和消息处理1. 消息的结构为了从消
4、息队列获取消息信息,需要使用 MSG 结构。例如,:GetMessage 函数(从消息队列得到消息并从队列中移走)和:PeekMessage 函数(从消息队列得到消息但是可以不移走)都使用了该结构来保存获得的消息信息。MSG 结构的定义如下:typedef struct tagMSG / msgHWND hwnd;UINT message;WPARAM wParam;LPARAM lParam;DWORD time;POINT pt;MSG;该结构包括了六个成员,用来描述消息的有关属性:接收消息的窗口句柄、消息标识(ID)、第一个消息参数、第二个消息参数、消息产生的时间、消息产生时鼠标的位置。
5、2. 应用程序通过窗口过程来处理消息如前所述,每个“窗口类”都要登记一个如下形式的窗口过程:LRESULT CALLBACK MainWndProc (HWND hwnd,/ 窗口句柄UINT msg,/ 消息标识WPARAM wParam,/消息参数 1LPARAM lParam/消息参数 2)应用程序通过窗口过程来处理消息:非队列消息由 Windows 直接送给目的窗口的窗口过程,队列消息由:DispatchMessage 等派发给目的窗口的窗口过程。窗口过程被调用时,接受四个参数:1.a window handle(窗口句柄);2.a message identifier(消息标识);3
6、.two 32-bit values called message parameters(两个 32 位的消息参数);需要的话,窗口过程用:GetMessageTime 获取消息产生的时间,用 :GetMessagePos 获取消息产生时鼠标光标所在的位置。在窗口过程里,用 switch/case 分支处理语句来识别和处理消息。3. 应用程序通过消息循环来获得对消息的处理每个 GDI 应用程序在主窗口创建之后,都会进入消息循环,接受用户输入、解释和处理消息。消息循环的结构如下:while (GetMessage(DispatchMessage( /发送消息消息循环从消息队列中得到消息,如果不是
7、快捷键消息或者对话框消息,就进行消息转换和派发,让目的窗口的窗口过程来处理。当得到消息 WM_QUIT,或者:GetMessage 出错时,退出消息循环。4. MFC 消息处理使用 MFC 框架编程时,消息发送和处理的本质也如上所述。但是,有一点需要强调的是,所有的 MFC 窗口都使用同一窗口过程,程序员不必去设计和实现自己的窗口过程,而是通过 MFC 提供的一套消息映射机制来处理消息。因此,MFC 简化了程序员编程时处理消息的复杂性。所谓消息映射,简单地讲,就是让程序员指定要某个 MFC 类(有消息处理能力的类)处理某个消息。MFC 提供了工具 ClassWizard 来帮助实现消息映射,在
8、处理消息的类中添加一些有关消息映射的内容和处理消息的成员函数。程序员将完成消息处理函数,实现所希望的消息处理能力。如果派生类要覆盖基类的消息处理函数,就用 ClassWizard 在派生类中添加一个消息映射条目,用同样的原型定义一个函数,然后实现该函数。这个函数覆盖派生类的任何基类的同名处理函数。3.消息结构和消息处理1. MFC 处理的三类消息根据处理函数和处理过程的不同,MFC 主要处理三类消息:* Windows 消息,前缀以“WM_”打头,WM_COMMAND 例外。Windows 消息直接送给MFC 窗口过程处理,窗口过程调用对应的消息处理函数。一般,由窗口对象来处理这类消息,也就是
9、说,这类消息处理函数一般是 MFC 窗口类的成员函数。* 控制通知消息,是控制子窗口送给父窗口的 WM_COMMAND 通知消息。窗口过程调用对应的消息处理函数。一般,由窗口对象来处理这类消息,也就是说,这类消息处理函数一般是 MFC 窗口类的成员函数。Win32 使用新的 WM_NOFITY 来处理复杂的通知消息。WM_COMMAND 类型的通知消息仅仅能传递一个控制窗口句柄(lparam)、控制窗 ID 和通知代码(wparam)。WM_NOTIFY 能传递任意复杂的信息。* 命令消息,这是来自菜单、工具条按钮、加速键等用户接口对象的 WM_COMMAND通知消息,属于应用程序自己定义的消
10、息。通过消息映射机制,MFC 框架把命令按一定的路径分发给多种类型的对象(具备消息处理能力)处理,如文档、窗口、应用程序、文档模板等对象。能处理消息映射的类必须从 CCmdTarget 类派生。2.MFC 消息映射的实现方法依靠以下几个宏1. DECLARE_MESSAGE_MAP 宏:#ifdef _AFXDLL#define DECLARE_MESSAGE_MAP() /private: /static const AFX_MSGMAP_ENTRY _messageEntries; /protected: /static AFX_DATA const AFX_MSGMAP messageM
11、ap; /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_DATA const AFX_MSGMAP messageMap; /virtual const AFX_MSGMAP* GetMessageMa
12、p() const; /#endifDECLARE_MESSAGE_MAP 定义了两个版本,分别用于静态或者动态链接到 MFC DLL的情形。2. BEGIN_MESSAE_MAP 宏#ifdef _AFXDLL#define BEGIN_MESSAGE_MAP(theClass, baseClass) /const AFX_MSGMAP* PASCAL theClass:_GetBaseMessageMap() / return /const AFX_MSGMAP* theClass:GetMessageMap() const / return /AFX_DATADEF const AFX_
13、MSGMAP theClass:messageMap = / /const AFX_MSGMAP_ENTRY theClass:_messageEntries = / /#else#define BEGIN_MESSAGE_MAP(theClass, baseClass) /const AFX_MSGMAP* theClass:GetMessageMap() const / return /AFX_DATADEF const AFX_MSGMAP theClass:messageMap = / /const AFX_MSGMAP_ENTRY theClass:_messageEntries =
14、 / /#endif#define END_MESSAGE_MAP() /0, 0, 0, 0, AfxSig_end, (AFX_PMSG)0 /; /对应地,BEGIN_MESSAGE_MAP 定义了两个版本,分别用于静态或者动态链接到 MFC DLL 的情形。END_MESSAGE_MAP 相对简单,就只有一种定义。3.END_MESSAGEP_MAP 宏 0,0,0,0.AfxSig_end,(AFX_PMSG)0 4. ON_COMMAND 宏最后,看 ON_COMMAND 宏的内容:#define ON_COMMAND(id, memberFxn) /WM_COMMAND,/CN_
15、COMMAND,/(WORD)id,/(WORD)id,/AfxSig_vv,/(AFX_PMSG)memberFxn/3.消息映射声明的解释在清楚了有关宏的定义之后,现在来分析它们的作用和功能。消息映射声明的实质是给所在类添加几个静态成员变量和静态或虚拟函数,当然它们是与消息映射相关的变量和函数。1. 成员变量有两个成员变量被添加,第一个是_messageEntries,第二个是 messageMap。AFX_MSGMAP_ENTRY _messageEntries这是一个 AFX_MSGMAP_ENTRY 类型的数组变量,是一个静态成员变量,用来容纳类的消息映射条目。一个消息映射条目可以用
16、 AFX_MSGMAP_ENTRY 结构来描述。AFX_MSGMAP_ENTRY 结构的定义如下:struct AFX_MSGMAP_ENTRY/Windows 消息 IDUINT nMessage;/控制消息的通知码UINT nCode;/Windows Control 的 IDUINT nID;/如果是一定范围的消息被映射,则 nLastID 指定其范围UINT nLastID;UINT nSig;/消息的动作标识/响应消息时应执行的函数(routine to call (or special value)AFX_PMSG pfn;从上述结构可以看出,每条映射有两部分的内容:第一部分是关于
17、消息 ID 的,包括前四个域;第二部分是关于消息对应的执行函数,包括后两个域。在上述结构的六个域中,pfn 是一个指向 CCmdTarger 成员函数的指针。函数指针的类型定义如下:typedef void (AFX_MSG_CALL CCmdTarget:*AFX_PMSG)(void);当使用一条或者多条消息映射条目初始化消息映射数组时,各种不同类型的消息函数都被转换成这样的类型:不接收参数,也不返回参数的类型。因为所有可以有消息映射的类都是从 CCmdTarge 派生的,所以可以实现这样的转换。nSig 是一个标识变量,用来标识不同原型的消息处理函数,每一个不同原型的消息处理函数对应一个
18、不同的 nSig。在消息分发时,MFC 内部根据 nSig 把消息派发给对应的成员函数处理,实际上,就是根据 nSig 的值把 pfn 还原成相应类型的消息处理函数并执行它。第二个变量 AFX_MSGMAP messageMap;这是一个 AFX_MSGMAP 类型的静态成员变量,从其类型名称和变量名称可以猜出,它是一个包含了消息映射信息的变量。的确,它把消息映射的信息(消息映射数组)和相关函数打包在一起,也就是说,得到了一个消息处理类的该变量,就得到了它全部的消息映射数据和功能。AFX_MSGMAP 结构的定义如下:struct AFX_MSGMAP/得到基类的消息映射入口地址的数据或者函数
19、#ifdef _AFXDLL/pfnGetBaseMap 指向_GetBaseMessageMap 函数const AFX_MSGMAP* (PASCAL* pfnGetBaseMap)();#else/pBaseMap 保存基类消息映射入口_messageEntries 的地址const AFX_MSGMAP* pBaseMap;#endif/lpEntries 保存消息映射入口_messageEntries 的地址const AFX_MSGMAP_ENTRY* lpEntries;从上面的定义可以看出,通过 messageMap 可以得到类的消息映射数组_messageEntries 和函
20、数_GetBaseMessageMap 的地址(不使用 MFC DLL 时,是基类消息映射数组的地址)。2. 成员函数_GetBaseMessageMap() 用来得到基类消息映射的函数。GetMessageMap() 用来得到自身消息映射的函数。4.消息映射实现的解释消息映射实现的实质是初始化声明中定义的静态成员函数_messageEntries 和messageMap,实现所声明的静态或虚拟函数GetMessageMap、_GetBaseMessageMap。这样,在进入 WinMain 函数之前,每个可以响应消息的 MFC 类都生成了一个消息映射表,程序运行时通过查询该表判断是否需要响应
21、某条消息。1. 对消息映射入口表( 消息映射数组) 的初始化如前所述,消息映射数组的元素是消息映射条目,条目的格式符合结构AFX_MESSAGE_ENTRY 的描述。所以,要初始化消息映射数组,就必须使用符合该格式的数据来填充:如果指定当前类处理某个消息,则把和该消息有关的信息(四个)和消息处理函数的地址及原型组合成为一个消息映射条目,加入到消息映射数组中。显然,这是一个繁琐的工作。为了简化操作,MFC 根据消息的不同和消息处理方式的不同,把消息映射划分成若干类别,每一类的消息映射至少有一个共性:消息处理函数的原型相同。对每一类消息映射,MFC 定义了一个宏来简化初始化消息数组的工作。例如,前
22、文提到的 ON_COMMAND 宏用来映射命令消息,只要指定命令 ID 和消息处理函数即可,因为对这类命令消息映射条目,其他四个属性都是固定的。 ON_COMMAND 宏的初始化内容如下:WM_COMMAND,CN_COMMAND,(WORD)ID_APP_ABOUT,(WORD)ID_APP_ABOUT,AfxSig_vv,(AFX_PMSG)OnAppAbout这个消息映射条目的含义是:消息 ID 是 ID_APP_ABOUT,OnAppAbout 被转换成AFX_PMSG 指针类型,AfxSig_vv 是 MFC 预定义的枚举变量,用来标识 OnAppAbout 的函数类型为参数空(Vo
23、id)、返回空(Void)。在消息映射数组的最后,是宏 END_MESSAGE_MAP 的内容,它标识消息处理类的消息映射条目的终止。2. 对 messageMap 的初始化如前所述,messageMap 的类型是 AFX_MESSMAP。经过初始化,域 lpEntries 保存了消息映射数组_messageEntries 的地址;如果动态链接到 MFC DLL,则 pfnGetBaseMap保存了_GetBaseMessageMap 成员函数的地址;否则 pBaseMap 保存了基类的消息映射数组的地址。3. 对函数的实现_GetBaseMessageMap()它返回基类的成员变量 mess
24、agMap(当使用 MFC DLL 时),使用该函数得到基类消息映射入口表。GetMessageMap():它返回成员变量 messageMap,使用该函数得到自身消息映射入口表。顺便说一下,消息映射类的基类 CCmdTarget 也实现了上述和消息映射相关的函数,不过,它的消息映射数组是空的。5.消息映射宏的种类为了简化程序员的工作,MFC 定义了一系列的消息映射宏和像 AfxSig_vv 这样的枚举变量,以及标准消息处理函数,并且具体地实现这些函数。这里主要讨论消息映射宏,常用的分为以下几类。1.用于 Windows 消息的宏,前缀为 “ON_WM_”。这样的宏不带参数,因为它对应的消息和
25、消息处理函数的函数名称、函数原型是确定的。MFC 提供了这类消息处理函数的定义和缺省实现。每个这样的宏处理不同的Windows 消息。例如:宏 ON_WM_CREATE()把消息 WM_CREATE 映射到 OnCreate 函数,消息映射条目的第一个成员 nMessage 指定为要处理的 Windows 消息的 ID,第二个成员 nCode 指定为 0。2.用于命令消息的宏 ON_COMMAND这类宏带有参数,需要通过参数指定命令 ID 和消息处理函数。这些消息都映射到WM_COMMAND 上,也就是将消息映射条目的第一个成员 nMessage 指定为WM_COMMAND,第二个成员 nCo
26、de 指定为 CN_COMMAND(即 0)。消息处理函数的原型是 void (void),不带参数,不返回值。除了单条命令消息的映射,还有把一定范围的命令消息映射到一个消息处理函数的映射宏 ON_COMMAND_RANGE。这类宏带有参数,需要指定命令 ID 的范围和消息处理函数。这些消息都映射到 WM_COMMAND 上,也就是将消息映射条目的第一个成员nMessage 指定为 WM_COMMAND,第二个成员 nCode 指定为 CN_COMMAND(即 0),第三个成员 nID 和第四个成员 nLastID 指定了映射消息的起止范围。消息处理函数的原型是 void (UINT),有一个
27、 UINT 类型的参数,表示要处理的命令消息 ID,不返回值。3.用于控制通知消息的宏这类宏可能带有三个参数,如 ON_CONTROL,就需要指定控制窗口 ID,通知码和消息处理函数;也可能带有两个参数,如具体处理特定通知消息的宏ON_BN_CLICKED、ON_LBN_DBLCLK 、ON_CBN_EDITCHANGE 等,需要指定控制窗口 ID 和消息处理函数。控制通知消息也被映射到 WM_COMMAND 上,也就是将消息映射条目的第一个成员的 nMessage 指定为 WM_COMMAND,但是第二个成员 nCode 是特定的通知码,第三个成员 nID 是控制子窗口的 ID,第四个成员
28、nLastID 等于第三个成员的值。消息处理函数的原型是 void (void),没有参数,不返回值。还有一类宏处理通知消息 ON_NOTIFY,它类似于 ON_CONTROL,但是控制通知消息被映射到 WM_NOTIFY。消息映射条目的第一个成员的 nMessage 被指定为WM_NOTIFY,第二个成员 nCode 是特定的通知码,第三个成员 nID 是控制子窗口的ID,第四个成员 nLastID 等于第三个成员的值。消息处理函数的原型是 void (NMHDR*, LRESULT*),参数 1 是 NMHDR 指针,参数 2 是 LRESULT 指针,用于返回结果,但函数不返回值。对应地
29、,还有把一定范围的控制子窗口的某个通知消息映射到一个消息处理函数的映射宏,这类宏包括 ON_CONTROL_RANGE 和 ON_NOTIFY_RANGE。这类宏带有参数,需要指定控制子窗口 ID 的范围和通知消息,以及消息处理函数。对于 ON_CONTROL_RANGE,是将消息映射条目的第一个成员的 nMessage 指定为WM_COMMAND,但是第二个成员 nCode 是特定的通知码,第三个成员 nID 和第四个成员 nLastID 等于指定了控制窗口 ID 的范围。消息处理函数的原型是 void (UINT),参数表示要处理的通知消息是哪个 ID 的控制子窗口发送的,函数不返回值。对
30、于 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 上,带有两个参
31、数,需要指定用户接口对象 ID和消息处理函数。消息映射条目的第一个成员 nMessage 被指定为 WM_COMMAND,第二个成员 nCode 被指定为-1 ,第三个成员 nID 和第四个成员 nLastID 都指定为用户接口对象 ID。消息处理函数的原型是 void (CCmdUI*),参数指向一个 CCmdUI 对象,不返回值。对应地,有更新一定 ID 范围的用户接口对象的宏ON_UPDATE_COMMAND_UI_RANGE,此宏带有三个参数,用于指定用户接口对象 ID的范围和消息处理函数。消息映射条目的第一个成员 nMessage 被指定为WM_COMMAND,第二个成员 nCode
32、 被指定为-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类型的值。6.扩展消息映射宏很多普通消息映射宏都有对应的扩展消息映射宏,例如:ON_COMMAND 对应的ON_COMMAND_EX,ON_ONTIFY 对应的 ON_ONTIFY_EX,等等。扩展宏除了具有普通宏的功能,还有特别的用途。关于扩展宏的具体讨论和分析,见 4.4.3.2 节。作为一个总结,下表列出了这些常用的消息映射宏。