MFC消息处理学习总结

VC++ 专栏收录该内容
381 篇文章 1 订阅

Windows消息机制概述

http://www.cppblog.com/suiaiguo/archive/2009/07/18/90412.html

消息是指什么?
     消息系统对于一个win32程序来说十分重要,它是一个程序运行的动力源泉。一个消息,是系统定义的一个32位的值,他唯一的定义了一个事件,向 Windows发出一个通知,告诉应用程序某个事情发生了。例如,单击鼠标、改变窗口尺寸、按下键盘上的一个键都会使Windows发送一个消息给应用程序。
    消息本身是作为一个记录传递给应用程序的,这个记录中包含了消息的类型以及其他信息。例如,对于单击鼠标所产生的消息来说,这个记录中包含了单击鼠标时的坐标。这个记录类型叫做MSG,MSG含有来自windows应用程序消息队列的消息信息,它在Windows中声明如下:
typedef struct tagMsg
{
       HWND    hwnd;       //接受该消息的窗口句柄
       UINT    message;    //消息常量标识符,也就是我们通常所说的消息号
       WPARAM  wParam;     //32位消息的特定附加信息,确切含义依赖于消息值
       LPARAM  lParam;     //32位消息的特定附加信息,确切含义依赖于消息值
       DWORD   time;       //消息创建时的时间
       POINT   pt;         //消息创建时的鼠标/光标在屏幕坐标系中的位置
}MSG;

    消息可以由系统或者应用程序产生。系统在发生输入事件时产生消息。举个例子, 当用户敲键, 移动鼠标或者单击控件。系统也产生消息以响应由应用程序带来的变化, 比如应用程序改变系统字体改变窗体大小。应用程序可以产生消息使窗体执行任务,或者与其他应用程序中的窗口通讯。
消息中有什么?
   我们给出了上面的注释,是不是会对消息结构有了一个比较清楚的认识?如果还没有,那么我们再试着给出下面的解释:
     hwnd 32位的窗口句柄。窗口可以是任何类型的屏幕对象,因为Win32能够维护大多数可视对象的句柄(窗口、对话框、按钮、编辑框等)。
     message用于区别其他消息的常量值,这些常量可以是Windows单元中预定义的常量,也可以是自定义的常量。消息标识符以常量命名的方式指出消息的含义。当窗口过程接收到消息之后,他就会使用消息标识符来决定如何处理消息。例如、WM_PAINT告诉窗口过程窗体客户区被改变了需要重绘。符号常量指定系统消息属于的类别,其前缀指明了处理解释消息的窗体的类型。
     wParam 通常是一个与消息有关的常量值,也可能是窗口或控件的句柄。
     lParam 通常是一个指向内存中数据的指针。由于WParam、lParam和Pointer都是32位的,因此,它们之间可以相互转换。

消息标识符的值
     系统保留消息标识符的值在0x0000在0x03ff(WM_USER-1)范围。这些值被系统定义消息使用。应用程序不能使用这些值给自己的消息。应用程序消息从WM_USER(0X0400)到0X7FFF,或0XC000到0XFFFF;WM_USER到 0X7FFF范围的消息由应用程序自己使用;0XC000到0XFFFF范围的消息用来和其他应用程序通信,我们顺便说一下具有标志性的消息值:
     WM_NULL---0x0000    空消息。
     0x0001----0x0087    主要是窗口消息。
     0x00A0----0x00A9    非客户区消息 
     0x0100----0x0108    键盘消息
     0x0111----0x0126    菜单消息
     0x0132----0x0138    颜色控制消息
     0x0200----0x020A    鼠标消息
     0x0211----0x0213    菜单循环消息
     0x0220----0x0230    多文档消息
     0x03E0----0x03E8    DDE消息
     0x0400              WM_USER
     0x8000              WM_APP
     0x0400----0x7FFF    应用程序自定义私有消息

消息有哪几种?
   其实,windows中的消息虽然很多,但是种类并不繁杂,大体上有3种:窗口消息、命令消息和控件通知消息。
     窗口消息大概是系统中最为常见的消息,它是指由操作系统和控制其他窗口的窗口所使用的消息。例如CreateWindow、DestroyWindow和MoveWindow等都会激发窗口消息,还有我们在上面谈到的单击鼠标所产生的消息也是一种窗口消息。
     命令消息,这是一种特殊的窗口消息,他用来处理从一个窗口发送到另一个窗口的用户请求,例如按下一个按钮,他就会向主窗口发送一个命令消息。
     控件通知消息,是指这样一种消息,一个窗口内的子控件发生了一些事情,需要通知父窗口。通知消息只适用于标准的窗口控件如按钮、列表框、组合框、编辑框,以及Windows公共控件如树状视图、列表视图等。例如,单击或双击一个控件、在控件中选择部分文本、操作控件的滚动条都会产生通知消息。她类似于命令消息,当用户与控件窗口交互时,那么控件通知消息就会从控件窗口发送到它的主窗口。但是这种消息的存在并不是为了处理用户命令,而是为了让主窗口能够改变控件,例如加载、显示数据。例如按下一个按钮,他向父窗口发送的消息也可以看作是一个控件通知消息;单击鼠标所产生的消息可以由主窗口直接处理,然后交给控件窗口处理。
    其中窗口消息及控件通知消息主要由窗口类即直接或间接由CWND类派生类处理。相对窗口消息及控件通知消息而言,命令消息的处理对象范围就广得多,它不仅可以由窗口类处理,还可以由文档类,文档模板类及应用类所处理。
    由于控件通知消息很重要的,人们用的也比较多,但是具体的含义往往令初学者晕头转向,所以我决定把常见的几个列出来供大家参考:
按扭控件
BN_CLICKED        用户单击了按钮
 BN_DISABLE 按钮被禁止
 BN_DOUBLECLICKED  用户双击了按钮
 BN_HILITE  用/户加亮了按钮
 BN_PAINT  按钮应当重画
 BN_UNHILITE 加亮应当去掉

组合框控件
 CBN_CLOSEUP 组合框的列表框被关闭
 CBN_DBLCLK 用户双击了一个字符串
 CBN_DROPDOWN 组合框的列表框被拉出
 CBN_EDITCHANGE 用户修改了编辑框中的文本
 CBN_EDITUPDATE 编辑框内的文本即将更新
 CBN_ERRSPACE 组合框内存不足
 CBN_KILLFOCUS 组合框失去输入焦点
 CBN_SELCHANGE 在组合框中选择了一项
 CBN_SELENDCANCEL 用户的选择应当被取消
 CBN_SELENDOK 用户的选择是合法的
 CBN_SETFOCUS 组合框获得输入焦点

编辑框控件
 EN_CHANGE 编辑框中的文本己更新
 EN_ERRSPACE 编辑框内存不足
 EN_HSCROLL 用户点击了水平滚动条
 EN_KILLFOCUS 编辑框正在失去输入焦点
 EN_MAXTEXT 插入的内容被截断
 EN_SETFOCUS 编辑框获得输入焦点
 EN_UPDATE 编辑框中的文本将要更新
 EN_VSCROLL 用户点击了垂直滚动条消息含义

列表框控件
 LBN_DBLCLK 用户双击了一项
 LBN_ERRSPACE 列表框内存不够
 LBN_KILLFOCUS 列表框正在失去输入焦点
 LBN_SELCANCEL 选择被取消
 LBN_SELCHANGE 选择了另一项
 LBN_SETFOCUS 列表框获得输入焦点

队列消息和非队列消息
   从消息的发送途径来看,消息可以分成2种:队列消息和非队列消息。消息队列由可以分成系统消息队列和线程消息队列。系统消息队列由Windows维护,线程消息队列则由每个GUI线程自己进行维护,为避免给non-GUI现成创建消息队列,所有线程产生时并没有消息队列,仅当线程第一次调用GDI函数时系统才给线程创建一个消息队列。队列消息送到系统消息队列,然后到线程消息队列;非队列消息直接送给目的窗口过程。
     对于队列消息,最常见的是鼠标和键盘触发的消息,例如WM_MOUSERMOVE,WM_CHAR等消息,还有一些其它的消息,例如:WM_PAINT、 WM_TIMER和WM_QUIT。当鼠标、键盘事件被触发后,相应的鼠标或键盘驱动程序就会把这些事件转换成相应的消息,然后输送到系统消息队列,由 Windows系统去进行处理。Windows系统则在适当的时机,从系统消息队列中取出一个消息,根据前面我们所说的MSG消息结构确定消息是要被送往那个窗口,然后把取出的消息送往创建窗口的线程的相应队列,下面的事情就该由线程消息队列操心了,Windows开始忙自己的事情去了。线程看到自己的消息队列中有消息,就从队列中取出来,通过操作系统发送到合适的窗口过程去处理。
     一般来讲,系统总是将消息Post在消息队列的末尾。这样保证窗口以先进先出的顺序接受消息。然而,WM_PAINT是一个例外,同一个窗口的多个 WM_PAINT被合并成一个 WM_PAINT 消息, 合并所有的无效区域到一个无效区域。合并WM_PAIN的目的是为了减少刷新窗口的次数。

    非队列消息将会绕过系统队列和消息队列,直接将消息发送到窗口过程,。系统发送非队列消息通知窗口,系统发送消息通知窗口。例如,当用户激活一个窗口系统发送WM_ACTIVATE, WM_SETFOCUS, and WM_SETCURSOR。这些消息通知窗口它被激活了。非队列消息也可以由当应用程序调用系统函数产生。例如,当程序调用SetWindowPos系统发送WM_WINDOWPOSCHANGED消息。一些函数也发送非队列消息,例如下面我们要谈到的函数。
     
消息的发送
     了解了上面的这些基础理论之后,我们就可以进行一下简单的消息发送与接收。
     把一个消息发送到窗口有3种方式:发送、寄送和广播。
     发送消息的函数有SendMessage、SendMessageCallback、SendNotifyMessage、 SendMessageTimeout;寄送消息的函数主要有PostMessage、PostThreadMessage、 PostQuitMessage;广播消息的函数我知道的只有BroadcastSystemMessage、 BroadcastSystemMessageEx。
     SendMessage的原型如下:LRESULT SendMessage(HWND hWnd,UINT Msg,WPARAM wParam,LPARAM lParam),这个函数主要是向一个或多个窗口发送一条消息,一直等到消息被处理之后才会返回。不过需要注意的是,如果接收消息的窗口是同一个应用程序的一部分,那么这个窗口的窗口函数就被作为一个子程序马上被调用;如果接收消息的窗口是被另外的线程所创建的,那么窗口系统就切换到相应的线程并且调用相应的窗口函数,这条消息不会被放进目标应用程序队列中。函数的返回值是由接收消息的窗口的窗口函数返回,返回的值取决于被发送的消息。
     PostMessage的原型如下:BOOL PostMessage(HWND hWnd,UINT Msg,WPARAM wParam,LPARAM lParam),该函数把一条消息放置到创建hWnd窗口的线程的消息队列中,该函数不等消息被处理就马上将控制返回。需要注意的是,如果hWnd参数为 HWND_BROADCAST,那么,消息将被寄送给系统中的所有的重叠窗口和弹出窗口,但是子窗口不会收到该消息;如果hWnd参数为NULL,则该函数类似于将dwThreadID参数设置成当前线程的标志来调用PostThreadMEssage函数。
  从上面的这2个具有代表性的函数,我们可以看出消息的发送方式和寄送方式的区别所在:被发送的消息是否会被立即处理,函数是否立即返回。被发送的消息会被立即处理,处理完毕后函数才会返回;被寄送的消息不会被立即处理,他被放到一个先进先出的队列中,一直等到应用程序空线的时候才会被处理,不过函数放置消息后立即返回。

  实际上,发送消息到一个窗口处理过程和直接调用窗口处理过程之间并没有太大的区别,他们直接的唯一区别就在于你可以要求操作系统截获所有被发送的消息,但是不能够截获对窗口处理过程的直接调用。
  以寄送方式发送的消息通常是与用户输入事件相对应的,因为这些事件不是十分紧迫,可以进行缓慢的缓冲处理,例如鼠标、键盘消息会被寄送,而按钮等消息则会被发送。
  广播消息用得比较少,BroadcastSystemMessage函数原型如下:
      long BroadcastSystemMessage(DWORD dwFlags,LPDWORD lpdwRecipients,UINT uiMessage,WPARAM wParam,LPARAM lParam);该函数可以向指定的接收者发送一条消息,这些接收者可以是应用程序、可安装的驱动程序、网络驱动程序、系统级别的设备驱动消息和他们的任意组合。需要注意的是,如果dwFlags参数是BSF_QUERY并且至少一个接收者返回了BROADCAST_QUERY_DENY,则返回值为0,如果没有指定BSF_QUERY,则函数将消息发送给所有接收者,并且忽略其返回值。
消息的接收
 消息的接收主要有3个函数:GetMessage、PeekMessage、WaitMessage。
  GetMessage原型如下:BOOL GetMessage(LPMSG lpMsg,HWND hWnd,UINT wMsgFilterMin,UINT wMsgFilterMax);该函数用来获取与hWnd参数所指定的窗口相关的且wMsgFilterMin和wMsgFilterMax参数所给出的消息值范围内的消息。需要注意的是,如果hWnd为NULL,则GetMessage获取属于调用该函数应用程序的任一窗口的消息,如果 wMsgFilterMin和wMsgFilterMax都是0,则GetMessage就返回所有可得到的消息。函数获取之后将删除消息队列中的除 WM_PAINT消息之外的其他消息,至于WM_PAINT则只有在其处理之后才被删除。
   PeekMessage原型如下:BOOL PeekMessage(LPMSG lpMsg,HWND hWnd,UINT wMsgFilterMin,UINT wMsgFilterMax,UINT wRemoveMsg);该函数用于查看应用程序的消息队列,如果其中有消息就将其放入lpMsg所指的结构中,不过,与GetMessage不同的是,PeekMessage函数不会等到有消息放入队列时才返回。同样,如果hWnd为NULL,则PeekMessage获取属于调用该函数应用程序的任一窗口的消息,如果hWnd=-1,那么函数只返回把hWnd参数为NULL的PostAppMessage函数送去的消息。如果 wMsgFilterMin和wMsgFilterMax都是0,则PeekMessage就返回所有可得到的消息。函数获取之后将视最后一个参数来决定是否删除消息队列中的除 WM_PAINT消息之外的其他消息,至于WM_PAINT则只有在其处理之后才被删除。
   WaitMessage原型如下:BOOL WaitMessage();当一个应用程序无事可做时,该函数就将控制权交给另外的应用程序,同时将该应用程序挂起,直到一个新的消息被放入应用程序的队列之中才返回。

消息的处理
  接下来我们谈一下消息的处理,首先我们来看一下VC中的消息泵:


while(GetMessage(&msg, NULL, 0, 0))
{
       if(!TranslateAccelerator(msg.hWnd, hAccelTable, &msg))
      { 
            TranslateMessage(&msg);
            DispatchMessage(&msg);
       }
}
 
   首先,GetMessage从进程的主线程的消息队列中获取一个消息并将它复制到MSG结构,如果队列中没有消息,则GetMessage函数将等待一个消息的到来以后才返回。如果你将一个窗口句柄作为第二个参数传入GetMessage,那么只有指定窗口的的消息可以从队列中获得。GetMessage也可以从消息队列中过滤消息只接受消息队列中落在范围内的消息。这时候就要利用GetMessage/PeekMessage指定一个消息过滤器。这个过滤器是一个消息标识符的范围或者是一个窗体句柄,或者两者同时指定。当应用程序要查找一个后入消息队列的消息是很有用。WM_KEYFIRST 和 WM_KEYLAST 常量用于接受所有的键盘消息。 WM_MOUSEFIRST 和 WM_MOUSELAST 常量用于接受所有的鼠标消息。 
 然后TranslateAccelerator判断该消息是不是一个按键消息并且是一个加速键消息,如果是,则该函数将把几个按键消息转换成一个加速键消息传递给窗口的回调函数。处理了加速键之后,函数TranslateMessage将把两个按键消息WM_KEYDOWN和WM_KEYUP转换成一个 WM_CHAR,不过需要注意的是,消息WM_KEYDOWN,WM_KEYUP仍然将传递给窗口的回调函数。     
 处理完之后,DispatchMessage函数将把此消息发送给该消息指定的窗口中已设定的回调函数。如果消息是WM_QUIT,则 GetMessage返回0,从而退出循环体。应用程序可以使用PostQuitMessage来结束自己的消息循环。通常在主窗口的 WM_DESTROY消息中调用。
 下面我们举一个常见的小例子来说明这个消息泵的运用:

if (::PeekMessage(&msg, m_hWnd, WM_KEYFIRST,WM_KEYLAST, PM_REMOVE))
{
          if (msg.message == WM_KEYDOWN && msg.wParam == VK_ESCAPE)...
}

  这里我们接受所有的键盘消息,所以就用WM_KEYFIRST 和 WM_KEYLAST作为参数。最后一个参数可以是PM_NOREMOVE 或者 PM_REMOVE,表示消息信息是否应该从消息队列中删除。                 
   所以这段小代码就是判断是否按下了Esc键,如果是就进行处理。

窗口过程
 窗口过程是一个用于处理所有发送到这个窗口的消息的函数。任何一个窗口类都有一个窗口过程。同一个类的窗口使用同样的窗口过程来响应消息。系统发送消息给窗口过程将消息数据作为参数传递给他,消息到来之后,按照消息类型排序进行处理,其中的参数则用来区分不同的消息,窗口过程使用参数产生合适行为。
 一个窗口过程不经常忽略消息,如果他不处理,它会将消息传回到执行默认的处理。窗口过程通过调用DefWindowProc来做这个处理。窗口过程必须 return一个值作为它的消息处理结果。大多数窗口只处理小部分消息和将其他的通过DefWindowProc传递给系统做默认的处理。窗口过程被所有属于同一个类的窗口共享,能为不同的窗口处理消息。下面我们来看一下具体的实例:

LRESULT CALLBACK WndProc(HWND hWnd, UINT message, WPARAM wParam, LPARAM lParam)
{
 int wmId, wmEvent;
 PAINTSTRUCT ps;
 HDC hdc;
 TCHAR szHello[MAX_LOADSTRING];
 LoadString(hInst, IDS_HELLO, szHello, MAX_LOADSTRING);

 switch (message) 
 {
  case WM_COMMAND:
         wmId    = LOWORD(wParam); 
         wmEvent = HIWORD(wParam); 
         // Parse the menu selections:
         switch (wmId)
         {
          case IDM_ABOUT:
             DialogBox(hInst, (LPCTSTR)IDD_ABOUTBOX, hWnd, (DLGPROC)About);
             break;
          case IDM_EXIT:
             DestroyWindow(hWnd);
             break;
          default:
             return DefWindowProc(hWnd, message, wParam, lParam);
         }
   break;

  case WM_PAINT:
         hdc = BeginPaint(hWnd, &ps);
         // TODO: Add any drawing code here
         RECT rt;
         GetClientRect(hWnd, &rt);
         DrawText(hdc, szHello, strlen(szHello), &rt, DT_CENTER);
         EndPaint(hWnd, &ps);
         break;

  case WM_DESTROY:
         PostQuitMessage(0);
         break;
  default:
         return DefWindowProc(hWnd, message, wParam, lParam);
  }
  return 0;
}

消息分流器
  通常的窗口过程是通过一个switch语句来实现的,这个事情很烦,有没有更简便的方法呢?有,那就是消息分流器,利用消息分流器,我们可以把switch语句分成更小的函数,每一个消息都对应一个小函数,这样做的好处就是对消息更容易管理。
  之所以被称为消息分流器,就是因为它可以对任何消息进行分流。下面我们做一个函数就很清楚了:


void MsgCracker(HWND hWnd,int id,HWND hWndCtl,UINT codeNotify)
{
      switch(id)
      {
     case ID_A:
                  if(codeNotify==EN_CHANGE)
                  break;
     case ID_B:
                  if(codeNotify==BN_CLICKED)
                  break;
             .
       }
}

然后我们修改一下窗口过程:

LRESULT CALLBACK WndProc(HWND hWnd, UINT message, WPARAM wParam, LPARAM lParam)
{
       switch(message)
      {
             HANDLE_MSG(hWnd,WM_COMMAND,MsgCracker);
             HANDLE_MSG(hWnd,WM_DESTROY,MsgCracker);
           default:
                    return DefWindowProc(hWnd, message, wParam, lParam);
   }
  return 0;
}


在WindowsX.h中定义了如下的HANDLE_MSG宏:


   #define HANDLE_MSG(hwnd,msg,fn) \
             switch(msg): return HANDLE_##msg((hwnd),(wParam),(lParam),(fn));


实际上,HANDLE_WM_XXXX都是宏,例如:HANDLE_MSG(hWnd,WM_COMMAND,MsgCracker);将被转换成如下定义:


   #define HANDLE_WM_COMMAND(hwnd,wParam,lParam,fn)\ 
             ((fn)((hwnd),(int)(LOWORD(wParam)),(HWND)(lParam),(UINT)HIWORD(wParam)),0L);


好了,事情到了这一步,应该一切都明朗了。
不过,我们发现在windowsx.h里面还有一个宏:FORWARD_WM_XXXX,我们还是那WM_COMMAND为例,进行分析:


   #define FORWARD_WM_COMMAND(hwnd, id, hwndCtl, codeNotify, fn) \
     (void)(fn)((hwnd), WM_COMMAND, MAKEWPARAM((UINT)(id),(UINT)(codeNotify)), (LPARAM)(HWND)(hwndCtl))


所以实际上,FORWARD_WM_XXXX将消息参数进行了重新构造,生成了wParam && lParam,然后调用了我们定义的函数。


前面,我们分析了消息的基本理论和基本的函数及用法,接下来,我们将进一步讨论消息传递在MFC中的实现。
MFC消息的处理实现方式
  初看MFC中的各种消息,以及在头脑中根深蒂固的C++的影响,我们可能很自然的就会想到利用C++的三大特性之一:虚拟机制来实现消息的传递,但是经过分析,我们看到事情并不是想我们想象的那样,在MFC中消息是通过一种所谓的消息映射机制来处理的。
  为什么呢?在潘爱民老师翻译的《Visual C++技术内幕》(第4版)中给出了详细的原因说明,我再简要的说一遍。在CWnd类中大约有110个消息,还有其它的MFC的类呢,算起来消息太多了,在C++中对程序中用到的每一个派生类都要有一个vtable,每一个虚函数在vtable中都要占用一个4字节大小的入口地址,这样一来,对于每个特定类型的窗口或控件,应用程序都需要一个440KB大小的表来支持虚拟消息控件函数。
  如果说上面的窗口或控件可以勉强实现的话,那么对于菜单命令消息及按钮命令消息呢?因为不同的应用程序有不同的菜单和按钮,我们怎么处理呢?在MFC 库的这种消息映射系统就避免了使用大的vtable,并且能够在处理常规Windows消息的同时处理各种各样的应用程序的命令消息。
  说白了,MFC中的消息机制其实质是一张巨大的消息及其处理函数的一一对应表,然后加上分析处理这张表的应用框架内部的一些程序代码.这样就可以避免在SDK编程中用到的繁琐的CASE语句。


MFC的消息映射的基类CCmdTarget
  如果你想让你的控件能够进行消息映射,就必须从CCmdTarget类中派生。CCmdTarget类是MFC处理命令消息的基础、核心。MFC为该类设计了许多成员函数和一些成员数据,基本上是为了解决消息映射问题的,所有响应消息或事件的类都从它派生,例如:应用程序类、框架类、文档类、视图类和各种各样的控件类等等,还有很多。
不过这个类里面有2个函数对消息映射非常重要,一个是静态成员函数DispatchCmdMsg,另一个是虚函数OnCmdMsg。
DispatchCmdMsg专门供MFC内部使用,用来分发Windows消息。OnCmdMsg用来传递和发送消息、更新用户界面对象的状态。
CCmdTarget对OnCmdMsg的默认实现:在当前命令目标(this所指)的类和基类的消息映射数组里搜索指定命令消息的消息处理函数。
  这里使用虚拟函数GetMessageMap得到命令目标类的消息映射入口数组_messageEntries,然后在数组里匹配命令消息ID相同、控制通知代码也相同的消息映射条目。其中GetMessageMap是虚拟函数,所以可以确认当前命令目标的确切类。
如果找到了一个匹配的消息映射条目,则使用DispachCmdMsg调用这个处理函数;
如果没有找到,则使用_GetBaseMessageMap得到基类的消息映射数组,查找,直到找到或搜寻了所有的基类(到CCmdTarget)为止;
如果最后没有找到,则返回FASLE。
  每个从CCmdTarget派生的命令目标类都可以覆盖OnCmdMsg,利用它来确定是否可以处理某条命令,如果不能,就通过调用下一命令目标的 OnCmdMsg,把该命令送给下一个命令目标处理。通常,派生类覆盖OnCmdMsg时,要调用基类的被覆盖的OnCmdMsg。
  在MFC框架中,一些MFC命令目标类覆盖了OnCmdMsg,如框架窗口类覆盖了该函数,实现了MFC的标准命令消息发送路径。必要的话,应用程序也可以覆盖OnCmdMsg,改变一个或多个类中的发送规定,实现与标准框架发送规定不同的发送路径。例如,在以下情况可以作这样的处理:在要打断发送顺序的类中把命令传给一个非MFC默认对象;在新的非默认对象中或在可能要传出命令的命令目标中。


消息映射的内容
    通过ClassWizard为我们生成的代码,我们可以看到,消息映射基本上分为2大部分:
    在头文件(.h)中有一个宏DECLARE_MESSAGE_MAP(),他被放在了类的末尾,是一个public属性的;与之对应的是在实现部分(.cpp)增加了一章消息映射表,内容如下:
    BEGIN_MESSAGE_MAP(当前类, 当前类的基类)
       //{{AFX_MSG_MAP(CMainFrame)
         消息的入口项
       //}}AFX_MSG_MAP
   END_MESSAGE_MAP()
   但是仅是这两项还远不足以完成一条消息,要是一个消息工作,必须有以下3个部分去协作:
1.在类的定义中加入相应的函数声明;
2.在类的消息映射表中加入相应的消息映射入口项;
3.在类的实现中加入相应的函数体;


消息的添加
   有了上面的这些只是作为基础,我们接下来就做我们最熟悉、最常用的工作:添加消息。MFC消息的添加主要有2种方法:自动/手动,我们就以这2种方法为例,说一下如何添加消息。
   1、利用Class Wizard实现自动添加
      在菜单中选择View-->Class Wizard,也可以用单击鼠标右键,选择Class Wizard,同样可以激活Class Wizard。选择Message Map标签,从Class name组合框中选取我们想要添加消息的类。在Object IDs列表框中,选取类的名称。此时, Messages列表框显示该类的大多数(若不是全部的话)可重载成员函数和窗口消息。类重载显示在列表的上部,以实际虚构成员函数的大小写字母来表示。其他为窗口消息,以大写字母出现,描述了实际窗口所能响应的消息ID。选中我们向添加的消息,单击Add Function按钮,Class Wizard自动将该消息添加进来。
      有时候,我们想要添加的消息本应该出现在Message列表中,可是就是找不到,怎么办?不要着急,我们可以利用Class Wizard上Class Info标签以扩展消息列表。在该页中,找到Message Filter组合框,通过它可以改变首页中Messages列表框中的选项。这里,我们选择Window,从而显示所有的窗口消息,一把情况下,你想要添加的消息就可以在Message列表框中出现了,如果还没有,那就接着往下看:)


   2、手动地添加消息处理函数
    如果在Messages列表框中仍然看不到我们想要的消息,那么该消息可能是被系统忽略掉或者是你自己创建的,在这种情况下,就必须自己手工添加。根据我们前面所说的消息工作的3个部件,我们一一进行处理:
      1) 在类的. h文件中添加处理函数的声明,紧接在//}}AFX_MSG行之后加入声明,注意:一定要以afx_msg开头。
     通常,添加处理函数声明的最好的地方是源代码中Class Wizard维护的表下面,但是在它标记其领域的{{}}括弧外面。这些括弧中的任何东西都将会被Class Wizard销毁。
      2) 接着,在用户类的.cpp文件中找到//}}AFX_MSG_MAP行,紧接在它之后加入消息入口项。同样,也是放在{ {} }的外面
      3) 最后,在该文件中添加消息处理函数的实体。
========

MFC的消息机制的实现原理和消息处理的过程



下面几节将分析MFC的消息机制的实现原理和消息处理的过程。为此,首先要分析ClassWizard实现消息映射的内幕,然后讨论MFC的窗口过程,分析MFC窗口过程是如何实现消息处理的。


消息映射的定义和实现
MFC处理的三类消息
根据处理函数和处理过程的不同,MFC主要处理三类消息:


Windows消息,前缀以“WM_”打头,WM_COMMAND例外。Windows消息直接送给MFC窗口过程处理,窗口过程调用对应的消息处理函数。一般,由窗口对象来处理这类消息,也就是说,这类消息处理函数一般是MFC窗口类的成员函数。
控制通知消息,是控制子窗口送给父窗口的WM_COMMAND通知消息。窗口过程调用对应的消息处理函数。一般,由窗口对象来处理这类消息,也就是说,这类消息处理函数一般是MFC窗口类的成员函数。
需要指出的是,Win32使用新的WM_NOFITY来处理复杂的通知消息。WM_COMMAND类型的通知消息仅仅能传递一个控制窗口句柄(lparam)、控制窗ID和通知代码(wparam)。WM_NOTIFY能传递任意复杂的信息。


命令消息,这是来自菜单、工具条按钮、加速键等用户接口对象的WM_COMMAND通知消息,属于应用程序自己定义的消息。通过消息映射机制,MFC框架把命令按一定的路径分发给多种类型的对象(具备消息处理能力)处理,如文档、窗口、应用程序、文档模板等对象。能处理消息映射的类必须从CCmdTarget类派生。
在讨论了消息的分类之后,应该是讨论各类消息如何处理的时候了。但是,要知道怎么处理消息,首先要知道如何映射消息。


MFC消息映射的实现方法
MFC使用ClassWizard帮助实现消息映射,它在源码中添加一些消息映射的内容,并声明和实现消息处理函数。现在来分析这些被添加的内容。


在类的定义(头文件)里,它增加了消息处理函数声明,并添加一行声明消息映射的宏DECLARE_MESSAGE_MAP。


在类的实现(实现文件)里,实现消息处理函数,并使用IMPLEMENT_MESSAGE_MAP宏实现消息映射。一般情况下,这些声明和实现是由MFC的ClassWizard自动来维护的。看一个例子:


在AppWizard产生的应用程序类的源码中,应用程序类的定义(头文件)包含了类似如下的代码:


//{{AFX_MSG(CTttApp)


afx_msg void OnAppAbout();


//}}AFX_MSG


DECLARE_MESSAGE_MAP()


应用程序类的实现文件中包含了类似如下的代码:


BEGIN_MESSAGE_MAP(CTApp, CWinApp)


//{{AFX_MSG_MAP(CTttApp)


ON_COMMAND(ID_APP_ABOUT, OnAppAbout)


//}}AFX_MSG_MAP


END_MESSAGE_MAP()


头文件里是消息映射和消息处理函数的声明,实现文件里是消息映射的实现和消息处理函数的实现。它表示让应用程序对象处理命令消息ID_APP_ABOUT,消息处理函数是OnAppAbout。


为什么这样做之后就完成了一个消息映射?这些声明和实现到底作了些什么呢?接着,将讨论这些问题。


在声明与实现的内部
DECLARE_MESSAGE_MAP宏:
首先,看DECLARE_MESSAGE_MAP宏的内容:


#ifdef _AFXDLL


#define DECLARE_MESSAGE_MAP() \


private: \


static const AFX_MSGMAP_ENTRY _messageEntries[]; \


protected: \


static AFX_DATA const AFX_MSGMAP messageMap; \


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* GetMessageMap() const; \


#endif


DECLARE_MESSAGE_MAP定义了两个版本,分别用于静态或者动态链接到MFC DLL的情形。


BEGIN_MESSAE_MAP宏
然后,看BEGIN_MESSAE_MAP宏的内容:


#ifdef _AFXDLL


#define BEGIN_MESSAGE_MAP(theClass, baseClass) \


const AFX_MSGMAP* PASCAL theClass::_GetBaseMessageMap() \


{ return &baseClass::messageMap; } \


const AFX_MSGMAP* theClass::GetMessageMap() const \


{ return &theClass::messageMap; } \


AFX_DATADEF const AFX_MSGMAP theClass::messageMap = \


{ &theClass::_GetBaseMessageMap,&theClass::_messageEntries[0] }; \


const AFX_MSGMAP_ENTRY theClass::_messageEntries[] = \


{ \


#else


#define BEGIN_MESSAGE_MAP(theClass, baseClass) \


const AFX_MSGMAP* theClass::GetMessageMap() const \


{ return &theClass::messageMap; } \


AFX_DATADEF const AFX_MSGMAP theClass::messageMap = \


{ &baseClass::messageMap,&theClass::_messageEntries[0] }; \


const AFX_MSGMAP_ENTRY theClass::_messageEntries[] = \


{ \


#endif


#define END_MESSAGE_MAP() \


{0, 0, 0, 0, AfxSig_end, (AFX_PMSG)0 } \


}; \


对应地,BEGIN_MESSAGE_MAP定义了两个版本,分别用于静态或者动态链接到MFC DLL的情形。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\


};


消息映射声明的解释
在清楚了有关宏的定义之后,现在来分析它们的作用和功能。


消息映射声明的实质是给所在类添加几个静态成员变量和静态或虚拟函数,当然它们是与消息映射相关的变量和函数。


成员变量
有两个成员变量被添加,第一个是_messageEntries,第二个是messageMap。


第一个成员变量的声明:
AFX_MSGMAP_ENTRY_messageEntries[]


这是一个AFX_MSGMAP_ENTRY类型的数组变量,是一个静态成员变量,用来容纳类的消息映射条目。一个消息映射条目可以用AFX_MSGMAP_ENTRY结构来描述。


AFX_MSGMAP_ENTRY结构的定义如下:


struct AFX_MSGMAP_ENTRY


{


//Windows消息ID


UINT nMessage;


//控制消息的通知码


UINT nCode;


//Windows Control的ID


UINT nID;


//如果是一定范围的消息被映射,则nLastID指定其范围


UINT nLastID;


UINT nSig;//消息的动作标识


//响应消息时应执行的函数(routine to call (orspecial value))


AFX_PMSG pfn;


};


从上述结构可以看出,每条映射有两部分的内容:第一部分是关于消息ID的,包括前四个域;第二部分是关于消息对应的执行函数,包括后两个域。


在上述结构的六个域中,pfn是一个指向CCmdTarger成员函数的指针。函数指针的类型定义如下:


typedef void (AFX_MSG_CALL CCmdTarget::*AFX_PMSG)(void);


当使用一条或者多条消息映射条目初始化消息映射数组时,各种不同类型的消息函数都被转换成这样的类型:不接收参数,也不返回参数的类型。因为所有可以有消息映射的类都是从CCmdTarge派生的,所以可以实现这样的转换。


nSig是一个标识变量,用来标识不同原型的消息处理函数,每一个不同原型的消息处理函数对应一个不同的nSig。在消息分发时,MFC内部根据nSig把消息派发给对应的成员函数处理,实际上,就是根据nSig的值把pfn还原成相应类型的消息处理函数并执行它。


第二个成员变量的声明
AFX_MSGMAP messageMap;


这是一个AFX_MSGMAP类型的静态成员变量,从其类型名称和变量名称可以猜出,它是一个包含了消息映射信息的变量。的确,它把消息映射的信息(消息映射数组)和相关函数打包在一起,也就是说,得到了一个消息处理类的该变量,就得到了它全部的消息映射数据和功能。AFX_MSGMAP结构的定义如下:


struct AFX_MSGMAP


{


//得到基类的消息映射入口地址的数据或者函数


#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和函数_GetBaseMessageMap的地址(不使用MFC DLL时,是基类消息映射数组的地址)。


成员函数
_GetBaseMessageMap()
用来得到基类消息映射的函数。


GetMessageMap()
用来得到自身消息映射的函数。


消息映射实现的解释
消息映射实现的实质是初始化声明中定义的静态成员函数_messageEntries和messageMap,实现所声明的静态或虚拟函数GetMessageMap、_GetBaseMessageMap。


这样,在进入WinMain函数之前,每个可以响应消息的MFC类都生成了一个消息映射表,程序运行时通过查询该表判断是否需要响应某条消息。


对消息映射入口表(消息映射数组)的初始化
如前所述,消息映射数组的元素是消息映射条目,条目的格式符合结构AFX_MESSAGE_ENTRY的描述。所以,要初始化消息映射数组,就必须使用符合该格式的数据来填充:如果指定当前类处理某个消息,则把和该消息有关的信息(四个)和消息处理函数的地址及原型组合成为一个消息映射条目,加入到消息映射数组中。


显然,这是一个繁琐的工作。为了简化操作,MFC根据消息的不同和消息处理方式的不同,把消息映射划分成若干类别,每一类的消息映射至少有一个共性:消息处理函数的原型相同。对每一类消息映射,MFC定义了一个宏来简化初始化消息数组的工作。例如,前文提到的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的函数类型为参数空(Void)、返回空(Void)。


在消息映射数组的最后,是宏END_MESSAGE_MAP的内容,它标识消息处理类的消息映射条目的终止。


对messageMap的初始化
如前所述,messageMap的类型是AFX_MESSMAP。


经过初始化,域lpEntries保存了消息映射数组_messageEntries的地址;如果动态链接到MFC DLL,则pfnGetBaseMap保存了_GetBaseMessageMap成员函数的地址;否则pBaseMap保存了基类的消息映射数组的地址。


对函数的实现
_GetBaseMessageMap()


它返回基类的成员变量messagMap(当使用MFC DLL时),使用该函数得到基类消息映射入口表。


GetMessageMap():


它返回成员变量messageMap,使用该函数得到自身消息映射入口表。


顺便说一下,消息映射类的基类CCmdTarget也实现了上述和消息映射相关的函数,不过,它的消息映射数组是空的。


既然消息映射宏方便了消息映射的实现,那么有必要详细的讨论消息映射宏。下一节,介绍消息映射宏的分类、用法和用途。


消息映射宏的种类
为了简化程序员的工作,MFC定义了一系列的消息映射宏和像AfxSig_vv这样的枚举变量,以及标准消息处理函数,并且具体地实现这些函数。这里主要讨论消息映射宏,常用的分为以下几类。


用于Windows消息的宏,前缀为“ON_WM_”。
这样的宏不带参数,因为它对应的消息和消息处理函数的函数名称、函数原型是确定的。MFC提供了这类消息处理函数的定义和缺省实现。每个这样的宏处理不同的Windows消息。


例如:宏ON_WM_CREATE()把消息WM_CREATE映射到OnCreate函数,消息映射条目的第一个成员nMessage指定为要处理的Windows消息的ID,第二个成员nCode指定为0。


用于命令消息的宏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类型的值。


(6)扩展消息映射宏


很多普通消息映射宏都有对应的扩展消息映射宏,例如:ON_COMMAND对应的ON_COMMAND_EX,ON_ONTIFY对应的ON_ONTIFY_EX,等等。扩展宏除了具有普通宏的功能,还有特别的用途。关于扩展宏的具体讨论和分析,见4.4.3.2节。


作为一个总结,下表列出了这些常用的消息映射宏。


表4-1 常用的消息映射宏


消息映射宏


用途


ON_COMMAND


把command message映射到相应的函数


ON_CONTROL


把control notification message映射到相应的函数。MFC根据不同的控制消息,在此基础上定义了更具体的宏,这样用户在使用时就不需要指定通知代码ID,如ON_BN_CLICKED。


ON_MESSAGE


把user-defined message.映射到相应的函数


ON_REGISTERED_MESSAGE


把registered user-defined message映射到相应的函数,实际上nMessage等于0x0C000,nSig等于宏的消息参数。nSig的真实值为Afxsig_lwl。


ON_UPDATE_COMMAND_UI


把user interface user update command message映射到相应的函数上。


ON_COMMAND_RANGE


把一定范围内的command IDs 映射到相应的函数上


ON_UPDATE_COMMAND_UI_RANGE


把一定范围内的user interface user update command message映射到相应的函数上


ON_CONTROL_RANGE


把一定范围内的control notification message映射到相应的函数上


在表4-1中,宏ON_REGISTERED_MESSAGE的定义如下:


#define ON_REGISTERED_MESSAGE(nMessageVariable, memberFxn) \


{ 0xC000, 0, 0, 0,\


(UINT)(UINT*)(&nMessageVariable), \


/*implied 'AfxSig_lwl'*/ \


(AFX_PMSG)(AFX_PMSGW)(LRESULT\


(AFX_MSG_CALL CWnd::*)\


(WPARAM, LPARAM))&memberFxn }


从上面的定义可以看出,实际上,该消息被映射到WM_COMMAND(0XC000),指定的registered消息ID存放在nSig域内,nSig的值在这样的映射条目下隐含地定为AfxSig_lwl。由于ID和正常的nSig域存放的值范围不同,所以MFC可以判断出是否是registered消息映射条目。如果是,则使用AfxSig_lwl把消息处理函数转换成参数1为Word、参数2为long、返回值为long的类型。


在介绍完了消息映射的内幕之后,应该讨论消息处理过程了。由于CCmdTarge的特殊性和重要性,在4.3节先对其作一个大略的介绍。


CcmdTarget类
除了CObject类外,还有一个非常重要的类CCmdTarget。所有响应消息或事件的类都从它派生。例如,CWinapp,CWnd,CDocument,CView,CDocTemplate,CFrameWnd,等等。


CCmdTarget类是MFC处理命令消息的基础、核心。MFC为该类设计了许多成员函数和一些成员数据,基本上是为了解决消息映射问题的,而且,很大一部分是针对OLE设计的。在OLE应用中,CCmdTarget是MFC处理模块状态的重要环节,它起到了传递模块状态的作用:其构造函数获取当前模块状态,并保存在成员变量m_pModuleState里头。关于模块状态,在后面章节讲述。


CCmdTarget有两个与消息映射有密切关系的成员函数:DispatchCmdMsg和OnCmdMsg。


静态成员函数DispatchCmdMsg
CCmdTarget的静态成员函数DispatchCmdMsg,用来分发Windows消息。此函数是MFC内部使用的,其原型如下:


static BOOL DispatchCmdMsg(


CCmdTarget* pTarget,


UINT nID,


int nCode,


AFX_PMSG pfn,


void* pExtra,


UINT nSig,


AFX_CMDHANDLERINFO* pHandlerInfo)


关于此函数将在4.4.3.2章节命令消息的处理中作更详细的描述。


虚拟函数OnCmdMsg
CCmdTarget的虚拟函数OnCmdMsg,用来传递和发送消息、更新用户界面对象的状态,其原型如下:


OnCmdMsg(


UINT nID,


int nCode,


void* pExtra,


AFX_CMDHANDLERINFO* pHandlerInfo)


框架的命令消息传递机制主要是通过该函数来实现的。其参数描述参见4.3.3.2章节DispacthCMdMessage的参数描述。


在本书中,命令目标指希望或者可能处理消息的对象;命令目标类指命令目标的类。


CCmdTarget对OnCmdMsg的默认实现:在当前命令目标(this所指)的类和基类的消息映射数组里搜索指定命令消息的消息处理函数(标准Windows消息不会送到这里处理)。


这里使用虚拟函数GetMessageMap得到命令目标类的消息映射入口数组_messageEntries,然后在数组里匹配指定的消息映射条目。匹配标准:命令消息ID相同,控制通知代码相同。因为GetMessageMap是虚拟函数,所以可以确认当前命令目标的确切类。


如果找到了一个匹配的消息映射条目,则使用DispachCmdMsg调用这个处理函数;


如果没有找到,则使用_GetBaseMessageMap得到基类的消息映射数组,查找,直到找到或搜寻了所有的基类(到CCmdTarget)为止;


如果最后没有找到,则返回FASLE。


每个从CCmdTarget派生的命令目标类都可以覆盖OnCmdMsg,利用它来确定是否可以处理某条命令,如果不能,就通过调用下一命令目标的OnCmdMsg,把该命令送给下一个命令目标处理。通常,派生类覆盖OnCmdMsg时,要调用基类的被覆盖的OnCmdMsg。


在MFC框架中,一些MFC命令目标类覆盖了OnCmdMsg,如框架窗口类覆盖了该函数,实现了MFC的标准命令消息发送路径。具体实现见后续章节。


必要的话,应用程序也可以覆盖OnCmdMsg,改变一个或多个类中的发送规定,实现与标准框架发送规定不同的发送路径。例如,在以下情况可以作这样的处理:在要打断发送顺序的类中把命令传给一个非MFC默认对象;在新的非默认对象中或在可能要传出命令的命令目标中。


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


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


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


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


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


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


LRESULT CALLBACK


AfxWndProc(HWND hWnd, UINT nMsg, WPARAM wParam, LPARAMlParam)


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函数完成的,分析该函数的流程,如图4-1所示。


图4-1中的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消息和命令消息的处理不一样,前者没有消息分发的过程。


对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被调用。下面描述每一步的详细处理。


从窗口过程到消息映射
首先,分析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将在讨论对话框等的实现时具体分析。


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,只有相等才调用对应的消息处理函数。


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来处理。


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


所有处理消息的类从CCmdTarget派生。
使用静态成员变量_messageEntries数组存放消息映射条目,使用静态成员变量messageMap来唯一地区别和得到类的消息映射。
通过GetMessage虚拟函数来获取当前对象的类的messageMap变量,进而得到消息映射入口。
按照先底层,后基层的顺序在类的消息映射数组中搜索消息处理函数。基于这样的机制,一般在覆盖基类的消息处理函数时,应该调用基类的同名函数。
以上论断适合于MFC其他消息处理机制,如对命令消息的处理等。不同的是其他消息处理有一个命令派发/分发的过程。


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


对命令消息的接收和处理
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的标准命令发送路径:


送给活动(Active)视处理,调用活动视的OnCmdMsg。由于当前对象是MFC视对象,所以,OnCmdMsg将搜索CTview及其基类的消息映射数组,试图得到相应的处理函数。
如果视对象自己不处理,则视得到和它关联的文档,调用关联文档的OnCmdMsg。由于当前对象是MFC视对象,所以,OnCmdMsg将搜索CTdoc及其基类的消息映射数组,试图得到相应的处理函数。
如果文档对象不处理,则它得到管理文档的文档模板对象,调用文档模板的OnCmdMsg。由于当前对象是MFC文档模板对象,所以,OnCmdMsg将搜索文档模板类及其基类的消息映射数组,试图得到相应的处理函数。
如果文档模板不处理,则把没有处理的信息逐级返回:文档模板告诉文档对象,文档对象告诉视对象,视对象告诉边框窗口对象。最后,边框窗口得知,视、文档、文档模板都没有处理消息。
CFrameWnd的OnCmdMsg继续调用CWnd::OnCmdMsg(斜体表示有类属限制)来处理消息。由于CWnd没有覆盖OnCmdMsg,故实际上调用了函数CCmdTarget::OnCmdMsg。由于当前对象是MFC边框窗口对象,所以OnCmdMsg函数将搜索CMainFrame类及其所有基类的消息映射数组,试图得到相应的处理函数。CWnd没有实现OnCmdMsg却指定要执行其OnCmdMsg函数,可能是为了以后MFC给CWnd实现了OnCmdMsg之后其他代码不用改变。
这一步是边框窗口自己尝试处理消息。


如果边框窗口对象不处理,则送给应用程序对象处理。调用CTApp的OnCmdMsg,由于实际上CTApp及其基类CWinApp没有覆盖OnCmdMsg,故实际上调用了函数CCmdTarget::OnCmdMsg。由于当前对象是MFC应用程序对象,所以OnCmdMsg函数将搜索CTApp类及其所有基类的的消息映射入口数组,试图得到相应的处理函数
如果应用程序对象不处理,则返回FALSE,表明没有命令目标处理当前的命令消息。这样,函数逐级别返回,OnCmdMsg告诉OnCommand消息没有被处理,OnCommand告诉OnWndMsg消息没有被处理,OnWndMsg告诉WindowProc消息没有被处理,于是WindowProc调用DefWindowProc进行缺省处理。
本例子在第六步中,应用程序对ID_APP_ABOUT消息作了处理。它找到处理函数CTApp::OnAbout,使用DispatchCmdMsg派发消息给该函数处理。


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


命令消息的派发和消息的多次处理
命令消息的派发
如前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消息的处理是一样的。


消息的多次处理
如果消息处理函数不返回值,则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节。


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


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


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


下面,讨论几个MFC类覆盖OnCmdMsg虚拟函数时的实现。这些类的OnCmdMsg或者可能是标准MFC命令消息路径的一个环节,或者可能是一个独立的处理过程(对于其中的MFC窗口类)。


从分析CView的OnCmdMsg实现开始。


CView的OnCmdMsg
CView::OnCmdMsg(UINT nID, int nCode, void* pExtra,


AFX_CMDHANDLERINFO* pHandlerInfo)


首先,调用CWnd::OnCmdMsg,结果是搜索当前视的类和基类的消息映射数组,搜索顺序是从下层到上层。若某一层实现了对命令消息nID的处理,则调用它的实现函数;否则,调用m_pDocument->OnCmdMsg,把命令消息送给文档类处理。m_pDocument是和当前视关联的文档对象指针。如果文档对象类实现了OnCmdMsg,则调用它的覆盖函数;否则,调用基类(例如CDocument)的OnCmdMsg。


接着,讨论CDocument的实现。


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


AFX_CMDHANDLERINFO* pHandlerInfo)


首先,调用CCmdTarget::OnCmdMsg,导致当前对象(this)的类和基类的消息映射数组被搜索,看是否有对应的消息处理函数可用。如果有,就调用它;如果没有,则调用文档模板的OnCmdMsg函数(m_pTemplate->OnCmdMsg)把消息送给文档模板处理。


MFC文档模板没有覆盖OnCmdMsg,导致基类CCmdTarget的OnCmdMsg被调用,看是否有文档模板类或基类实现了对消息的处理。是的话,调用对应的消息处理函数,否则,返回FALSE。从前面的分析知道,CCmdTarget类的消息映射数组是空的,所以这里返回FALSE。


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


AFX_CMDHANDLERINFO* pHandlerInfo)


调用CWnd::OnCmdMsg,让对话框或其基类处理消息。
如果还没有处理,而且是控制消息或系统命令或非命令按钮,则返回FALSE,不作进一步处理。否则,调用父窗口的OnCmdmsg(GetParent()->OnCmdmsg)把消息送给父窗口处理。
如果仍然没有处理,则调用当前线程的OnCmdMsg(GetThread()->OnCmdMsg)把消息送给线程对象处理。
如果最后没有处理,返回FALSE。
CMDIFrameWnd的OnCmdMsg
对于MDI应用程序,MDI主边框窗口首先是把命令消息发送给活动的MDI文档边框窗口进行处理。MDI主边框窗口对OnCmdMsg的实现函数的原型如下:


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


AFX_CMDHANDLERINFO* pHandlerInfo)


如果有激活的文档边框窗口,则调用它的OnCmdMsg(MDIGetActive()->OnCmdMsg)把消息交给它进行处理。MFC的文档边框窗口类并没有覆盖OnCmdMsg函数,所以基类CFrameWnd的函数被调用,导致文档边框窗口的活动视、文档边框窗口本身、应用程序对象依次来进行消息处理。
如果文档边框窗口没有处理,调用CFrameWnd::OnCmdMsg把消息按标准路径发送,重复第一次的步骤,不过对于MDI边框窗口来说不存在活动视,所以省却了让视处理消息的必要;接着让MDI边框窗口本身来处理消息,如果它还没有处理,则让应用程序对象进行消息处理──虽然这是一个无用的重复。
除了CView、CDocument和CMDIFrameWnd类,还有几个OLE相关的类覆盖了OnCmdMsg函数。OLE的处理本书暂不涉及,CDialog::OnCmdMsg将在对话框章节专项讨论其具体实现。


一些消息处理类的OnCommand的实现
除了虚拟函数OnCmdMsg,还有一个虚拟函数OnCommand在命令消息的发送中占有重要地位。在处理命令或者通知消息时,OnCommand被MFC窗口过程调用,然后它调用OnCmdMsg按一定路径传送消息。除了CWnd类和一些OLE相关类外,MFC里主要还有MDI边框窗口实现了OnCommand。


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


第一,如果存在活动的文档边框窗口,则使用AfxCallWndProc调用它的窗口过程,把消息送给文档边框窗口来处理。这将导致文档边框窗口的OnCmdMsg作如下的处理:


活动视处理消息→与视关联的文档处理消息→本文档边框窗口处理消息→应用程序对象处理消息→文档边框窗口缺省处理


任何一个环节如果处理消息,则不再向下发送消息,处理终止。如果消息仍然没有被处理,就只有交给主边框窗口了。


第二,第一步没有处理命令,继续调用CFrameWnd::OnCommand,将导致CMDIFrameWnd的OnCmdMsg被调用。从前面的分析知道,将再次把消息送给MDI边框窗口的活动文档边框窗口,第一步的过程除了文档边框窗口缺省处理外都将被重复。具体的处理过程见前文的CMDIFrameWnd::OnCmdMsg函数。


对于MDI消息,如果主边框窗口还不处理的话,交给CMDIFrameWnd的DefWindowProc作缺省处理。
消息没有处理,返回FALSE。
上述分析综合了OnCommand和OnCmdMsg的处理,它们是在MFC内部MDI边框窗口处理命令消息的完整的流程和标准的步骤。整个处理过程再次表明了边框窗口在处理命令消息时的中心作用。从程序员的角度来看,可以认为整个标准处理路径如下:


活动视处理消息→与视关联的文档处理消息→本文档边框窗口处理消息→应用程序对象处理消息→文档边框窗口缺省处理→MDI边框窗口处理消息→MDI边框窗口缺省处理


任何一个环节如果处理消息,不再向下发送消息,急处理终止。


对控制通知消息的接收和处理
WM_COMMAND控制通知消息的处理
WM_COMMAND控制通知消息的处理和WM_COMMAND命令消息的处理类似,但是也有不同之处。


首先,分析处理WM_COMMAND控制通知消息和命令消息的相似处。如前所述,命令消息和控制通知消息都是由窗口过程给OnCommand处理(参见CWnd::OnWndMsg的实现),OnCommand通过wParam和lParam参数区分是命令消息或通知消息,然后送给OnCmdMsg处理(参见CWnd::OnCommnd的实现)。


其次,两者的不同之处是:


命令消息一般是送给主边框窗口的,这时,边框窗口的OnCmdMsg被调用;而控制通知消息送给控制子窗口的父窗口,这时,父窗口的OnCmdMsg被调用。
OnCmdMsg处理命令消息时,通过命令分发可以由多种命令目标处理,包括非窗口对象如文档对象等;而处理控制通知消息时,不会有消息分发的过程,控制通知消息最终肯定是由窗口对象处理的。
不过,在某种程度上可以说,控制通知消息由窗口对象处理是一种习惯和约定。当使用ClassWizard进行消息映射时,它不提供把控制通知消息映射到非窗口对象的机会。但是,手工地添加消息映射,让非窗口对象处理控制通知消息的可能是存在的。例如,对于CFormView,一方面它具备接受WM_COMMAND通知消息的条件,另一方面,具备把WM_COMMAND消息派发给关联文档对象处理的能力,所以给CFormView的通知消息是可以让文档对象处理的。


事实上,BN_CLICKED控制通知消息的处理和命令消息的处理完全一样,因为该消息的通知代码是0,ON_BN_CLICKED(id,memberfunction)和ON_COMMAND(id,memberfunction)是等同的。


此外,MFC的状态更新处理机制就是建立在通知消息可以发送给各种命令目标的基础之上的。关于MFC的状态更新处理机制,见后面4.4.4.4节的讨论。


控制通知消息可以反射给子窗口处理。OnCommand判定当前消息是WM_COMAND通知消息之后,首先它把消息反射给控制子窗口处理,如果子窗口处理了反射消息,OnCommand不会继续调用OnCmdMsg让父窗口对象来处理通知消息。
WM_NOTIFY消息及其处理:
(1)WM_NOTIFY消息


还有一种通知消息WM_NOTIFY,在Win32中用来传递信息复杂的通知消息。WM_NOTIFY消息怎么来传递复杂的信息呢?WM_NOTIFY的消息参数wParam包含了发送通知消息的控制窗口ID,另一个参数lParam包含了一个指针。该指针指向一个NMHDR结构,或者更大的结构,只要它的第一个结构成员是NMHDR结构。


NMHDR结构:


typedef struct tagNMHDR {


HWND hwndFrom;


UINT idFrom;


UINT code;


} NMHDR;


上述结构有三个成员,分别是发送通知消息的控制窗口的句柄、ID和通知消息代码。


举一个更大、更复杂的结构例子:列表控制窗发送LVN_KEYDOWN控制通知消息,则lParam包含了一个指向LV_KEYDOWN结构的指针。其结构如下:


typedef struct tagLV_KEYDOWN {


NMHDR hdr;


WORD wVKey;


UINT flags;


}LV_KEYDOWN;


它的第一个结构成员hdr就是NMHDR类型。其他成员包含了更多的信息:哪个键被按下,哪些辅助键(SHIFT、CTRL、ALT等)被按下。


(2)WM_NOTIFY消息的处理


在分析CWnd::OnWndMsg函数时,曾指出当消息是WM_NOTIFY时,它把消息传递给OnNotify虚拟函数处理。这是一个虚拟函数,类似于OnCommand,CWnd和派生类都可以覆盖该函数。OnNotify的函数原型如下:


BOOL CWnd::OnNotify(WPARAM, LPARAM lParam, LRESULT* pResult)


参数1是发送通知消息的控制窗口ID,没有被使用;参数2是一个指针;参数3指向一个long类型的数据,用来返回处理结果。


WM_NOTIFY消息的处理过程如下:


反射消息给控制子窗口处理。
如果子窗口不处理反射消息,则交给OnCmdMsg处理。给OnCmdMsg的四个参数分别如下:第一个是命令消息ID,第四个为空;第二个高阶word是WM_NOTIFY,低阶word是通知消息;第三个参数是指向AFX_NOTIFY结构的指针。第二、三个参数有别于OnCommand送给OnCmdMsg的参数。
AFX_NOTIFY结构:


struct AFX_NOTIFY


{


LRESULT* pResult;


NMHDR* pNMHDR;


};


pNMHDR的值来源于参数2 lParam,该结构的域pResult用来保存处理结果,域pNMHDR用来传递信息。


OnCmdMsg后续的处理和WM_COMMAND通知消息基本相同,只是在派发消息给消息处理函数时,DispatchMsdMsg的第五个参数pExtra指向OnCmdMsg传递给它的AFX_NOTIFY类型的参数,而不是空指针。这样,处理函数就得到了复杂的通知消息信息。


消息反射
(1)消息反射的概念


前面讨论控制通知消息时,曾经多次提到了消息反射。MFC提供了两种消息反射机制,一种用于OLE控件,一种用于Windows控制窗口。这里只讨论后一种消息反射。


Windows控制常常发送通知消息给它们的父窗口,通常控制消息由父窗口处理。但是在MFC里头,父窗口在收到这些消息后,或者自己处理,或者反射这些消息给控制窗口自己处理,或者两者都进行处理。如果程序员在父窗口类覆盖了通知消息的处理(假定不调用基类的实现),消息将不会反射给控制子窗口。这种反射机制是MFC实现的,便于程序员创建可重用的控制窗口类。


MFC的CWnd类处理以下控制通知消息时,必要或者可能的话,把它们反射给子窗口处理:


WM_CTLCOLOR,


WM_VSCROLL,WM_HSCROLL,


WM_DRAWITEM,WM_MEASUREITEM,


WM_COMPAREITEM,WM_DELETEITEM,


WM_CHARTOITEM,WM_VKEYTOITEM,


WM_COMMAND、WM_NOTIFY。


例如,对WM_VSCROLL、WM_HSCROLL消息的处理,其消息处理函数如下:


void CWnd::OnHScroll(UINT, UINT, CScrollBar* pScrollBar)


{


//如果是一个滚动条控制,首先反射消息给它处理


if (pScrollBar != NULL &&pScrollBar->SendChildNotifyLastMsg())


return; //控制窗口成功处理了该消息


Default();


}


又如:在讨论OnCommand和OnNofity函数处理通知消息时,都曾经指出,它们首先调用ReflectLastMsg把消息反射给控制窗口处理。


为了利用消息反射的功能,首先需要从适当的MFC窗口派生出一个控制窗口类,然后使用ClassWizard给它添加消息映射条目,指定它处理感兴趣的反射消息。下面,讨论反射消息映射宏。


上述消息的反射消息映射宏的命名遵循以下格式:“ON”前缀+消息名+“REFLECT”后缀,例如:消息WM_VSCROLL的反射消息映射宏是ON_WM_VSCROLL_REFECT。但是通知消息WM_COMMAND和WM_NOTIFY是例外,分别为ON_CONTROL_REFLECT和ON_NOFITY_REFLECT。状态更新通知消息的反射消息映射宏是ON_UPDATE_COMMAND_UI_REFLECT。


消息处理函数的名字和去掉“WM_”前缀的消息名相同 ,例如WM_HSCROLL反射消息处理函数是Hscroll。


消息处理函数的原型这里不一一列举了。


这些消息映射宏和消息处理函数的原型可以借助于ClassWizard自动地添加到程序中。ClassWizard添加消息处理函数时,可以处理的反射消息前面有一个等号,例如处理WM_HSCROLL的反射消息,选择映射消息“=EN_HSC ROLL”。ClassWizard自动的添加消息映射宏和处理函数到框架文件。


(2)消息反射的处理过程


如果不考虑有OLE控件的情况,消息反射的处理流程如下图所示:


首先,调用CWnd的成员函数SendChildNotifyLastMsg,它从线程状态得到本线程最近一次获取的消息(关于线程状态,后面第9章会详细介绍)和消息参数,并且把这些参数传递给函数OnChildNotify。注意,当前的CWnd对象就是MFC控制子窗口对象。


OnChlidNofify是CWnd定义的虚拟函数,不考虑OLE控制的话,它仅仅只调用ReflectChildNotify。OnChlidNofify可以被覆盖,所以如果程序员希望处理某个控制的通知消息,除了采用消息映射的方法处理通知反射消息以外,还可以覆盖OnChlidNotify虚拟函数,如果成功地处理了通知消息,则返回TRUE。


ReflectChildNotify是CWnd的成员函数,完成反射消息的派发。对于WM_COMMAND,它直接调用CWnd::OnCmdMsg派发反射消息WM_REFLECT_BASE+WM_COMMAND;对于WM_NOTIFY,它直接调用CWnd::OnCmdMsg派发反射消息WM_REFLECT_BASE+WM_NOFITY;对于其他消息,则直接调用CWnd::OnWndMsg(即CmdTarge::OnWndMsg)派发相应的反射消息,例如WM_REFLECT_BASE+WM_HSCROLL。


注意:ReflectChildNotify直接调用了CWnd的OnCmdMsg或OnWndMsg,这样反射消息被直接派发给控制子窗口,省却了消息发送的过程。


接着,控制子窗口如果处理了当前的反射消息,则返回反射消息被成员处理的信息。


(3)一个示例


如果要创建一个编辑框控制,要求它背景使用黄色,其他特性不变,则可以从CEdit派生一个类CYellowEdit,处理通知消息WM_CTLCOLOR的反射消息。CYellowEdit有三个属性,定义如下:


CYellowEdit::CYellowEdit()


{


m_clrText = RGB( 0, 0, 0 );


m_clrBkgnd = RGB( 255, 255, 0 );


m_brBkgnd.CreateSolidBrush( m_clrBkgnd );


}


使用ClassWizard添加反射消息处理函数:


函数原型:


afx_msg void HScroll();


消息映射宏:


ON_WM_CTLCOLOR_REFLECT()


函数的框架


HBRUSH CYellowEdit::CtlColor(CDC* pDC, UINT nCtlColor)


{


// TODO:添加代码改变设备描述表的属性


// TODO: 如果不再调用父窗口的处理,则返回一个非空的刷子句柄


return NULL;


}


添加一些处理到函数CtlColor中,如下:


pDC->SetTextColor( m_clrText );//设置文本颜色


pDC->SetBkColor( m_clrBkgnd );//设置背景颜色


return m_brBkgnd; //返回背景刷


这样,如果某个地方需要使用黄色背景的编辑框,则可以使用CYellowEdit控制。


对更新命令的接收和处理
用户接口对象如菜单、工具条有多种状态,例如:禁止,可用,选中,未选中,等等。这些状态随着运行条件的变化,由程序来进行更新。虽然程序员可以自己来完成更新,但是MFC框架为自动更新用户接口对象提供了一个方便的接口,使用它对程序员来说可能是一个好的选择。


实现方法
每一个用户接口对象,如菜单、工具条、控制窗口的子窗口,都由唯一的ID号标识,用户和它们交互时,产生相应ID号的命令消息。在MFC里,一个用户接口对象还可以响应CN_UPDATE_COMMAND_UI通知消息。因此,对每个标号ID的接口对象,可以有两个处理函数:一个消息处理函数用来处理该对象产生的命令消息ID,另一个状态更新函数用来处理给该对象的CN_UPDATE_COMMAND_UID的通知消息。


使用ClassWizard可把状态更新函数加入到某个消息处理类,其结果是:


在类的定义中声明一个状态函数;


在消息映射中使用ON_UPDATE_COMMAND_UI宏添加一个映射条目;


在类的实现文件中实现状态更新函数的定义。


ON_UPDATE_COMMAND_UI给指定ID的用户对象指定状态更新函数,例如:


ON_UPDATE_COMMAND_UI(ID_EDIT_COPY, OnUpdateEditCopy)


映射标识号ID为ID_EDIT_COPY菜单的通知消息CN_UPDATE_COMMAND_UI到函数OnUpdateEditCopy。用于给EDIT(编辑菜单)的菜单项ID_EDIT_COPY(复制)添加一个状态处理函数OnUpdateEditCopy,通过处理通知消息CN_UPDATE_COMMAND_UI实现该菜单项的状态更新。


状态处理函数的原型如下:


afxmsg void ClassName::OnUpdateEditPaste(CCmdUI* pCmdUI)


CCmdUI对象由MFC自动地构造。在完善函数的实现时,使用pCmdUI对象和CmdUI的成员函数实现菜单项ID_EDIT_COPY的状态更新,让它变灰或者变亮,也就是禁止或者允许用户使用该菜单项。


状态更新命令消息
要讨论MFC的状态更新处理,先得了解一条特殊的消息。MFC的消息映射机制除了处理各种Windows消息、控制通知消息、命令消息、反射消息外,还处理一种特别的“通知命令消息”,并通过它来更新菜单、工具栏(包括对话框工具栏)等命令目标的状态。


这种“通知命令消息”是MFC内部定义的,消息ID是WM_COMMAND,通知代码是CN_UPDATE_COMMAND_UI(0XFFFFFFFF)。


它不是一个真正意义上的通知消息,因为没有控制窗口产生这样的通知消息,而是MFC自己主动产生,用于送给工具条窗口或者主边框窗口,通知它们更新用户接口对象的状态。


它和标准WM_COMMAND命令消息也不相同,因为它有特定的通知代码,而命令消息通知代码是0。


但是,从消息的处理角度,可以把它看作是一条通知消息。如果是工具条窗口接收该消息,则在发送机制上它和WM_COMMAND控制通知消息是相同的,相当于让工具条窗口处理一条通知消息。如果是边框窗口接收该消息,则在消息的发送机制上它和WM_COMMAND命令消息是相同的,可以让任意命令目标处理该消息,也就是说边框窗口可以把该条通知消息发送给任意命令目标处理。


从程序员的角度,可以把它看作一条“状态更新命令消息”,像处理命令消息那样处理该消息。每条命令消息都可以对应有一条“状态更新命令消息”。ClassWizard也支持让任意消息目标处理“状态更新命令消息”(包括非窗口命令目标),实现用户接口状态的更新。


在这条消息发送时,通过OnCmdMsg的第三个参数pExtra传递一些信息,表示要更新的用户接口对象。pExtra指向一个CCmdUI对象。这些信息将传递给状态更新命令消息的处理函数。


下面讨论用于更新用户接口对象状态的类CCmdUI。


类CCmdUI
CCmdUI不是从CObject派生,没有基类。


成员变量
m_nID 用户接口对象的ID


m_nIndex 用户接口对象的index


m_pMenu 指向CCmdUI对象表示的菜单


m_pSubMenu 指向CCmdUI对象表示的子菜单


m_pOther 指向其他发送通知消息的窗口对象


m_pParentMenu 指向CCmdUI对象表示的子菜单


成员函数
Enable(BOOL bOn = TRUE ) 禁止用户接口对象或者使之可用


SetCheck( int nCheck = 1) 标记用户接口对象选中或未选中


SetRadio(BOOL bOn = TRUE)


SetText(LPCTSTR lpszText)


ContinueRouting()


还有一个MFC内部使用的成员函数:


DoUpdate(CCmdTarget* pTarget, BOOL bDisableIfNoHndler)


其中,参数1指向处理接收更新通知的命令目标,一般是边框窗口;参数2指示如果没有提供处理函数(例如某个菜单没有对应的命令处理函数),是否禁止用户对象。


DoUpdate作以下事情:


首先,发送状态更新命令消息给参数1表示的命令目标:调用pTarget->OnCmdMsg(m_nID, CN_UPDATE_COMMAND_UI, this, NULL)发送m_nID对象的通知消息CN_UPDATE_COMMAND_UI。OnCmdMsg的参数3取值this,包含了当前要更新的用户接口对象的信息。


然后,如果参数2为TRUE,调用pTarget->OnCmdMsg(m_nID,CN_COMMAND, this, &info)测试命令消息m_nID是否被处理。这时,OnCmdMsg的第四个参数非空,表示仅仅是测试,不是真的要派发消息。如果没有提供命令消息m_nID的处理函数,则禁止用户对象m_nID,否则使之可用。


从上面的讨论可以知道:通过其结构,一个CCmdUI对象标识它表示了哪一个用户接口对象,如果是菜单接口对象,pMenu表示了要更新的菜单对象;如果是工具条,pOther表示了要更新的工具条窗口对象,nID表示了工具条按钮ID。


所以,由参数上状态更新消息的消息处理函数就知道要更新什么接口对象的状态。例如,第1节的函数OnUpdateEditPaste,函数参数pCmdUI表示一个菜单对象,需要更新该菜单对象的状态。


通过其成员函数,一个CCmdUI可以更新、改变用户接口对象的状态。例如,CCmdUI可以管理菜单和对话框控制的状态,调用Enable禁止或者允许菜单或者控制子窗口,等等。


所以,函数OnUpdateEditPaste可以直接调用参数的成员函数(如pCmdUI->Enable)实现菜单对象的状态更新。


由于接口对象的多样性,其他接口对象将从CCmdUI派生出管理自己的类来,覆盖基类的有关成员函数如Enable等,提供对自身状态更新的功能。例如管理状态条和工具栏更新的CStatusCmdUI类和CToolCmdUI类。


自动更新用户接口对象状态的机制
MFC提供了分别用于更新菜单和工具条的两种途径。


更新菜单状态
当用户对菜单如File单击鼠标时,就产生一条WM_INITMENUPOPUP消息,边框窗口在菜单下拉之前响应该消息,从而更新该菜单所有项的状态。


在应用程序开始运行时,边框也会收到WM_INITMENUPOPUP消息。


更新工具条等状态
当应用程序进入空闲处理状态时,将发送WM_IDLEUPDATECMDUI消息,导致所有的工具条用户对象的状态处理函数被调用,从而改变其状态。WM_IDLEUPDATECMDUI是MFC自己定义和使用的消息。


在窗口初始化时,工具条也会收到WM_IDLEUPDATECMDUI消息。


菜单状态更新的实现
MFC让边框窗口来响应WM_INITMENUPOPUP消息,消息处理函数是OnInitMenuPopup,其原型如下:


afx_msg void CFrameWnd::OnInitMenuPopup( CMenu* pPopupMenu,


UINT nIndex, BOOL bSysMenu );


第一个参数指向一个CMenu对象,是当前按击的菜单;第二个参数是菜单索引;第三个参数表示子菜单是否是系统控制菜单。


函数的处理:


如果是系统控制菜单,不作处理;否则,创建CCmdUI对象state,给它的各个成员如m_pMenu,m_pParentMenu,m_pOther等赋值。


对该菜单的各个菜单项,调函数state.DoUpdate,用CCmdUI的DoUpdate来更新状态。DoUpdate的第一个参数是this,表示命令目标是边框窗口;在CFrameWnd的成员变量m_bAutoMenuEnable为TRUE时(表示如果菜单m_nID没有对应的消息处理函数或状态更新函数,则禁止它),把DoUpdate的第二个参数bDisableIfNoHndler置为TRUE。


顺便指出,m_bAutoMenuEnable缺省时为TRUE,所以,应用程序启动时菜单经过初始化处理,没有提供消息处理函数或状态更新函数的菜单项被禁止。


工具条等状态更新的实现
图4-5表示了消息空闲时MFC更新用户对象状态的流程:


MFC提供的缺省空闲处理向顶层窗口(框架窗口)的所有子窗口发送消息WM_IDLEUPDATECMDUI;MFC的控制窗口(工具条、状态栏等)实现了对该消息的处理,导致用户对象状态处理函数的调用。


虽然两种途径调用了同一状态处理函数,但是传递的 CCmdUI参数从内部构成上是不一样的:第一种传递的CCmdUI对象表示了一菜单对象,(pMenu域被赋值);第二种传递了一个窗口对象(pOther域被赋值)。同样的状态改变动作,如禁止、允许状态的改变,前者调用了CMenu的成员函数EnableMenuItem,后者使用了CWnd的成员函数EnabelWindow。但是,这些不同由CCmdUI对象内部区分、处理,对用户是透明的:不论菜单还是对应的工具条,用户都用同一个状态处理函数使用同样的形式来处理。


 


这一节分析了用户界面更新的原理和机制。在后面第13章讨论工具条和状态栏时,将详细的分析这种机制的具体实现。


消息的预处理
到现在为止,详细的讨论了MFC的消息映射机制。但是,为了提高效率和简化处理,MFC提供了一种消息预处理机制,如果一条消息在预处理时被过滤掉了(被处理),则不会被派发给目的窗口的窗口过程,更不会进入消息循环了。


显然,能够进行预处理的消息只可能是队列消息,而且必须在消息派发之前进行预处理。因此,MFC在实现消息循环时,对于得到的每一条消息,首先送给目的窗口、其父窗口、其祖父窗口乃至最顶层父窗口,依次进行预处理,如果没有被处理,则进行消息转换和消息派发,如果某个窗口实现了预处理,则终止。有关实现见后面关于CWinThread线程类的章节,CWinThread的Run函数和PreTranslateMessage函数以及CWnd的函数WalkPreTranslateTree实现了上述要求和功能。这里要讨论的是MFC窗口类如何进行消息预处理。


CWnd提供了虚拟函数PreTranslateMessage来进行消息预处理。CWnd的派生类可以覆盖该函数,实现自己的预处理。下面,讨论几个典型的预处理。


首先,是CWnd的预处理:


预处理函数的原型为:


BOOL CWnd::PreTranslateMessage(MSG* pMsg)


CWnd类主要是处理和过滤Tooltips消息。关于该函数的实现和Tooltips消息,见后面第13章关于工具栏的讨论。


然后,是CFrameWnd的预处理:


CFrameWnd除了调用基类CWnd的实现过滤Tooltips消息之外,还要判断当前消息是否是键盘快捷键被按下,如果是,则调用函数::TranslateAccelerator(m_hWnd, hAccel, pMsg)处理快捷键。


接着,是CMDIChildWnd的预处理:


CMDIChildWnd的预处理过程和CFrameWnd的一样,但是不能依靠基类CFrameWnd的实现,必须覆盖它。因为MDI子窗口没有菜单,所以它必须在MDI边框窗口的上下文中来处理快捷键,它调用了函数::TranslateAccelerator(GetMDIFrame()->m_hWnd, hAccel,pMsg)。


讨论了MDI子窗口的预处理后,还要讨论MDI边框窗口:


CMDIFrameWnd的实现除了CFrameWnd的实现的功能外,它还要处理MDI快捷键(标准MDI界面统一使用的系统快捷键)。


在后面,还会讨论CDialog、CFormView、CToolBar等的消息预处理及其实现。


至于CWnd::WalkPreTranslateTree函数,它从接受消息的窗口开始,逐级向父窗回溯,逐一对各层窗口调用PreTranslateMessage函数,直到消息被处理或者到最顶层窗口为止。


MFC消息映射的回顾
从处理命令消息的过程可以看出,Windows消息和控制消息的处理要比命令消息的处理简单,因为查找消息处理函数时,后者只要搜索当前窗口对象(this所指)的类或其基类的消息映射入口表。但是,命令消息就要复杂多了,它沿一定的顺序链查找链上的各个命令目标,每一个被查找的命令目标都要搜索它的类或基类的消息映射入口表。


MFC通过消息映射的手段,以一种类似C++虚拟函数的概念向程序员提供了一种处理消息的方式。但是,若使用C++虚拟函数实现众多的消息,将导致虚拟函数表极其庞大;而使用消息映射,则仅仅感兴趣的消息才加入映射表,这样就要节省资源、提高效率。这套消息映射机制的基础包括以下几个方面:


消息映射入口表的实现:采用了C++静态成员和虚拟函数的方法来表示和得到一个消息映射类(CCmdTarget或派生类)的映射表。
消息查找的实现:从低层到高层搜索消息映射入口表,直至根类CCmdTarget。
消息发送的实现:主要以几个虚拟函数为基础来实现标准MFC消息发送路径:OnComamnd、OnNotify、OnWndMsg和OnCmdMsg。、
OnWndMsg是CWnd类或其派生类的成员函数,由窗口过程调用。它处理标准的Windows消息。


OnCommand是CWnd类或其派生类的成员函数,由OnWndMsg调用来处理WM_COMMAND消息,实现命令消息或者控制通知消息的发送。如果派生类覆盖该函数,则必须调用基类的实现,否则将不能自动的处理命令消息映射,而且必须使用该函数接受的参数(不是程序员给定值)调用基类的OnCommand。


OnNotify是CWnd类或其派生类的成员函数,由OnWndMsg调用来处理WM_NOTIFY消息,实现控制通知消息的发送。


OnCmdMsg是CCmdTarget类或其派生类的成员函数。被OnCommand调用,用来实现命令消息发送和派发命令消息到命令消息处理函数。


自动更新用户对象状态是通过MFC的命令消息发送机制实现的。


控制消息可以反射给控制窗口处理。


队列消息在发送给窗口过程之前可以进行消息预处理,如果消息被MFC窗口对象预处理了,则不会进入消息发送过程
========

深度解析VC中的消息传递机制

 http://www.cnblogs.com/zhwx/archive/2012/05/16/2504323.html


摘要:Windows编程和Dos编程,一个很大的区别就是,Windows编程是事件驱动,消息传递的。所以,要学好Windows编程,必须


对消息机制有一个清楚的认识,本文希望能够对消息的传递做一个全面的分析。


一、什么是消息?


消息系统对于一个win32程序来说十分重要,它是一个程序运行的动力源泉。一个消息,是系统定义的一个32位的值,他唯一的定


义了一个事件,向Windows发出一个通知,告诉应用程序某个事情发生了。例如,单击鼠标、改变窗口尺寸、按下键盘上的一个键


都会使Windows发送一个消息给应用程序。


消息本身是作为一个记录传递给应用程序的,这个记录中包含了消息的类型以及其他信息。例如,对于单击鼠标所产生的消息来


说,这个记录中包含了单击鼠标时的坐标。这个记录类型叫做MSG,MSG含有来自windows应用程序消息队列的消息信息,它在


Windows中声明如下: 
typedef struct tagMsg 

HWND hwnd;          // 接受该消息的窗口句柄 
UINT message;         // 消息常量标识符,也就是我们通常所说的消息号 
WPARAM wParam;     // 32位消息的特定附加信息,确切含义依赖于消息值 
LPARAM lParam;       // 32位消息的特定附加信息,确切含义依赖于消息值 
DWORD time;         // 消息创建时的时间 
POINT pt;             // 消息创建时的鼠标/光标在屏幕坐标系中的位置 
}MSG; 
消息可以由系统或者应用程序产生。系统在发生输入事件时产生消息。举个例子, 当用户敲键, 移动鼠标或者单击控件。系统也


产生消息以响应由应用程序带来的变化, 比如应用程序改变系统字体,改变窗体大小。应用程序可以产生消息使窗体执行任务,


或者与其他应用程序中的窗口通讯。


二、消息中有什么? 
我们给出了上面的注释,是不是会对消息结构有了一个比较清楚的认识?如果还没有,那么我们再试着给出下面的解释:


hwnd 32位的窗口句柄。窗口可以是任何类型的屏幕对象,因为Win32能够维护大多数可视对象的句柄(窗口、对话框、按钮、编辑


框等)。 
message 用于区别其他消息的常量值。这些常量可以是Windows单元中预定义的常量,也可以是自定义的常量。消息标识符以常量


命名的方式指出消息的含义。当窗口过程接收到消息之后,他就会使用消息标识符来决定如何处理消息。例如、WM_PAINT告诉窗


口过程窗体客户区被改变了需要重绘。符号常量指定系统消息属于的类别,其前缀指明了处理解释消息的窗体的类型。 
wParam 通常是一个与消息有关的常量值,也可能是窗口或控件的句柄。 
lParam 通常是一个指向内存中数据的指针。由于WParam、lParam和Pointer都是32位的,因此,它们之间可以相互转换。


三、消息标识符的值


系统保留消息标识符的值在0x0000在0x03ff(WM_USER-1)范围。这些值被系统定义消息使用。 应用程序不能使用这些值给自己的


消息。


应用程序消息从WM_USER(0X0400)到0X7FFF,或0XC000到0XFFFF;WM_USER到0X7FFF范围的消息由应用程序自己使用;0XC000到


0XFFFF范围的消息用来和其他应用程序通信,我们顺便说一下具有标志性的消息值: 
WM_NULL---0x0000 空消息。 
0x0001----0x0087 主要是窗口消息。 
0x00A0----0x00A9 非客户区消息  
0x0100----0x0108 键盘消息 
0x0111----0x0126 菜单消息 
0x0132----0x0138 颜色控制消息 
0x0200----0x020A 鼠标消息 
0x0211----0x0213 菜单循环消息 
0x0220----0x0230 多文档消息 
0x03E0----0x03E8 DDE消息 
0x0400 WM_USER 
0x8000 WM_APP 
0x0400----0x7FFF 应用程序自定义私有消息


四、消息有的分类 
其实,windows中的消息虽然很多,但是种类并不繁杂,大体上有3种:窗口消息、命令消息和控件通知消息。 
窗口消息 大概是系统中最为常见的消息,它是指由操作系统和控制其他窗口的窗口所使用的消息。例如CreateWindow、


DestroyWindow和MoveWindow等都会激发窗口消息,还有我们在上面谈到的单击鼠标所产生的消息也是一种窗口消息。 
命令消息 这是一种特殊的窗口消息,他用来处理从一个窗口发送到另一个窗口的用户请求,例如按下一个按钮,他就会向主窗口


发送一个命令消息。 
控件通知消息 是指这样一种消息,一个窗口内的子控件发生了一些事情,需要通知父窗口。通知消息只适用于标准的窗口控件如


按钮、列表框、组合框、编辑框,以及Windows公共控件如树状视图、列表视图等。例如,单击或双击一个控件、在控件中选择部


分文本、操作控件的滚动条都会产生通知消息。 她类似于命令消息,当用户与控件窗口交互时,那么控件通知消息就会从控件窗


口发送到它的主窗口。但是这种消息的存在并不是为了处理用户命令,而是为了让主窗口能够改变控件,例如加载、显示数据。


例如按下一个按钮,他向父窗口发送的消息也可以看作是一个控件通知消息;单击鼠标所产生的消息可以由主窗口直接处理,然


后交给控件窗口处理。 
其中窗口消息及控件通知消息主要由窗口类(即直接或间接由CWND类派生类)处理。相对窗口消息及控件通知消息而言,命令消


息的处理对象范围就广得多,它不仅可以由窗口类处理,还可以由文档类,文档模板类及应用类所处理。 
由于控件通知消息很重要的,人们用的也比较多,但是具体的含义往往令初学者晕头转向,所以我决定把常见的几个列出来供大


家参考: 
按扭控件 
BN_CLICKED 用户单击了按钮 
BN_DISABLE 按钮被禁止 
BN_DOUBLECLICKED 用户双击了按钮 
BN_HILITE 用/户加亮了按钮 
BN_PAINT 按钮应当重画 
BN_UNHILITE 加亮应当去掉 
组合框控件 
CBN_CLOSEUP 组合框的列表框被关闭 
CBN_DBLCLK 用户双击了一个字符串 
CBN_DROPDOWN 组合框的列表框被拉出 
CBN_EDITCHANGE 用户修改了编辑框中的文本 
CBN_EDITUPDATE 编辑框内的文本即将更新 
CBN_ERRSPACE 组合框内存不足 
CBN_KILLFOCUS 组合框失去输入焦点 
CBN_SELCHANGE 在组合框中选择了一项 
CBN_SELENDCANCEL 用户的选择应当被取消 
CBN_SELENDOK 用户的选择是合法的 
CBN_SETFOCUS 组合框获得输入焦点 
编辑框控件 
EN_CHANGE 编辑框中的文本己更新 
EN_ERRSPACE 编辑框内存不足 
EN_HSCROLL 用户点击了水平滚动条 
EN_KILLFOCUS 编辑框正在失去输入焦点 
EN_MAXTEXT 插入的内容被截断 
EN_SETFOCUS 编辑框获得输入焦点 
EN_UPDATE 编辑框中的文本将要更新 
EN_VSCROLL 用户点击了垂直滚动条消息含义 
列表框控件 
LBN_DBLCLK 用户双击了一项 
LBN_ERRSPACE 列表框内存不够 
LBN_KILLFOCUS 列表框正在失去输入焦点 
LBN_SELCANCEL 选择被取消 
LBN_SELCHANGE 选择了另一项 
LBN_SETFOCUS 列表框获得输入焦点


五、队列消息和非队列消息 
从消息的发送途径来看,消息可以分成2种:队列消息和非队列消息。消息队列又可以分成系统消息队列和线程消息队列。


系统消息队列由Windows维护,线程消息队列则由每个GUI线程自己进行维护,为避免给non-GUI现成创建消息队列,所有线程产生


时并没有消息队列,仅当线程第一次调用GDI函数时,系统给线程创建一个消息队列。队列消息送到系统消息队列,然后到线程消


息队列;非队列消息直接送给目的窗口过程。 
对于队列消息,最常见的是鼠标和键盘触发的消息,例如WM_MOUSERMOVE,WM_CHAR等消息,还有一些其它的消息,例如:WM_PAINT


、WM_TIMER和WM_QUIT。当鼠标、键盘事件被触发后,相应的鼠标或键盘驱动程序就会把这些事件转换成相应的消息,然后输送到


系统消息队列,由Windows系统去进行处理。Windows系统则在适当的时机,从系统消息队列中取出一个消息,根据前面我们所说


的MSG消息结构确定消息是要被送往那个窗口,然后把取出的消息送往创建窗口的线程的相应队列,下面的事情就该由线程消息队


列操心了,Windows开始忙自己的事情去了。线程看到自己的消息队列中有消息,就从队列中取出来,通过操作系统发送到合适的


窗口过程去处理。 
一般来讲,系统总是将消息Post在消息队列的末尾。这样保证窗口以先进先出的顺序接受消息。然而,WM_PAINT是一个例外,同一


个窗口的多个 WM_PAINT被合并成一个 WM_PAINT 消息, 合并所有的无效区域到一个无效区域。合并WM_PAIN的目的是为了减少刷


新窗口的次数。 
非队列消息将会绕过系统消息队列和线程消息队列,直接将消息发送到窗口过程。系统发送非队列消息通知窗口。例如,当用户激


活一个窗口,系统发送WM_ACTIVATE, WM_SETFOCUS, and WM_SETCURSOR。这些消息通知窗口它被激活了。非队列消息也可以由当


应用程序调用系统函数产生。例如,当程序调用SetWindowPos,系统发送WM_WINDOWPOSCHANGED消息。一些函数也发送非队列消息


,例如下面我们要谈到的函数。


六、消息的发送 
了解了上面的这些基础理论之后,我们就可以进行一下简单的消息发送与接收。 
把一个消息发送到窗口有3种方式:发送、寄送和广播。 
发送消息的函数有SendMessage、SendMessageCallback、SendNotifyMessage、SendMessageTimeout;寄送消息的函数主要有


PostMessage、PostThreadMessage、PostQuitMessage;广播消息的函数我知道的只有BroadcastSystemMessage、


BroadcastSystemMessageEx。


SendMessage的原型如下:


LRESULT SendMessage(HWND hWnd, UINT Msg, WPARAM wParam, LPARAM lParam)


这个函数主要是向一个或多个窗口发送一条消息,一直等到消息被处理之后才会返回。不过需要注意的是,如果接收消息的窗口


是同一个应用程序的一部分,那么这个窗口的窗口函数就被作为一个子程序马上被调用;如果接收消息的窗口是被另外的线程所


创建的,那么窗口系统就切换到相应的线程并且调用相应的窗口函数,这条消息不会被放进目标应用程序队列中。函数的返回值


是由接收消息的窗口的窗口函数返回,返回的值取决于被发送的消息。


PostMessage的原型如下:


BOOL PostMessage(HWND hWnd, UINT Msg, WPARAM wParam, LPARAM lParam)该函数把一条消息放置到创建hWnd窗口的线程的消息


队列中,该函数不等消息被处理就马上将控制返回。需要注意的是,如果hWnd参数为HWND_BROADCAST,那么,消息将被寄送给系


统中的所有的重叠窗口和弹出窗口,但是子窗口不会收到该消息;如果hWnd参数为NULL,则该函数类似于将dwThreadID参数设置


成当前线程的标志来调用PostThreadMEssage函数。 
从上面的这2个具有代表性的函数,我们可以看出消息的发送方式和寄送方式的区别所在:被发送的消息是否会被立即处理,函数


是否立即返回。被发送的消息会被立即处理,处理完毕后函数才会返回;被寄送的消息不会被立即处理,他被放到一个先进先出


的队列中,一直等到应用程序空闲的时候才会被处理,不过函数放置消息后立即返回。 
实际上,发送消息到一个窗口处理过程和直接调用窗口处理过程之间并没有太大的区别,他们直接的唯一区别就在于你可以要求


操作系统截获所有被发送的消息,但是不能够截获对窗口处理过程的直接调用。 
以寄送方式发送的消息通常是与用户输入事件相对应的,因为这些事件不是十分紧迫,可以进行缓慢的缓冲处理,例如鼠标、键


盘消息会被寄送,而按钮等消息则会被发送。 
广播消息用得比较少,BroadcastSystemMessage函数原型如下: 
long BroadcastSystemMessage(DWORD dwFlags, LPDWORD lpdwRecipients, UINT uiMessage, WPARAM wParam, LPARAM lParam); 
该函数可以向指定的接收者发送一条消息,这些接收者可以是应用程序、可安装的驱动程序、网络驱动程序、系统级别的设备驱


动消息和他们的任意组合。需要注意的是,如果dwFlags参数是BSF_QUERY并且至少一个接收者返回了BROADCAST_QUERY_DENY,则


返回值为0,如果没有指定BSF_QUERY,则函数将消息发送给所有接收者,并且忽略其返回值。


七、消息的接收 
消息的接收主要有3个函数:GetMessage、PeekMessage、WaitMessage。 
GetMessage原型如下:BOOL GetMessage(LPMSG lpMsg, HWND hWnd, UINT wMsgFilterMin, UINT wMsgFilterMax);


该函数用来获取与hWnd参数所指定的窗口相关的且wMsgFilterMin和wMsgFilterMax参数所给出的消息值范围内的消息。需要注意


的是,如果hWnd为NULL,则GetMessage获取属于调用该函数应用程序的任一窗口的消息,如果wMsgFilterMin和wMsgFilterMax都


是0,则GetMessage就返回所有可得到的消息。函数获取之后将删除消息队列中的除WM_PAINT消息之外的其他消息,至于


WM_PAINT则只有在其处理之后才被删除。


PeekMessage原型如下:BOOL PeekMessage(LPMSG lpMsg, HWND hWnd, UINT wMsgFilterMin, UINT wMsgFilterMax, UINT


wRemoveMsg); 
该函数用于查看应用程序的消息队列,如果其中有消息就将其放入lpMsg所指的结构中,不过,与GetMessage不同的是,


PeekMessage函数不会等到有消息放入队列时才返回。同样,如果hWnd为NULL,则PeekMessage获取属于调用该函数应用程序的任


一窗口的消息,如果hWnd=-1,那么函数只返回把hWnd参数为NULL的PostAppMessage函数送去的消息。如果wMsgFilterMin和


wMsgFilterMax都是0,则PeekMessage就返回所有可得到的消息。函数获取之后将删除消息队列中的除WM_PAINT消息之外的其他


消息,至于WM_PAINT则只有在其处理之后才被删除。 
WaitMessage原型如下:BOOL VaitMessage();


当一个应用程序无事可做时,该函数就将控制权交给另外的应用程序,同时将该应用程序挂起,直到一个新的消息被放入应用程


序的队列之中才返回。


八、消息的处理 
接下来我们谈一下消息的处理,首先我们来看一下VC中的消息泵: 
while(GetMessage(&msg, NULL, 0, 0)) 

    if(!TranslateAccelerator(msg.hWnd, hAccelTable, &msg)) 
    {  
        TranslateMessage(&msg); 
        DispatchMessage(&msg); 
    } 
}


首先,GetMessage从进程的主线程的消息队列中获取一个消息并将它复制到MSG结构,如果队列中没有消息,则GetMessage函数将


等待一个消息的到来以后才返回。 如果你将一个窗口句柄作为第二个参数传入GetMessage,那么只有指定窗口的消息可以从队列


中获得。GetMessage也可以从消息队列中过滤消息,只接受消息队列中落在范围内的消息。这时候就要利用GetMessage/


PeekMessage指定一个消息过滤器。这个过滤器是一个消息标识符的范围或者是一个窗体句柄,或者两者同时指定。当应用程序要


查找一个后入消息队列的消息是很有用。WM_KEYFIRST 和 WM_KEYLAST 常量用于接受所有的键盘消息。 WM_MOUSEFIRST 和


WM_MOUSELAST 常量用于接受所有的鼠标消息。  
然后TranslateAccelerator判断该消息是不是一个按键消息并且是一个加速键消息,如果是,则该函数将把几个按键消息转换成


一个加速键消息传递给窗口的回调函数。处理了加速键之后,函数TranslateMessage将把两个按键消息WM_KEYDOWN和WM_KEYUP转


换成一个WM_CHAR,不过需要注意的是,消息WM_KEYDOWN,WM_KEYUP仍然将传递给窗口的回调函数。


处理完之后,DispatchMessage函数将把此消息发送给该消息指定的窗口中已设定的回调函数。如果消息是WM_QUIT,则


GetMessage返回0,从而退出循环体。应用程序可以使用PostQuitMessage来结束自己的消息循环。通常在主窗口的WM_DESTROY消


息中调用。


下面我们举一个常见的小例子来说明这个消息泵的运用: 
if (::PeekMessage(&msg, m_hWnd, WM_KEYFIRST,WM_KEYLAST, PM_REMOVE)) 

    if (msg.message == WM_KEYDOWN && msg.wParam == VK_ESCAPE)... 
}


这里我们接受所有的键盘消息,所以就用WM_KEYFIRST 和 WM_KEYLAST作为参数。最后一个参数可以是PM_NOREMOVE 或者


PM_REMOVE,表示消息信息是否应该从消息队列中删除。  
所以这段小代码就是判断是否按下了Esc键,如果是就进行处理。


九、窗口过程 
窗口过程是一个用于处理所有发送到这个窗口的消息的函数。任何一个窗口类都有一个窗口过程。同一个类的窗口使用同样的窗


口过程来响应消息。


系统发送消息给窗口过程将消息数据作为参数传递给他,消息到来之后,按照消息类型排序进行处理,其中的参数则用来区分不


同的消息,窗口过程使用参数产生合适行为。 
一个窗口过程不经常忽略消息,如果他不处理,它会将消息传回到执行默认的处理。窗口过程通过调用DefWindowProc来做这个处


理。窗口过程必须return一个值作为它的消息处理结果。大多数窗口只处理小部分消息和将其他的通过DefWindowProc传递给系统


做默认的处理。窗口过程被所有属于同一个类的窗口共享,能为不同的窗口处理消息。下面我们来看一下具体的实例:


LRESULT CALLBACK WndProc(HWND hWnd, UINT message, WPARAM wParam, LPARAM lParam) 

    int wmId, wmEvent; 
    PAINTSTRUCT ps; 
    HDC hdc; 
   TCHAR szHello[MAX_LOADSTRING]; 
    LoadString(hInst, IDS_HELLO, szHello, MAX_LOADSTRING);


switch (message)  

    case WM_COMMAND: 
        wmId = LOWORD(wParam);  
        wmEvent = HIWORD(wParam);  
        // Parse the menu selections: 
        switch (wmId) 
        { 
        case IDM_ABOUT: 
            DialogBox(hInst, (LPCTSTR)IDD_ABOUTBOX, hWnd, (DLGPROC)About); 
        break; 
        case IDM_EXIT: 
            DestroyWindow(hWnd); 
        break; 
        default: 
            return DefWindowProc(hWnd, message, wParam, lParam); 
        } 
    break; 
    case WM_PAINT: 
        hdc = BeginPaint(hWnd, &ps); 
        // TODO: Add any drawing code here... 
        RECT rt; 
        GetClientRect(hWnd, &rt); 
        DrawText(hdc, szHello, strlen(szHello), &rt, DT_CENTER); 
        EndPaint(hWnd, &ps); 
    break; 
    case WM_DESTROY: 
        PostQuitMessage(0); 
    break; 
    default: 
        return DefWindowProc(hWnd, message, wParam, lParam); 

return 0;


}


消息分流器 
通常的窗口过程是通过一个switch语句来实现的,这个事情很烦,有没有更简便的方法呢?有,那就是消息分流器,利用消息分


流器,我们可以把switch语句分成更小的函数,每一个消息都对应一个小函数,这样做的好处就是对消息更容易管理。 
之所以被称为消息分流器,就是因为它可以对任何消息进行分流。下面我们做一个函数就很清楚了: 
void MsgCracker(HWND hWnd,int id,HWND hWndCtl,UINT codeNotify) 

    switch(id) 
    { 
        case ID_A: 
            if(codeNotify==EN_CHANGE)... 
       break; 
        case ID_B: 
            if(codeNotify==BN_CLICKED)... 
        break; 
        .... 
    } 

然后我们修改一下窗口过程:


LRESULT CALLBACK WndProc(HWND hWnd, UINT message, WPARAM wParam, LPARAM lParam) 

    switch(message) 
    { 
        HANDLE_MSG(hWnd,WM_COMMAND,MsgCracker); 
        HANDLE_MSG(hWnd,WM_DESTROY,MsgCracker); 
        default: 
            return DefWindowProc(hWnd, message, wParam, lParam); 

    return 0; 

在WindowsX.h中定义了如下的HANDLE_MSG宏: 
#define HANDLE_MSG(hwnd,msg,fn) \ 
switch(msg): return HANDLE_##msg((hwnd),(wParam),(lParam),(fn)); 
实际上,HANDLE_WM_XXXX都是宏,例如:HANDLE_MSG(hWnd,WM_COMMAND,MsgCracker);将被转换成如下定义: 
#define HANDLE_WM_COMMAND(hwnd,wParam,lParam,fn)\  
((fn)((hwnd),(int)(LOWORD(wParam)),(HWND)(lParam),(UINT)HIWORD(wParam)),0L); 
好了,事情到了这一步,应该一切都明朗了。 
不过,我们发现在windowsx.h里面还有一个宏:FORWARD_WM_XXXX,我们还是那WM_COMMAND为例,进行分析: 
#define FORWARD_WM_COMMAND(hwnd, id, hwndCtl, codeNotify, fn) \ 
(void)(fn)((hwnd), WM_COMMAND, MAKEWPARAM((UINT)(id),(UINT)(codeNotify)), (LPARAM)(HWND)(hwndCtl)) 
所以实际上,FORWARD_WM_XXXX将消息参数进行了重新构造,生成了wParam && lParam,然后调用了我们定义的函数。


十、MFC消息的处理实现方式 
初看MFC中的各种消息,以及在头脑中根深蒂固的C++的影响,我们可能很自然的就会想到利用C++的三大特性之一:虚拟机制来实


现消息的传递,但是经过分析,我们看到事情并不是想我们想象的那样,


在MFC中消息是通过一种所谓的消息映射机制来处理的。 
为什么呢?在潘爱民老师翻译的《Visual C++技术内幕》(第4版)中给出了详细的原因说明,我再简要的说一遍。在CWnd类中大


约有110个消息,还有其它的MFC的类呢,算起来消息太多了,在C++中对程序中用到的每一个派生类都要有一个vtable,每一个虚


函数在vtable中都要占用一个4字节大小的入口地址,这样一来,对于每个特定类型的窗口或控件,应用程序都需要一个440KB大


小的表来支持虚拟消息控件函数。 
如果说上面的窗口或控件可以勉强实现的话,那么对于菜单命令消息及按钮命令消息呢?因为不同的应用程序有不同的菜单和按


钮,我们怎么处理呢?在MFC库的这种消息映射系统就避免了使用大的vtable,并且能够在处理常规Windows消息的同时处理各种


各样的应用程序的命令消息。 
说白了,MFC中的消息机制其实质是一张巨大的消息及其处理函数的一一对应表,然后加上分析处理这张表的应用框架内部的一些


程序代码。这样就可以避免在SDK编程中用到的繁琐的CASE语句。


MFC的消息映射的基类CCmdTarget 
如果你想让你的控件能够进行消息映射,就必须从CCmdTarget类中派生。CCmdTarget类是MFC处理命令消息的基础、核心。MFC为


该类设计了许多成员函数和一些成员数据,基本上是为了解决消息映射问题的,所有响应消息或事件的类都从它派生,例如:应


用程序类、框架类、文档类、视图类和各种各样的控件类等等,还有很多。 
不过这个类里面有2个函数对消息映射非常重要,一个是静态成员函数DispatchCmdMsg,另一个是虚函数OnCmdMsg。 
DispatchCmdMsg专门供MFC内部使用,用来分发Windows消息。OnCmdMsg用来传递和发送消息、更新用户界面对象的状态。 
CCmdTarget对OnCmdMsg的默认实现:在当前命令目标(this所指)的类和基类的消息映射数组里搜索指定命令消息的消息处理函数


。 
这里使用虚拟函数GetMessageMap得到命令目标类的消息映射入口数组_messageEntries,然后在数组里匹配命令消息ID相同、控


制通知代码也相同的消息映射条目。其中GetMessageMap是虚拟函数,所以可以确认当前命令目标的确切类。 
如果找到了一个匹配的消息映射条目,则使用DispachCmdMsg调用这个处理函数; 
如果没有找到,则使用_GetBaseMessageMap得到基类的消息映射数组,查找,直到找到或搜寻了所有的基类(到CCmdTarget)为


止;如果最后没有找到,则返回FASLE。 
每个从CCmdTarget派生的命令目标类都可以覆盖OnCmdMsg,利用它来确定是否可以处理某条命令,如果不能,就通过调用下一命


令目标的OnCmdMsg,把该命令送给下一个命令目标处理。通常,派生类覆盖OnCmdMsg时 ,要调用基类的被覆盖的OnCmdMsg。 
在MFC框架中,一些MFC命令目标类覆盖了OnCmdMsg,如框架窗口类覆盖了该函数,实现了MFC的标准命令消息发送路径。必要的话


,应用程序也可以覆盖OnCmdMsg,改变一个或多个类中的发送规定,实现与标准框架发送规定不同的发送路径。例如,在以下情


况可以作这样的处理:在要打断发送顺序的类中把命令传给一个非MFC默认对象;在新的非默认对象中或在可能要传出命令的命令


目标中。


消息映射的内容 
通过ClassWizard为我们生成的代码,我们可以看到,消息映射基本上分为2大部分:


在头文件(.h)中有一个宏DECLARE_MESSAGE_MAP(),他被放在了类的末尾,是一个public属性的;与之对应的是在实现部分


(.cpp)增加了一张消息映射表,内容如下: 
BEGIN_MESSAGE_MAP(当前类, 当前类的基类) 
file://{{AFX_MSG_MAP(CMainFrame) 
消息的入口项 
file://}}AFX_MSG_MAP 
END_MESSAGE_MAP() 
但是仅有这两项还远不足以完成一条消息,要使一个消息工作,必须有以下3个部分去协作: 
1.在类的定义中加入相应的函数声明; 
2.在类的消息映射表中加入相应的消息映射入口项; 
3.在类的实现中加入相应的函数体;


消息的添加 
有了上面的这些只是作为基础,我们接下来就做我们最熟悉、最常用的工作:添加消息。


MFC消息的添加主要有2种方法:自动/手动,我们就以这2种方法为例,说一下如何添加消息。 
1、利用Class Wizard实现自动添加 
在菜单中选择View-->Class Wizard,也可以用单击鼠标右键,选择Class Wizard,同样可以激活Class Wizard。选择Message


Map标签,从Class name组合框中选取我们想要添加消息的类。在Object IDs列表框中,选取类的名称。此时, Messages列表框


显示该类的大多数(若不是全部的话)可重载成员函数和窗口消息。类重载显示在列表的上部,以实际虚构成员函数的大小写字母


来表示。其他为窗口消息,以大写字母出现,描述了实际窗口所能响应的消息ID。选中我们向添加的消息,单击Add Function按


钮,Class Wizard自动将该消息添加进来。 
有时候,我们想要添加的消息本应该出现在Message列表中,可是就是找不到,怎么办?不要着急,我们可以利用Class Wizard上


Class Info标签以扩展消息列表。在该页中,找到Message Filter组合框,通过它可以改变首页中Messages列表框中的选项。这


里,我们选择Window,从而显示所有的窗口消息,一把情况下,你想要添加的消息就可以在Message列表框中出现了,如果还没有


,那就接着往下看:)


2、手动地添加消息处理函数 
如果在Messages列表框中仍然看不到我们想要的消息,那么该消息可能是被系统忽略掉或者是你自己创建的,在这种情况下,就


必须自己手工添加。根据我们前面所说的消息工作的3个部件,我们一一进行处理: 
1) 在类的. h文件中添加处理函数的声明,紧接在//}}AFX_MSG行之后加入声明,注意:一定要以afx_msg开头。 
通常,添加处理函数声明的最好的地方是源代码中Class Wizard维护的表下面,但是在它标记其领域的{{}}括弧外面。这些


括弧中的任何东西都将会被Class Wizard销毁。 
2) 接着,在用户类的.cpp文件中找到//}}AFX_MSG_MAP行,紧接在它之后加入消息入口项。同样,也是放在{ {} }的外面


3) 最后,在该文件中添加消息处理函数的实体。
========

VS2010/MFC编程入门之五(MFC消息映射机制概述)

http://www.jizhuomi.com/software/147.html
      
       前面已经说过,Windows应用程序是消息驱动的。在MFC软件开发中,界面操作或者线程之间通信都会经常用到消息,通过对消息的处理实现相应的操作。比较典型的过程是,用户操作窗口,然后有消息产生,送给窗口的消息处理函数处理,对用户的操作做出响应。


       什么是消息


       窗口消息一般由三个部分组成:1.一个无符号整数,是消息值;(2)消息附带的WPARAM类型的参数;(3)消息附带的LPARAM类型的参数。其实我们一般所说的消息是狭义上的消息值,也就是一个无符号整数,经常被定义为宏。


       什么是消息映射机制


       MFC使用一种消息映射机制来处理消息,在应用程序框架中的表现就是一个消息与消息处理函数一一对应的消息映射表,以及消息处理函数的声明和实现等代码。当窗口接收到消息时,会到消息映射表中查找该消息对应的消息处理函数,然后由消息处理函数进行相应的处理。SDK编程时需要在窗口过程中一一判断消息值进行相应的处理,相比之下MFC的消息映射机制要方便好用的多。


       Windows消息分类


       先讲下Windows消息的分类。Windows消息分为系统消息和用户自定义消息。Windows系统消息有三种:


       1.标准Windows消息。除WM_COMMAND外以WM_开头的消息是标准消息。例如,WM_CREATE、WM_CLOSE。


       2.命令消息。消息名为WM_COMMAND,消息中附带了标识符ID来区分是来自哪个菜单、工具栏按钮或加速键的消息。


       3.通知消息。通知消息一般由列表框等子窗口发送给父窗口,消息名也是WM_COMMAND,其中附带了控件通知码来区分控件。


       CWnd的派生类都可以接收到标准Windows消息、通知消息和命令消息。命令消息还可以由文档类等接收。


       用户自定义消息是实际上就是用户定义一个宏作为消息,此宏的值应该大于等于WM_USER,然后此宏就可以跟系统消息一样使用,窗口类中可以定义它的处理函数。


       消息映射表


       除了一些没有基类的类或CObject的直接派生类外,其他的类都可以自动生成消息映射表。下面的讲解都以前面例程HelloWorld的CMainFrame为例。消息映射表如下:


C++代码
BEGIN_MESSAGE_MAP(CMainFrame, CFrameWndEx)   
    ON_WM_CREATE()   
    ON_COMMAND(ID_VIEW_CUSTOMIZE, &CMainFrame::OnViewCustomize)   
    ON_REGISTERED_MESSAGE(AFX_WM_CREATETOOLBAR, &CMainFrame::OnToolbarCreateNew)   
    ON_COMMAND_RANGE(ID_VIEW_APPLOOK_WIN_2000, ID_VIEW_APPLOOK_WINDOWS_7, &CMainFrame::OnApplicationLook)   
    ON_UPDATE_COMMAND_UI_RANGE(ID_VIEW_APPLOOK_WIN_2000, ID_VIEW_APPLOOK_WINDOWS_7, &CMainFrame::OnUpdateApplicationLook)   
    ON_WM_SETTINGCHANGE()   
END_MESSAGE_MAP()  
       在BEGIN_MESSAG_MAP和END_MESSAGE_MAP之间的内容成为消息映射入口项。消息映射除了在CMainFrame的实现文件中添加消息映射表外,在类的定义文件MainFrm.h中还会添加一个宏调用:


       DECLARE_MESSAGE_MAP()


       一般这个宏调用写在类定义的结尾处。


VS2010/MFC编程入门之五(MFC消息映射机制概述)


       添加消息处理函数


       如何添加消息处理函数呢?不管是自动还是手动添加都有三个步骤:


       1.在类定义中加入消息处理函数的函数声明,注意要以afx_msg打头。例如MainFrm.h中WM_CREATE的消息处理函数的函数声明:afx_msg int OnCreate(LPCREATESTRUCT lpCreateStruct);。


       2.在类的消息映射表中添加该消息的消息映射入口项。例如WM_CREATE的消息映射入口项:ON_WM_CREATE()。


       3.在类实现中添加消息处理函数的函数实现。例如,MainFrm.cpp中WM_CREATE的消息处理函数的实现:


          int CMainFrame::OnCreate(LPCREATESTRUCT lpCreateStruct)
         {
                  ......
         }


       通过以上三个步骤以后,WM_CREATE等消息就可以在窗口类中被消息处理函数处理了。


       各种Windows消息的消息处理函数


       标准Windows消息的消息处理函数都与WM_CREATE消息类似。


       命令消息的消息映射入口项形式如:ON_COMMAND(ID_VIEW_CUSTOMIZE, &CMainFrame::OnViewCustomize),消息为ID_VIEW_CUSTOMIZE,消息处理函数为OnViewCustomize。


       如果想要使用某个处理函数批量处理某些命令消息,则可以像CMainFrame消息映射表中的ON_COMMAND_RANGE(ID_VIEW_APPLOOK_WIN_2000, ID_VIEW_APPLOOK_WINDOWS_7, &CMainFrame::OnApplicationLook)一样添加消息映射入口项,这样值在ID_VIEW_APPLOOK_WIN_2000到ID_VIEW_APPLOOK_WINDOWS_7之间的菜单项等的命令消息都由CMainFrame的OnApplicationLook函数处理。函数原型为afx_msg void OnApplicationLook(UINT id);,参数id为用户操作的菜单项等的ID。


       在操作列表框等控件时往往会给父窗口发送WM_NOTIFY通知消息。WM_NOTIFY消息的wParam参数为发送通知消息的控件的ID,lParam参数指向一个结构体,可能是NMHDR结构体,也可能是第一个元素为NMHDR结构体变量的其他结构体。NMHDR结构体的定义如下(仅作了解):


       Typedef sturct tagNMHDR{
                HWND hwndFrom;
                UINT idFrom;
                UINT code;
       } NMHDR;


       hwndFrom为发送通知消息控件的句柄,idFrom为控件ID,code为要处理的通知消息的通知码,例如NM_CLICK。


       通知消息的消息映射入口项形式如:


       ON_NOTIFY(wNotifyCode,id,memberFxn)


       wNotifyCode为要处理的通知消息通知码,例如:NM_CLICK。id为控件标识ID。MemberFxn为此消息的处理函数。


       通知消息的处理函数的原型为:


       afx_msg void memberFxn( NMHDR * pNotifyStruct, LRESULT * result);


       如果需要使用用户自定义消息,首先要定义消息宏,如:#define WM_UPDATE_WND (WM_USER+1),再到消息映射表中添加消息映射入口项:ON_MESSAGE(WM_UPDATE_WND, &CMainFrame::OnUpdateWnd),然后在MainFrm.h中添加消息处理函数的函数声明:afx_msg LRESULT OnUpdateWnd(WPARAM wParam, LPARAM lParam);,最后在MainFrm.cpp中实现此函数。


========
  • 1
    点赞
  • 0
    评论
  • 13
    收藏
  • 一键三连
    一键三连
  • 扫一扫,分享海报

©️2021 CSDN 皮肤主题: 大白 设计师:CSDN官方博客 返回首页
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、C币套餐、付费专栏及课程。

余额充值