本节对CCmdTarget的两个成员函数作一些讨论,是为了对MFC的消息处理有一个大致印象。后面4.4.3.2节和4.4.3.3节将作进一步的讨论。

       

    1. MFC窗口过程

       

      前文曾经提到,所有的消息都送给窗口过程处理,MFC的所有窗口都使用同一窗口过程,消息或者直接由窗口过程调用相应的消息处理函数处理,或者按MFC命令消息派发路径送给指定的命令目标处理。

      那么,MFC的窗口过程是什么?怎么处理标准Windows消息?怎么实现命令消息的派发?这些都将是下文要回答的问题。

         

      1. MFC窗口过程的指定

         

        从前面的讨论可知,每一个“窗口类”都有自己的窗口过程。正常情况下使用该“窗口类”创建的窗口都使用它的窗口过程。

        MFC的窗口对象在创建HWND窗口时,也使用了已经注册的“窗口类”,这些“窗口类”或者使用应用程序提供的窗口过程,或者使用Windows提供的窗口过程(例如Windows控制窗口、对话框等)。那么,为什么说MFC创建的所有HWND窗口使用同一个窗口过程呢?

        在MFC中,的确所有的窗口都使用同一个窗口过程:AfxWndProc或AfxWndProcBase(如果定义了_AFXDLL)。它们的原型如下:

        LRESULT CALLBACK

        AfxWndProc(HWND hWnd, UINT nMsg, WPARAM wParam, LPARAM lParam)

         

        LRESULT CALLBACK

        AfxWndProcBase(HWND hWnd, UINT nMsg, WPARAM wParam, LPARAM lParam)

        这两个函数的原型都如4.1.1节描述的窗口过程一样。

        如果动态链接到MFC DLL(定义了_AFXDLL),则AfxWndProcBase被用作窗口过程,否则AfxWndProc被用作窗口过程。AfxWndProcBase首先使用宏AFX_MANAGE_STATE设置正确的模块状态,然后调用AfxWndProc。

         

        下面,假设不使用MFC DLL,讨论MFC如何使用AfxWndProc取代各个窗口的原窗口过程。

        窗口过程的取代发生在窗口创建的过程时,使用了子类化(Subclass)的方法。所以,从窗口的创建过程来考察取代过程。从前面可以知道,窗口创建最终是通过调用CWnd::CreateEx函数完成的,分析该函数的流程。

         

        CREATESTRUCT结构类型的变量cs包含了传递给窗口过程的初始化参数。CREATESTRUCT结构描述了创建窗口所需要的信息,定义如下:

        typedef struct tagCREATESTRUCT {

        LPVOID lpCreateParams; //用来创建窗口的数据

        HANDLE hInstance; //创建窗口的实例

        HMENU hMenu; //窗口菜单

        HWND hwndParent; //父窗口

        int cy; //高度

        int cx; //宽度

        int y; //原点Y坐标

        int x;//原点X坐标

        LONG style; //窗口风格

        LPCSTR lpszName; //窗口名

        LPCSTR lpszClass; //窗口类

        DWORD dwExStyle; //窗口扩展风格

        } CREATESTRUCT;

        cs表示的创建参数可以在创建窗口之前被程序员修改,程序员可以覆盖当前窗口类的虚拟成员函数PreCreateWindow,通过该函数来修改cs的style域,改变窗口风格。这里cs的主要作用是保存创建窗口的各种信息,::CreateWindowEx函数使用cs的各个域作为参数来创建窗口,关于该函数见2.2.2节。

        在创建窗口之前,创建了一个WH_CBT类型的钩子(Hook)。这样,创建窗口时所有的消息都会被钩子过程函数_AfxCbtFilterHook截获。

        AfxCbtFilterHook函数首先检查是不是希望处理的Hook──HCBT_CREATEWND。如果是,则先把MFC窗口对象(该对象必须已经创建了)和刚刚创建的Windows窗口对象捆绑在一起,建立它们之间的映射(见后面模块-线程状态);然后,调用::SetWindowLong设置窗口过程为AfxWndProc,并保存原窗口过程在窗口类成员变量m_pfnSuper中,这样形成一个窗口过程链。需要的时候,原窗口过程地址可以通过窗口类成员函数GetSuperWndProcAddr得到。

        这样,AfxWndProc就成为CWnd或其派生类的窗口过程。不论队列消息,还是非队列消息,都送到AfxWndProc窗口过程来处理(如果使用MFC DLL,则AfxWndProcBase被调用,然后是AfxWndProc)。经过消息分发之后没有被处理的消息,将送给原窗口过程处理。

        最后,有一点可能需要解释:为什么不直接指定窗口过程为AfxWndProc,而要这么大费周折呢?这是因为原窗口过程(“窗口类”指定的窗口过程)常常是必要的,是不可缺少的。

        接下来,讨论AfxWndProc窗口过程如何使用消息映射数据实现消息映射。Windows消息和命令消息的处理不一样,前者没有消息分发的过程。

         

      2. 对Windows消息的接收和处理

         

        Windows消息送给AfxWndProc窗口过程之后,AfxWndProc得到HWND窗口对应的MFC窗口对象,然后,搜索该MFC窗口对象和其基类的消息映射数组,判定它们是否处理当前消息,如果是则调用对应的消息处理函数,否则,进行缺省处理。

        下面,以一个应用程序的视窗口创建时,对WM_CREATE消息的处理为例,详细地讨论Windows消息的分发过程。

        用第一章的例子,类CTview要处理WM_CREATE消息,使用ClassWizard加入消息处理函数CTview::OnCreate。下面,看这个函数怎么被调用:

        视窗口最终调用::CreateEx函数来创建。由Windows系统发送WM_CREATE消息给视的窗口过程AfxWndProc,参数1是创建的视窗口的句柄,参数2是消息ID(WM_CREATE),参数3、4是消息参数。图4-2描述了其余的处理过程。图中函数的类属限制并非源码中所具有的,而是根据处理过程得出的判断。例如,“CWnd::WindowProc”表示CWnd类的虚拟函数WindowProc被调用,并不一定当前对象是CWnd类的实例,事实上,它是CWnd派生类CTview类的实例;而“CTview::OnCreate”表示CTview的消息处理函数OnCreate被调用。下面描述每一步的详细处理。

         

           

        1. 从窗口过程到消息映射

           

首先,分析AfxWndProc窗口过程函数。

     

  • AfxWndProc的原型如下:

     

LRESULT AfxWndProc(HWND hWnd,

UINT nMsg, WPARAM wParam, LPARAM lParam)

如果收到的消息nMsg不是WM_QUERYAFXWNDPROC(该消息被MFC内部用来确认窗口过程是否使用AfxWndProc),则从hWnd得到对应的MFC Windows对象(该对象必须已存在,是永久性<Permanent>对象)指针pWnd。pWnd所指的MFC窗口对象将负责完成消息的处理。这里,pWnd所指示的对象是MFC视窗口对象,即CTview对象。

然后,把pWnd和AfxWndProc接受的四个参数传递给函数AfxCallWndProc执行。

 

     

  • AfxCallWndProc原型如下:

     

LRESULT AFXAPI AfxCallWndProc(CWnd* pWnd, HWND hWnd,

UINT nMsg, WPARAM wParam = 0, LPARAM lParam = 0)

MFC使用AfxCallWndProc函数把消息送给CWnd类或其派生类的对象。该函数主要是把消息和消息参数(nMsg、wParam、lParam)传递给MFC窗口对象的成员函数WindowProc(pWnd->WindowProc)作进一步处理。如果是WM_INITDIALOG消息,则在调用WindowProc前后要作一些处理。

 

WindowProc的函数原型如下:

LRESULT CWnd::WindowProc(UINT message,

WPARAM wParam, LPARAM lParam)

这是一个虚拟函数,程序员可以在CWnd的派生类中覆盖它,改变MFC分发消息的方式。例如,MFC的CControlBar就覆盖了WindowProc,对某些消息作了自己的特别处理,其他消息处理由基类的WindowProc函数完成。

但是在当前例子中,当前对象的类CTview没有覆盖该函数,所以CWnd的WindowProc被调用。

这个函数把下一步的工作交给OnWndMsg函数来处理。如果OnWndMsg没有处理,则交给DefWindowProc来处理。

OnWndMsg和DefWindowProc都是CWnd类的虚拟函数。

 

     

  • OnWndMsg的原型如下:

     

BOOL CWnd::OnWndMsg( UINT message,

WPARAM wParam, LPARAM lParam,RESULT*pResult );

该函数是虚拟函数。

和WindowProc一样,由于当前对象的类CTview没有覆盖该函数,所以CWnd的OnWndMsg被调用。

在CWnd中,MFC使用OnWndMsg来分别处理各类消息:

如果是WM_COMMAND消息,交给OnCommand处理;然后返回。

如果是WM_NOTIFY消息,交给OnNotify处理;然后返回。

如果是WM_ACTIVATE消息,先交给_AfxHandleActivate处理(后面5.3.3.7节会解释它的处理),再继续下面的处理。

如果是WM_SETCURSOR消息,先交给_AfxHandleSetCursor处理;然后返回。

如果是其他的Windows消息(包括WM_ACTIVATE),则

首先在消息缓冲池进行消息匹配,

若匹配成功,则调用相应的消息处理函数;

若不成功,则在消息目标的消息映射数组中进行查找匹配,看它是否处理当前消息。这里,消息目标即CTview对象。

如果消息目标处理了该消息,则会匹配到消息处理函数,调用它进行处理;

否则,该消息没有被应用程序处理,OnWndMsg返回FALSE。

 

关于Windows消息和消息处理函数的匹配,见下一节。

 

缺省处理函数DefWindowProc将在讨论对话框等的实现时具体分析。

           

        1. Windows消息的查找和匹配

           

          CWnd或者派生类的对象调用OnWndMsg搜索本对象或者基类的消息映射数组,寻找当前消息的消息处理函数。如果当前对象或者基类处理了当前消息,则必定在其中一个类的消息映射数组中匹配到当前消息的处理函数。

          消息匹配是一个比较耗时的任务,为了提高效率,MFC设计了一个消息缓冲池,把要处理的消息和匹配到的消息映射条目(条目包含了消息处理函数的地址)以及进行消息处理的当前类等信息构成一条缓冲信息,放到缓冲池中。如果以后又有同样的消息需要同一个类处理,则直接从缓冲池查找到对应的消息映射条目就可以了。

          MFC用哈希查找来查询消息映射缓冲池。消息缓冲池相当于一个哈希表,它是应用程序的一个全局变量,可以放512条最新用到的消息映射条目的缓冲信息,每一条缓冲信息是哈希表的一个入口。

          采用AFX_MSG_CACHE结构描述每条缓冲信息,其定义如下:

          struct AFX_MSG_CACHE

          {

          UINT nMsg;

          const AFX_MSGMAP_ENTRY* lpEntry;

          const AFX_MSGMAP* pMessageMap;

          };

          nMsg存放消息ID,每个哈希表入口有不同的nMsg。

          lpEnty存放和消息ID匹配的消息映射条目的地址,它可能是this所指对象的类的映射条目,也可能是这个类的某个基类的映射条目,也可能是空。

          pMessageMap存放消息处理函数匹配成功时进行消息处理的当前类(this所指对象的类)的静态成员变量messageMap的地址,它唯一的标识了一个类(每个类的messageMap变量都不一样)。

          this所指对象是一个CWnd或其派生类的实例,是正在处理消息的MFC窗口对象。

          哈希查找:使用消息ID的值作为关键值进行哈希查找,如果成功,即可从lpEntry获得消息映射条目的地址,从而得到消息处理函数及其原型。

          如何判断是否成功匹配呢?有两条标准:

          第一,当前要处理的消息message在哈希表(缓冲池)中有入口;第二,当前窗口对象(this所指对象)的类的静态变量messageMap的地址应该等于本条缓冲信息的pMessagMap。MFC通过虚拟函数GetMessagMap得到messageMap的地址。

          如果在消息缓冲池中没有找到匹配,则搜索当前对象的消息映射数组,看是否有合适的消息处理函数。

          如果匹配到一个消息处理函数,则把匹配结果加入到消息缓冲池中,即填写该条消息对应的哈希表入口:

          nMsg=message;

          pMessageMap=this->GetMessageMap;

          lpEntry=查找结果

          然后,调用匹配到的消息处理函数。否则(没有找到),使用_GetBaseMessageMap得到基类的消息映射数组,查找和匹配;直到匹配成功或搜寻了所有的基类(到CCmdTarget)为止。

          如果最后没有找到,则也把该条消息的匹配结果加入到缓冲池中。和匹配成功不同的是:指定lpEntry为空。这样OnWndMsg返回,把控制权返还给AfxCallWndProc函数,AfxCallWndProc将继续调用DefWndProc进行缺省处理。

           

          消息映射数组的搜索在CCmdTarget::OnCmdMsg函数中也用到了,而且算法相同。为了提高速度,MFC把和消息映射数组条目逐一比较、匹配的函数AfxFindMessageEntry用汇编书写。

          const AFX_MSGMAP_ENTRY* AFXAPI

          AfxFindMessageEntry(const AFX_MSGMAP_ENTRY* lpEntry,

          UINT nMsg, UINT nCode, UINT nID)

          第一个参数是要搜索的映射数组的入口;第二个参数是Windows消息标识;第三个参数是控制通知消息标识;第四个参数是命令消息标识。

          对Windows消息来说,nMsg是每条消息不同的,nID和nCode为0。

          对命令消息来说,nMsg固定为WM_COMMAND,nID是每条消息不同,nCode都是CN_COMMAND(定义为0)。

          对控制通知消息来说,nMsg固定为WM_COMMAND或者WM_NOTIFY,nID和nCode是每条消息不同。

          对于Register消息,nMsg指定为0XC000,nID和nCode为0。在使用函数AfxFindMessageEntry得到匹配结果之后,还必须判断nSig是否等于message,只有相等才调用对应的消息处理函数。

           

        2. Windows消息处理函数的调用

           

          对一个Windows消息,匹配到了一个消息映射条目之后,将调用映射条目所指示的消息处理函数。

          调用处理函数的过程就是转换映射条目的pfn指针为适当的函数类型并执行它:MFC定义了一个成员函数指针mmf,首先把消息处理函数的地址赋值给该函数指针,然后根据消息映射条目的nSig值转换指针的类型。但是,要给函数指针mmf赋值,必须使该指针可以指向所有的消息处理函数,为此则该指针的类型是所有类型的消息处理函数指针的联合体。

          对上述过程,MFC的实现大略如下:

          union MessageMapFunctions mmf;

          mmf.pfn = lpEntry->pfn;

          swithc (value_of_nsig){

          case AfxSig_is: //OnCreate就是该类型

          lResult = (this->*mmf.pfn_is)((LPTSTR)lParam);

          break;

          default:

          ASSERT(FALSE); break;

          }

          LDispatchRegistered: // 处理registered windows messages

          ASSERT(message >= 0xC000);

          mmf.pfn = lpEntry->pfn;

          lResult = (this->*mmf.pfn_lwl)(wParam, lParam);

          如果消息处理函数有返回值,则返回该结果,否则,返回TRUE。

          对于图4-1所示的例子,nSig等于AfxSig_is,所以将执行语句

          (this->*mmf.pfn_is)((LPTSTR)lParam)

          也就是对CTview::OnCreate的调用。

          顺便指出,对于Registered窗口消息,消息处理函数都是同一原型,所以都被转换成lwl型(关于Registered窗口消息的映射,见4.4.2节)。

          综上所述,标准Windwos消息和应用程序消息中的Registered消息,由窗口过程直接调用相应的处理函数处理:

          如果某个类型的窗口(C++类)处理了某条消息(覆盖了CWnd或直接基类的处理函数),则对应的HWND窗口(Winodws window)收到该消息时就调用该覆盖函数来处理;如果该类窗口没有处理该消息,则调用实现该处理函数最直接的基类(在C++的类层次上接近该类)来处理,上述例子中如果CTview不处理WM_CREATE消息,则调用上一层的CWnd::OnCreate处理;

          如果基类都不处理该消息,则调用DefWndProc来处理。

           

        3. 消息映射机制完成虚拟函数功能的原理

           

综合对Windows消息的处理来看,MFC使用消息映射机制完成了C++虚拟函数的功能。这主要基于以下几点:

     

  • 所有处理消息的类从CCmdTarget派生。

     

     

  • 使用静态成员变量_messageEntries数组存放消息映射条目,使用静态成员变量messageMap来唯一地区别和得到类的消息映射。

     

     

  • 通过GetMessage虚拟函数来获取当前对象的类的messageMap变量,进而得到消息映射入口。

     

     

  • 按照先底层,后基层的顺序在类的消息映射数组中搜索消息处理函数。基于这样的机制,一般在覆盖基类的消息处理函数时,应该调用基类的同名函数。

     

以上论断适合于MFC其他消息处理机制,如对命令消息的处理等。不同的是其他消息处理有一个命令派发/分发的过程。

下一节,讨论命令消息的接受和处理。

         

      1. 对命令消息的接收和处理

         

           

        1. MFC标准命令消息的发送

           

在SDI或者MDI应用程序中,命令消息由用户界面对象(如菜单、工具条等)产生,然后送给主边框窗口。主边框窗口使用标准MFC窗口过程处理命令消息。窗口过程把命令传递给MFC主边框窗口对象,开始命令消息的分发。MFC边框窗口类CFrameWnd提供了消息分发的能力。

下面,还是通过一个例子来说明命令消息的处理过程。

使用AppWizard产生一个单文档应用程序t。从help菜单选择“About”,就会弹出一个ABOUT对话框。下面,讨论从命令消息的发出到对话框弹出的过程。

首先,选择“ About”菜单项的动作导致一个Windows命令消息ID_APP_ABOUT的产生。Windows系统发送该命令消息到边框窗口,导致它的窗口过程AfxWndProc被调用,参数1是边框窗口的句柄,参数2是消息ID(即WM_COMMAND),参数3、4是消息参数,参数3的值是ID_APP_ABOUT。接着的系列调用如图4-3所示。

 

 

下面分别讲述每一层所调用的函数。

前4步同对Windows消息的处理。这里接受消息的HWND窗口是主边框窗口,因此,AfxWndProc根据HWND句柄得到的MFC窗口对象是MFC边框窗口对象。

在4.2.2节谈到,如果CWnd::OnWndMsg判断要处理的消息是命令消息(WM_COMMAND),就调用OnCommand进一步处理。由于OnCommand是虚拟函数,当前MFC窗口对象是边框窗口对象,它的类从CFrameWnd类导出,没有覆盖CWnd的虚拟函数OnCommand,而CFrameWnd覆盖了CWnd的OnCommand,所以,CFrameWnd的OnCommand被调用。换句话说,CFrameWnd的OnCommand被调用是动态约束的结果。接着介绍的本例子的有关调用,也是通过动态约束而实际发生的函数调用。

接着的有关调用,将不进行为什么调用某个类的虚拟或者消息处理函数的分析。

(1)CFrameWnd的OnCommand函数

BOOL CFrameWnd::OnCommand(WPARAM wParam, LPARAM lParam)

参数wParam的低阶word存放了菜单命令nID或控制子窗口ID;如果消息来自控制窗口,高阶word存放了控制通知消息;如果消息来自加速键,高阶word值为1;如果消息来自菜单,高阶word值为0。

如果是通知消息,参数lParam存放了控制窗口的句柄hWndCtrl,其他情况下lParam是0。

在这个例子里,低阶word是ID_APP_ABOUT,高阶word是1;lParam是0。

MFC对CFrameWnd的缺省实现主要是获得一个机会来检查程序是否运行在HELP状态,需要执行上下文帮助,如果不需要,则调用基类的CWnd::OnCommand实现正常的命令消息发送。

(2)CWnd的OnCommand函数

BOOL CWnd::OnCommand(WPARAM wParam, LPARAM lParam)

它按一定的顺序处理命令或者通知消息,如果发送成功,返回TRUE,否则,FALSE。处理顺序如下:

如果是命令消息,则调用OnCmdMsg(nID, CN_UPDATE_COMMAND_UI, &state, NULL)测试nID命令是否已经被禁止,如果这样,返回FALSE;否则,调用OnCmdMsg进行命令发送。关于CN_UPDATE_COMMAND_UI通知消息,见后面用户界面状态的更新处理。

如果是控制通知消息,则先用ReflectLastMsg反射通知消息到子窗口。如果子窗口处理了该消息,则返回TRUE;否则,调用OnCmdMsg进行命令发送。关于通知消息的反射见后面4.4.4.3节。OnCommand给OnCmdMsg传递四个参数:nID,即命令消息ID;nCode,如果是通知消息则为通知代码,如果是命令消息则为NC_COMMAND(即0);其余两个参数为空。

(3)CFrameWnd的OnCmdMsg函数

BOOL CFrameWnd::OnCmdMsg(UINT nID, int nCode, void* pExtra,

AFX_CMDHANDLERINFO* pHandlerInfo)

参数1是命令ID;如果是通知消息(WM_COMMAND或者WM_NOTIFY),则参数2表示通知代码,如果是命令消息,参数2是0;如果是WM_NOTIFY,参数3包含了一些额外的信息;参数4在正常消息处理中应该是空。

在这个例子里,参数1是命令ID,参数2为0,参数3空。

OnCmdMsg是虚拟函数,CFrameWnd覆盖了该函数,当前对象(this所指)是MFC单文档的边框窗口对象。故CFrameWnd的OnCmdMsg被调用。CFrameWnd::OnCmdMsg在MFC消息发送中占有非常重要的地位,MFC对该函数的缺省实现确定了MFC的标准命令发送路径:

     

  1. 送给活动(Active)视处理,调用活动视的OnCmdMsg。由于当前对象是MFC视对象,所以,OnCmdMsg将搜索CTview及其基类的消息映射数组,试图得到相应的处理函数。

     

     

  2. 如果视对象自己不处理,则视得到和它关联的文档,调用关联文档的OnCmdMsg。由于当前对象是MFC视对象,所以,OnCmdMsg将搜索CTdoc及其基类的消息映射数组,试图得到相应的处理函数。

     

     

  3. 如果文档对象不处理,则它得到管理文档的文档模板对象,调用文档模板的OnCmdMsg。由于当前对象是MFC文档模板对象,所以,OnCmdMsg将搜索文档模板类及其基类的消息映射数组,试图得到相应的处理函数。

     

     

  4. 如果文档模板不处理,则把没有处理的信息逐级返回:文档模板告诉文档对象,文档对象告诉视对象,视对象告诉边框窗口对象。最后,边框窗口得知,视、文档、文档模板都没有处理消息。

     

     

  5. CFrameWnd的OnCmdMsg继续调用CWnd::OnCmdMsg(斜体表示有类属限制)来处理消息。由于CWnd没有覆盖OnCmdMsg,故实际上调用了函数CCmdTarget::OnCmdMsg。由于当前对象是MFC边框窗口对象,所以OnCmdMsg函数将搜索CMainFrame类及其所有基类的消息映射数组,试图得到相应的处理函数。CWnd没有实现OnCmdMsg却指定要执行其OnCmdMsg函数,可能是为了以后MFC给CWnd实现了OnCmdMsg之后其他代码不用改变。

     

    这一步是边框窗口自己尝试处理消息。

     

  6. 如果边框窗口对象不处理,则送给应用程序对象处理。调用CTApp的OnCmdMsg,由于实际上CTApp及其基类CWinApp没有覆盖OnCmdMsg,故实际上调用了函数CCmdTarget::OnCmdMsg。由于当前对象是MFC应用程序对象,所以OnCmdMsg函数将搜索CTApp类及其所有基类的的消息映射入口数组,试图得到相应的处理函数

     

     

  7. 如果应用程序对象不处理,则返回FALSE,表明没有命令目标处理当前的命令消息。这样,函数逐级别返回,OnCmdMsg告诉OnCommand消息没有被处理,OnCommand告诉OnWndMsg消息没有被处理,OnWndMsg告诉WindowProc消息没有被处理,于是WindowProc调用DefWindowProc进行缺省处理。

     

 

本例子在第六步中,应用程序对ID_APP_ABOUT消息作了处理。它找到处理函数CTApp::OnAbout,使用DispatchCmdMsg派发消息给该函数处理。

如果是MDI边框窗口,标准发送路径还有一个环节,该环节和第二、三、四步所涉及的OnCmdMsg函数,将在下两节再次具体分析。

           

        1. 命令消息的派发和消息的多次处理

           

     

  1. 命令消息的派发

     

    如前3.1所述,CCmdTarget的静态成员函数DispatchCmdMsg用来派发命令消息给指定的命令目标的消息处理函数。

    static BOOL DispatchCmdMsg(CCmdTarget* pTarget,

    UINT nID, int nCode,

    AFX_PMSG pfn, void* pExtra, UINT nSig,

    AFX_CMDHANDLERINFO* pHandlerInfo)

    前面在讲CCmdTarget时,提到了该函数。这里讲述它的实现:

    第一个参数指向处理消息的对象;第二个参数是命令ID;第三个是通知消息等;第四个是消息处理函数地址;第五个参数用于存放一些有用的信息,根据nCode的值表示不同的意义,例如当消息是WM_NOFITY,指向一个NMHDR结构(关于WM_NOTIFY,参见4.4.4.2节通知消息的处理);第六个参数标识消息处理函数原型;第七个参数是一个指针,指向AFX_CMDHANDLERINFO结构。前六个参数(除了第五个外)都是向函数传递信息,第五个和第七个参数是双向的,既向函数传递信息,也可以向调用者返回信息。

    关于AFX_CMDHANDLERINFO结构:

    struct AFX_CMDHANDLERINFO

    {

    CCmdTarget* pTarget;

    void (AFX_MSG_CALL CCmdTarget::*pmf)(void);

    };

    第一个成员是一个指向命令目标对象的指针,第二个成员是一个指向CCmdTarget成员函数的指针。

     

    该函数的实现流程可以如下描述:

    首先,它检查参数pHandlerInfo是否空,如果不空,则用pTarget和pfn填写其指向的结构,返回TRUE;通常消息处理时传递来的pHandlerInfo空,而在使用OnCmdMsg来测试某个对象是否处理某条命令时,传递一个非空的pHandlerInfo指针。若返回TRUE,则表示可以处理那条消息。

    如果pHandlerInfo空,则进行消息处理函数的调用。它根据参数nSig的值,把参数pfn的类型转换为要调用的消息处理函数的类型。这种指针转换技术和前面讲述的Windows消息的处理是一样的。

     

  2. 消息的多次处理

     

如果消息处理函数不返回值,则DispatchCmdMsg返回TRUE;否则,DispatchCmdMsg返回消息处理函数的返回值。这个返回值沿着消息发送相反的路径逐级向上传递,使得各个环节的OnCmdMsg和OnCommand得到返回的处理结果:TRUE或者FALSE,即成功或者失败。

这样就产生了一个问题,如果消息处理函数有意返回一个FALSE,那么不就传递了一个错误的信息?例如,OnCmdMsg函数得到FALSE返回值,就认为消息没有被处理,它将继续发送消息到下一环节。的确是这样的,但是这不是MFC的漏洞,而是有意这么设计的,用来处理一些特别的消息映射宏,实现同一个消息的多次处理。

通常的命令或者通知消息是没有返回值的(见4.4.2节的消息映射宏),仅仅一些特殊的消息处理函数具有返回值,这类消息的消息处理函数是使用扩展消息映射宏映射的,例如:

ON_COMMAND对应的ON_COMMAND_EX

扩展映射宏和对应的普通映射宏的参数个数相同,含义一样。但是扩展映射宏的消息处理函数的原型和对应的普通映射宏相比,有两个不同之处:一是多了一个UINT类型的参数,另外就是有返回值(返回BOOL类型)。回顾4.4.2章节,范围映射宏ON_COMMAND_RANGE的消息处理函数也有一个这样的参数,该参数在两处的含义是一样的,例如:命令消息扩展映射宏ON_COMMAND_EX定义的消息处理函数解释该参数是当前要处理的命令消息ID。有返回值的意义在于:如果扩展映射宏的消息处理函数返回FALSE,则导致当前消息被发送给消息路径上的下一个消息目标处理。

综合来看,ON_COMMAND_EX宏有两个功能:

一是可以把多个命令消息指定给一个消息处理函数处理。这类似于ON_COMMAND_RANGE宏的作用。不过,这里的多条消息的命令ID或者控制子窗口ID可以不连续,每条消息都需要一个ON_COMMAND_EX宏。

二是可以让几个消息目标处理同一个命令或者通知或者反射消息。如果消息发送路径上较前的命令目标不处理消息或者处理消息后返回FALSE,则下一个命令目标将继续处理该消息。

 

对于通知消息、反射消息,它们也有扩展映射宏,而且上述论断也适合于它们。例如:

ON_NOTIFY对应的ON_NOTIFY_EX

ON_CONTROL对应的ON_CONTROL_EX

ON_CONTROL_REFLECT对应的ON_CONTROL_REFLECT_EX

等等。

 

范围消息映射宏也有对应的扩展映射宏,例如:

ON_NOTIFY_RANGE对应的ON_NOTIFY_EX_RANGE

ON_COMMAND_RANGE对应的ON_COMMAND_EX_RANGE

使用这些宏的目的在于利用扩展宏的第二个功能:实现消息的多次处理。

 

关于扩展消息映射宏的例子,参见13.2..4.4节和13.2.4.6节。

           

        1. 一些消息处理类的OnCmdMsg的实现

           

从以上论述知道,OnCmdMsg虚拟函数在MFC命令消息的发送中扮演了重要的角色,CFrameWnd的OnCmdMsg实现了MFC的标准命令消息发送路径。

那么,就产生一个问题:如果命令消息不送给边框窗口对象,那么就不会有按标准命令发送路径发送消息的过程?答案是肯定的。例如一个菜单被一个对话框窗口所拥有,那么,菜单命令将送给MFC对话框窗口对象处理,而不是MFC边框窗口处理,当然不会和CFrameWnd的处理流程相同。

但是,有一点需要指出,一般标准的SDI和MDI应用程序,只有主边框窗口拥有菜单和工具条等用户接口对象,只有在用户与用户接口对象进行交互时,才产生命令,产生的命令必然是送给SDI或者MDI程序的主边框窗口对象处理。