转贴:深度解析VC中的消息(下)

消息的接收

   消息的接收主要有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) 最后,在该文件中添加消息处理函数的实体

你可能感兴趣的:(转贴:深度解析VC中的消息(下))