1. 窗口过程
每个窗口会有一个称为窗口过程的回调函数(WndProc),它带有四个参数,分别为:窗口句柄(Window
Handle),消息ID(Message ID),和两个消息参数(wParam, lParam),
当窗口收到消息时系统就会调用此窗口过程来处理消息。(所以叫回调函数)
2 消息类型
1) 系统定义消息(System-Defined Messages)
在SDK中事先定义好的消息,非用户定义的,其范围在[0x0000, 0x03ff]之间, 可以分为以下三类:
1> 窗口消息(Windows Message)
与窗口的内部运作有关,如创建窗口,绘制窗口,销毁窗口等。可以是一般的窗口,也可以是Dialog,控件等。
如:WM_CREATE, WM_PAINT, WM_MOUSEMOVE, WM_CTLCOLOR, WM_HSCROLL...
2> 命令消息(Command Message)
与处理用户请求有关, 如单击菜单项或工具栏或控件时, 就会产生命令消息。
WM_COMMAND, LOWORD(wParam)表示菜单项,工具栏按钮或控件的ID。如果是控件, HIWORD(wParam)表示控件消息类型
3> 控件通知(Notify Message)
控件通知消息, 这是最灵活的消息格式, 其Message, wParam, lParam分别为:WM_NOTIFY, 控件ID,指向NMHDR的指针。NMHDR包含控件通知的内容, 可以任意扩展。
2) 程序定义消息(Application-Defined Messages)
用户自定义的消息, 对于其范围有如下规定:
WM_USER: 0x0400-0x7FFF (ex. WM_USER+10)
WM_APP(winver> 4.0): 0x8000-0xBFFF (ex.WM_APP+4)
RegisterWindowMessage: 0xC000-0xFFFF
3 消息队列(Message Queues)
Windows中有两种类型的消息队列
1) 系统消息队列(System Message Queue)
这是一个系统唯一的Queue,设备驱动(mouse, keyboard)会把操作输入转化成消息存在系统队列中,然后系统会把此消息放到目标窗口所在的线程的消息队列(thread-specific message queue)中等待处理
2) 线程消息队列(Thread-specific Message Queue)
每一个GUI线程都会维护这样一个线程消息队列。(这个队列只有在线程调用GDI函数时才会创建,默认不创建)。然后线程消息队列中的消息会被送到相应的窗口过程(WndProc)处理.
注意: 线程消息队列中WM_PAINT,WM_TIMER只有在Queue中没有其他消息的时候才会被处理,WM_PAINT消息还会被合并以提高效率。其他所有消息以先进先出(FIFO)的方式被处理。
4 队列消息(Queued Messages)和非队列消息(Non-Queued Messages)
1)队列消息(Queued Messages)
消息会先保存在消息队列中,消息循环会从此队列中取消息并分发到各窗口处理
如鼠标,键盘消息。
2) 非队列消息(NonQueued Messages)
消息会绕过系统消息队列和线程消息队列直接发送到窗口过程被处理
如: WM_ACTIVATE, WM_SETFOCUS, WM_SETCURSOR, WM_WINDOWPOSCHANGED
注意: postMessage发送的消息是队列消息,它会把消息Post到消息队列中; SendMessage发送的消息是非队列消息, 被直接送到窗口过程处理
5 PostMessage(PostThreadMessage), SendMessage
PostMessage:把消息放到指定窗口所在的线程消息队列中后立即返回。 PostThreadMessage:把消息放到指定线程的消息队列中后立即返回。
SendMessage:直接把消息送到窗口过程处理, 处理完了才返回。
6 GetMessage, PeekMessage
PeekMessage会立即返回 可以保留消息
GetMessage在有消息时返回 会删除消息
7 TranslateMessage, TranslateAccelerator
TranslateMessage: 把一个virtual-key消息转化成字符消息(character message),并放到当前线程的消息队列中,消息循环下一次取出处理。
TranslateAccelerator:
将快捷键对应到相应的菜单命令。它会把WM_KEYDOWN 或 WM_SYSKEYDOWN转化成快捷键表中相应的WM_COMMAND
或WM_SYSCOMMAND消息, 然后把转化后的 WM_COMMAND或WM_SYSCOMMAND直接发送到窗口过程处理, 处理完后才会返回。
8(消息死锁( Message Deadlocks)
假设有线程A和B, 现在有以下下步骤
1) 线程A SendMessage给线程B, A等待消息在线程B中处理后返回
2) 线程B收到了线程A发来的消息,并进行处理, 在处理过程中,B也向线程A SendMessgae,然后等待从A返回。
因为此时, 线程A正等待从线程B返回, 无法处理B发来的消息, 从而导致了线程A,B相互等待, 形成死锁。多个线程也可以形成环形死锁。
可以使用 SendNotifyMessage或SendMessageTimeout来避免出现死锁。
9 BroadcastSystemMessage
我
们一般所接触到的消息都是发送给窗口的, 其实, 消息的接收者可以是多种多样的,它可以是应用程序(applications),
可安装驱动(installable drivers), 网络设备(network drivers), 系统级设备驱动(system-level
device drivers)等,
BroadcastSystemMessage这个API可以对以上系统组件发送消息。
一、引言
随着Windows操作系统的不断推广,众多软件开发包都提供有开发基于Windows平台应用软件的功能。虽然这些开发包不尽相同,流行的有
Visual C++、Visual Basic、Delphi、C++ Builder
等多种,但由这些不同语言开发的软件有一点却是相同的--都是运行于Windows 操作平台,都必须接受Windows
的运行机制。作为Windows
操作系统灵魂的消息机制也就必然为众多用不同语言开发的Windows操作系统下运行的应用程序所接受。因此,要编写深入的Windows程序,就必须对
Windows的运行机制有很好的认识和理解。本文下面将对Windows操作系统下的消息运行机制做较为深入的剖析。
二、Windows事件驱动机制
我们当中不少使用VC、Delphi等作为开发语言的程序员是一步步从DOS下的Basic、C++中走过来的,而且大多在刚开始学习编程时也是先从
DOS下的编程环境入手的,因此在习惯了DOS下的过程驱动形式的顺序程序设计方法后,往往在向Windows下的开发环境转型的过程中会对
Windows所采取的事件驱动方式感到无法适应。因为DOS和Windows这两种操作系统的运行机制是截然不同的,DOS下的任何程序都是使用顺序
的、过程驱动的程序设计方法。这种程序都有一个明显的开始、明显的过程以及一个明显的结束,因此通过程序就能直接控制程序事件或过程的全部顺序。即使是在
处理异常时,处理过程也仍然是顺序的、过程驱动的结构。而Windows的驱动方式则是事件驱动的,即程序的流程不是由事件的顺序来控制,而是由事件的发
生来控制,所有的事件是无序的,所为一个程序员,在编写程序时,并不知道用户会先按下哪个按纽,也就不知道程序先触发哪个消息。因此我们的主要任务就是对
正在开发的应用程序要发出的或要接收的消息进行排序和管理。事件驱动程序设计是密切围绕消息的产生与处理而展开的,一条消息是关于发生的事件的消息。
三、Windows的消息循环
Windows操作系统为每一个正在运行的应用程序保持有一个消息队列。当有事件发生后,Windows并不是将这个激发事件直接送给应用程序,而是先将
其翻译成一个Windows消息,然后再把这个消息加入到这个应用程序的消息队列中去。应用程序需要通过消息循环来接收这些消息。在MFC中使用了对
WinAPI进行了很好封装的类库,虽然可以为编程提供一个面向对象的界面,使Windows程序员能够以面象对象的方式进行编程,把那些进行SDK编程
时最繁琐的部分提供给程序员,使之专注于功能的实现,但是由于引入了很好的封装特性,使我们不能直接操纵部分核心代码。对于消息的循环和接收也只是通过类
似于下面的消息映射予以很简单的表示:
BEGIN_MESSAGE_MAP(CTEMMSView, CFormView)
//{ { AFX_MSG_MAP(CTEMMSView)
ON_WM_LBUTTONDOWN()
ON_COMMAND(ID_OPENDATA, OnOpenData)
ON_WM_TIMER()
ON_WM_PAINT()
//} } AFX_MSG_MAP
END_MESSAGE_MAP()
虽然上述消息映射在编程过程中处理消息非常简练方便,但显然是难于理解消息是如何参与循环和分发的。因此有必要通过SDK(Software
Developers
Kit,软件开发工具箱)代码深入到被MFC封装的Windows编程的核心中来研究其具体是如何工作的。在SDK编程中,一般是在Windows应用程
序的入口点WinMain函数中添加处理消息循环的代码以检索Windows送来的消息,然后WinMain再把这些消息分配给相应的窗口函数并处理它
们:
……
MSG msg; //定义消息名
while (GetMessage (& msg, NULL, 0, 0))
{
TranslateMessage (& msg) ; //翻译消息
DispatchMessage (& msg) ; //撤去消息
}
return msg.wParam ;
上述几句虽然简单但却是所有Windows程序的关键代码,担负着获取、解释和分发消息的任务,下面就重点对其功能和作用进行分析:
MSG结构在头文件中定义如下:
typedef struct tagMSG
{
HWND hwnd;
UINT message;
WPARAM wParam;
LPARAM lParam;
DWORD time;
POINT pt;
} MSG, *PMSG;
其数据成员的具体意义如下:
hwnd:消息将要发送到的那个窗口的句柄,用这个参数可以决定让哪个窗口接收消息。
message:消息号,它唯一标识了一种消息类型。每种消息类型都在Windows文件进行了预定义。
wParam:一个32位的消息参数,这个值的确切意义取决于消息本身。
lParam:同上。
time:消息放入消息队列中的时间,在这个域中写入的并非当时日期,而是从Windows启动后所测量的时间值。Windows用
这个域来使用消息保持正确的顺序。
pt:消息放入消息队列时的鼠标坐标。
消息循环以GetMessage调用开始,它从消息队列中取出一个消息。该函数的四个参数可以有控制地获取消息,第一个参数指定要接收消息的MSG结构的
地址,第二个参数表示窗口句柄,一般将其设置为空,表示要获取该应用程序创建的所有窗口的消息;第三、四参数用于指定消息范围。后面三个参数被设置为默认
值,用于接收发送到属于这个应用程序的任何一个窗口的所有消息。在接收到除WM_QUIT之外的任何一个消息后,GetMessage()返回
TRUE;如果GetMessage收到一个WM_QUIT消息,则返回FALSE以退出消息循环,终止程序运行。因此,在接收到WM_QUIT之前,带
有GetMessage()的消息循环可以一直循环下去。当除WM_QUIT的消息用GetMessage读入后,首先要经过函数
TranslateMessage()对其进行解释,但对大多数消息来说并不起什么作用。这里起关键作用的是DispatchMessage()函数,把
由GetMessage获取的Windows消息传送给在MSG结构中为窗口所指定的窗口过程。在消息处理函数处理完消息之后,代码又循环到开始去接收另
一个消息,这样就完成了一个完整的消息循环。
由于Windows操作系统是一种非剥夺式多任务操作系统。只有在应用程序主动交出CPU控制权后,Windows才能把控制权交给其他应用程序。在消息
循环中,一定要有能交出控制的系统函数才能实现协同式多任务操作。能完成该功能的只有GetMessage、PeekMessage和
WaitMessage这三个函数,如果在应用程序中长期不去调用这三个函数之一其他任务则无法执行。GetMessage函数在找不到等待应用程序处理
的消息时,会自动交出控制权,由Windows把CPU的控制权交给其他等待获取控制权的应用程序。由于任何Windows应用程序都含有一个消息循环,
这种隐式交出控制权的方式可以保证合并各个应用程序共享控制权。一旦发往该应用程序的消息到达应用程序队列,即开始执行GetMessage语句的下一条
语句。使用GetMessage函数的消息循环在消息队列中没有消息时将等待,如果需要,可以利用这段时间进行I/O端口操作等耗时操作,不过需要在消息
循环中使用PeekMessage函数来代替GetMessage。使用PeekMessage的方法同GetMessage类似,下面是一段使用
PeekMessage函数的消息循环的典型例子:
MSG msg;
BOOL bDone=FALSE;
do{
if(PeekMessage(& msg,NULL,0,0,PM_REMOVE)){
if(msg.message==WM_QUIT)
bDone=TRUE;
else{
TranslateMessage(& msg);
DispatchMessage(& msg);
}
}
//无消息处理,进行长时间操作
else{
……//长时间操作
}
} while(!bDone)
……
无论应用程序消息队列中是否有消息,PeekMessage函数都立即返回,如果希望等待新消息入队,可以利用无返回值的函数WaitMessage配合PeekMessage进行消息循环。
四、对Windowds消息的处理
窗口过程处理消息通常以switch语句开始,对于它要处理的每一条消息ID都跟有一条case语句,这在功能上同MFC的消息映射有些类似:
switch(uMsgId)
{
case WM_TIMER:
//对WM_TIMER定时器消息的处理过程
return 0;
case WM_LBUTTONDOWN:
//对WM_ LBUTTONDOWN鼠标左键单击消息的处理过程
ruturn 0;
……
default:
//其他消息由这个默认处理函数来处理
return DefWindowProc(hwnd,uMsgId,wParam,lParam);
}
在处理完消息后必须返回0,这很重要,否则Windows将要不停地重试下去。对于那些在程序中不准备处理的消息,窗口过程会把它们都扔给
DefWindowProc进行缺省处理,而且还要返回那个函数的返回值。在消息传递层次中,可以认为DefWindowProc函数是最顶层的函数。该
函数发出WM_SYSCOMMAND消息,由系统执行Windows环境中多数窗口所公用的各种通用操作,如更新窗口的正文标题等等。在MFC下可以用下
述部分代码实现与上述SDK代码相同的功能:
BEGIN_MESSAGE_MAP(CTEMMSView, CFormView)
//{ { AFX_MSG_MAP(CTEMMSView)
ON_WM_LBUTTONDOWN()
ON_WM_TIMER()
//} } AFX_MSG_MAP
END_MESSAGE_MAP()
小结:Windows环境提供有非常丰富的系统资源,在这个基础上可以编制出能满足各种各样目标功能的应用系统。要深入Windows编程就必须首先对
Windows系统的运行机理有很好的认识,本文仅针对Windows的一种重要运行机制--消息机制作了较深入的剖析和阐述。对培养在Windows
下的编程思想有一定的帮助。对某些相关问题的详细论述可以参考MSDN在线帮助的" SDK Reference" 部分。
1、简单理解Windows的消息
消息,就是指Windows发出的一个通知,告诉应用程序某个事情发生了。
举个例子来说,鼠标单击某应用程序的一个按钮。这时,Windows(操作系统)给应用程序发送这个消息,通知应用程序该按钮被点击,应用程序将进行相应反应。
消息一般用一个32位的数来标识,这个数唯一地标识这个消息。这些消息的标识符一般在头文件winuser.h 中定义,如:
#define WM_PAINT 0x000F
#define WM_QUIT 0x0012
其实消息本身是一个MSG结构。MSG结构定义如下:
typedef struct tagMSG {
HWND hwnd; //接受消息的窗口句柄
UINT message; //消息标识符
WPARAM wParam; //32位附加信息
LPARAM lParam; //32位附加信息
DWORD time; //消息创建的时间
POINT pt; //消息创建时鼠标在屏幕坐标系中的位置
} MSG;
也就是说,对于任何一个消息,都有一个MSG变量与之对应,该变量包含了消息的相关信息。而我们在一般情况下,只使用消息的消息标识符,该标识符也唯一地代表了这个消息。
举个例子来说,当我们收到一个字符消息的时候,message成员变量的值就是WM_CHAR,但用户到底输入的是什么字符,那么就由wParam和lParam来说明。wParam、lParam表示的信息随消息的不同而不同。
Windows操作系统已经给我们定义了大量的消息,这些消息我们称为系统消息。除了系统消息,我们还可以自己定义消息,即自定义消息。
值小于0x0400的消息都是系统消息,自定义消息一般都大于0x0400。
系统消息取值一般有如下规律,如表1:
| 范围 | 意义 |
| 0x0001——0x0087 |
主要是窗口消息 |
| 0x00A0——0x00A9 |
非客户区消息 |
| 0x0100——0x0108 |
键盘消息 |
| 0x0111——0x0126 |
菜单消息 |
| 0x0132——0x0138 |
颜色控制消息 |
| 0x0200——0x020A |
鼠标消息 |
| 0x0211——0x0213 |
菜单循环消息 |
| 0x0220——0x0230 |
多文档消息 |
| 0x03E0——0x03E8 |
DDE消息 |
| 0x0400 |
WM_USER |
| 0x0400——0x7FFF |
自定义消息 |
表1
在WINUSER.H中,我们有定义:
#define WM_USER 0x0400
对于自定义消息,我们一般采用WM_USER 加一个整数值的方法定义自定义消息,如:
#define WM_RECVDATA WM_USER + 1
如果您初次接触Windows编程,或是初次接触Windows消息,对于上述解释可能没有看懂,这也不要着急,后面的实例将会逐步带您对Windows的消息编程有一个了解。
2、通过一个简单的Win32程序理解Windows消息
例程1:一个简单的Win32程序代码(见附带源码 工程M1)
打开VC++ 6.0,新建一个Win32 Application,工程名为M1,在该工程添加C++ Source File,文件名为M1,在该文件中添加如下代码:
//一个简单的Win32应用程序
//通过这个简单的实例讲解Windows消息是如何传递的
#include <windows.h>
//声明窗口过程函数
LRESULT CALLBACK WndProc(HWND,UINT,WPARAM,LPARAM);
//定义一个全局变量,作为窗口类名
TCHAR szClassName[] = TEXT("SimpleWin32");
//应用程序主函数
int WINAPI WinMain (HINSTANCE hInstance,
HINSTANCE hPrevInstance,
LPSTR szCmdLine,
int iCmdShow)
{
//窗口类
WNDCLASS wndclass;
//当窗口水平方向的宽度和垂直方向的高度变化时重绘整个窗口
wndclass.style = CS_HREDRAW|CS_VREDRAW;
//关联窗口过程函数
wndclass.lpfnWndProc = WndProc;
wndclass.cbClsExtra = 0;
wndclass.cbWndExtra = 0;
wndclass.hInstance = hInstance;//实例句柄
wndclass.hIcon = LoadIcon(NULL,IDI_APPLICATION);//图标
wndclass.hCursor = LoadCursor(NULL,IDC_ARROW);//光标
wndclass.hbrBackground = (HBRUSH)GetStockObject(WHITE_BRUSH);//画刷
wndclass.lpszMenuName = NULL;//菜单
wndclass.lpszClassName = szClassName;//类名称
//注册窗口类
if(!RegisterClass (&wndclass))
{
MessageBox (NULL, TEXT ("RegisterClass Fail!"),
szClassName, MB_ICONERROR);
return 0;
}
//建立窗口
HWND hwnd;
hwnd = CreateWindow(szClassName,//窗口类名称
TEXT ("The Simple Win32 Application"),//窗口标题
WS_OVERLAPPEDWINDOW,//窗口风格,即通常我们使用的windows窗口样式
CW_USEDEFAULT,//指定窗口的初始水平位置,即屏幕坐标系的窗口的左上角的X坐标
CW_USEDEFAULT,//指定窗口的初始垂直位置,即屏幕坐标系的窗口的左上角的Y坐标
CW_USEDEFAULT,//窗口的宽度
CW_USEDEFAULT,//窗口的高度
NULL,//父窗口句柄
NULL,//窗口菜单句柄
hInstance,//实例句柄
NULL);
ShowWindow(hwnd,iCmdShow);//显示窗口
UpdateWindow(hwnd);//立即显示窗口
//消息循环
MSG msg;
while(GetMessage(&msg,NULL,0,0))//从消息队列中取消息
{
TranslateMessage (&msg); //转换消息
DispatchMessage (&msg); //派发消息
}
return msg.wParam;
}
//消息处理函数
//参数:窗口句柄,消息,消息参数,消息参数
LRESULT CALLBACK WndProc(HWND hwnd, UINT message, WPARAM wParam, LPARAM lParam)
{
//处理感兴趣的消息
switch (message)
{
case WM_DESTROY:
//当用户关闭窗口,窗口销毁,程序需结束,发退出消息,以退出消息循环
PostQuitMessage(0);
return 0;
}
//其他消息交给由系统提供的缺省处理函数
return :
efWindowProc (hwnd, message, wParam, lParam);
}
这是一个非常简单的Win32小程序,编译运行会显示一个窗口,关闭窗口程序会结束运行。 代码中已经做了简单注解,这里我们不作过多说明。我在这里再着重讲解一下消息循环部分。
//消息循环
MSG msg;
while(GetMessage(&msg,NULL,0,0))//从消息队列中取消息
{
TranslateMessage (&msg); //转换消息
DispatchMessage (&msg); //派发消息
}
这段代码是消息循环部分,它的作用是循环检测消息队列(不懂消息队列?没关系,后面会详细说明)中的消息并进行处理。这段代码涉及
GetMessage,TranslateMessage,DispatchMessage这三个函数,相关函数还有
PeekMessage,WaitMessage。在此,我们先对这五个函数简单讲解。
1、GetMessage
函数原型:
BOOL GetMessage(LPMSG lpMsg, HWND hWnd, UINT wMsgFilterMin, UINT wMsgFilterMax);
参数:
lpMsg:一个指向MSG结构的指针,该结构用于存放从消息队列里取出的消息。
hWnd:窗口句柄。如果该参数是非零值,则GetMessage只检索该窗口(也包括其子窗口)消息,如果为零,则GetMessage检索整个进程内的消息。
wMsgFilterMin:指定被检索的最小消息值,也就是消息范围的下界限参数。
wMsgFilterMax:上界限参数。如果wMsgFilterMin和wMsgFilterMax都为零,则不进行消息过滤,GetMessage检索所有有效的消息。
返回值
GetMessage检索到WM_QUIT消息,返回值是零;其它情况,返回非零值。
函数功能:
这个API函数用来从消息队列中“摘取”一个消息,放到lpMsg所指的变量里。(注:如果所取窗口的消息队列中没有消息,则程序会暂停在GetMessage(…) 函数里,不会返回。)
再通俗一点讲解GetMessage函数:
当程序执行GetMessage()的时候,会检查消息队列,如果有消息在消息队列里,它取出该消息,将该消息填充到lpMsg所指的MSG结构,并返回
TRUE值。如果此时消息队列里没有消息(消息队列为空),它会将线程阻塞,也就是将控制权交给系统,直到消息队列中有内容时,才唤醒线程继续执行。
对于GetMessage()函数,还有一点需要说明,就是当从消息队列中取出的消息是WM_QUIT时,函数返回值是0。我们一般利用这一点退出消息循环,结束程序。
如语句:
while(GetMessage(&msg,NULL,0,0))
……
2 、PeekMessage
函数原型:
BOOL PeekMessage(LPMSG lpMsg,HWND hWnd,UINT wMsgFilterMin,UINT wMsgFilterMax,UINT wRemoveMsg);
参数:
lpMsg、hWnd、wMsgFilterMin、wMsgFilterMax这四个参数的意义和GetMessage对应参数的意义相同,在此不再赘述。
wRemoveMsg:这个参数决定读消息时是否删除消息,可选值有PM_NOREMOVE和PM_REMOVE。如果您选PM_NOREMOVE,执行
该函数后消息仍然留在消息队列(我称为读消息);如果您选PM_REMOVE,执行该函数后将在消息队列中移除该消息(同GetMessage())。
返回值:
消息队列中有消息,返回值为TRUE;消息队列中没有消息,返回值为FALSE。
函数功能:
PeekMessage()也是从消息队列中取消息,但它是GetMessage()不同,主要在以下两点:
(一)、GetMessage()只能从消息队列中取走消息,也就是说,GetMessage()执行后,该消息将从消息队列中移除。
PeekMessage()可以从消息队列中取走消息。也可以读消息,让消息继续留在消息队列里。
(二)、当消息队列中没有消息时,GetMessage()将会阻塞线程,等待消息;而PeekMessage()与GetMessage()不同,它执行后会立刻返回,消息队列中有消息时,返回值为TRUE;消息队列中没有消息时,返回值为FALSE。
3 、WaitMessage
函数原型:
BOOL WaitMessage(VOID);
函数功能:
这个函数的作用是当消息队列中没有消息时,将控制权交给其它线程。该函数将会使线程挂起,直到消息队列中又有新消息。
这个函数专门和PeekMessage配合使用,当消息队列中没有消息时,挂起线程,等待消息队列中新消息的到来,这样可以减轻CPU的运算负担。
4 、TranslateMessage
函数原型:
BOOL TranslateMessage(CON ST MSG*lpMsg);
参数:
IpMsg:指向MSG结构的指针,该结构是函数GetMessage或PeekMessage从消息队列里取得的消息。
函数功能:该函数将虚拟键消息转换为字符消息。字符消息被寄送到调用线程的消息队列里,当下一次线程调用函数GetMessage或PeekMessage时被读出。
什么是虚拟键码呢?Windows为了方便输入管理,减少程序对设备的依赖性,将键盘上所有的按键都用一个两位十六进制数对应,这些数称为虚拟键码。虚拟
键码一般以VK_开头,如:Esc键对应的虚拟键码是VK_ESCAPE;空格键对应的虚拟键码是VK_SPACE;VK_LWIN与左边的
Windows徽标键相对应。
当一个按键被按下时,会触发WM_KEYDOWN消息, WM_KEYDOWN消息的wParam参数值就是虚拟键值。通过这个值就可以判断哪个键被按下了。
为什么我们要把虚拟键码转换为字符码呢?
比如我们按下了‘A’键,此时我们得到的字符可能是‘A’,也可能是小写的‘a’,这由当时的大写状态(Caps
Lock)以及是否同时按下了Shift键有关。TranslateMessage()函数的作用就是不用我们考虑这些问题,而是根据这些情况,自动返回
一个ASCII码值,以方便用户使用。
并不是所有的虚拟键码值都会Translate成字符码。字母、数字键都有字符码相对应,而像方向箭头键、F1—F12功能键这些按键就没有字符码相对
应。当虚拟键码需要转化成字符码时,TranslateMessage()函数就在消息队列里放一条WM_CHAR消息,WM_CHAR消息的
wParam参数值就是转换后的ASCII码值。
5、DispatchMessage
函数原型:
LONG DispatchMessage(CON ST MSG *lpmsg);
函数功能:
它的作用很简单,就是分派消息到窗口的消息处理函数去执行。
了解了这5个函数,消息循环这段代码就不难理解:
GetMessage()从消息队列中取消息,对取出的消息进行转换(TranslateMessage),对于能够将虚拟键码转化成字符码的消息,会在
消息队列里放一条WM_CHAR消息,最后将消息发送到相应的消息处理函数进行处理。循环执行这个处理过程,直到收到WM_QUIT消息,才退出循环,结
束程序。
3、通过几个Win32程序实例进一步深入理解Windows消息
例程2:对比使用GetMessage和PeekMessage处理消息循环(见附带源码 工程M2)
同工程M1,新建工程M2,将工程M1的源代码全部拷贝到M2,并将消息循环部分的代码改为:
//消息循环
MSG msg;
while(true)
{
if(PeekMessage(&msg,NULL,0,0,PM_REMOVE)) //从消息队列中取消息
{
if(msg.message == WM_QUIT)
break;
TranslateMessage (&msg); //转换消息
DispatchMessage (&msg); //派发消息
}
else
WaitMessage();
} //End of while(true)
编译、运行工程M2,观察运行效果,可以看出,使用PeekMessage处理消息循环同样能够达到与GetMessage相同的效果。
PeekMessage处理消息循环比GetMessage还要灵活,尤其体现在游戏编程中。游戏编程者不希望玩家在没有键盘或鼠标输入时游戏是静止不动
的,他们希望怪兽从后面冲出来,围攻玩家,追捕玩家。为了做到这样的效果,需要这样一种消息循环:当遇到需要处理的消息时去处理消息,其余的时间都让程序
代码自动产生激烈的场面。
下面的例程3将模拟这种消息循环。
例程3:模拟演示游戏编程如何进行消息处理(见附带源码工程M3)。
详细的代码参看工程M3,编译并执行,您会发现程序不停地自己画圆,这模拟游戏自动产生激烈的场面。当您按下上、下、左、右箭头键,您就会发现您在相应的方向画线,这模拟游戏程序及时处理玩家的消息。
4、队列消息和非队列消息
Windows把消息分为两种:一种是需要立即处理的消息,另一种是不需要立即处理的消息。
对于需要立即处理的消息,Windows直接把它送给窗口的消息处理函数进行处理,这类消息我们叫做非队列消息;
而对于不需要立即处理的消息,Windows会把它发送给应用程序的消息队列进行排队,由应用程序逐个进行处理,我们把这类消息叫做队列消息。
为了更清楚地说明这个问题,我们参看图1:

图1
图1的解释:
1、Windows操作系统有一个消息队列,它存放操作系统收到的消息。如:当按键被按下,键盘会发送一个消息到操作系统的消息队列。
2、操作系统把系统消息队列中的消息分派到各个应用程序的消息队列。如果它是第1个应用程序的消息,操作系统把它发给第1个应用程序,把它放在第1个应用程序的消息队列;如果它是第2个应用程序的消息,发送给第2个程序的消息队列。
3、应用程序的消息循环从自己的消息队列中取消息,取出的消息调用窗口过程函数进行处理。
4、PostMessage是寄送消息,函数执行后立即返回。寄送的消息是队列消息,放在程序的消息队列中排队处理。一般来说,新寄送的消息排在消息队列的末尾,这样可以保证窗口以先进先出的顺序处理消息。
SendMessage是发送消息,它发出的消息是非队列消息,直接调用窗口过程函数处理。SendMessage函数一直等消息处理完成后才返回。
我们有必要再专门学习一下SendMessage和PostMessage函数。
SendMessage的函数原型:
LRESULT SendMessage(HWND hWnd,UINT Msg,WPARAM wParam,LPARAM lParam);
这个函数向窗口发送一条消息,一直等到消息被处理之后才返回。也就是说,接收消息的窗口的窗口函数立即被调用。函数的返回值由接收消息的窗口的窗口函数返回。
PostMessage的函数原型:
BOOL PostMessage(HWND hWnd,UINT Msg,WPARAM wParam,LPARAM lParam);
该函数把一条消息放置到创建hWnd窗口的线程的消息队列中,该函数不等消息被处理就马上将控制返回。
从上面这两个函数,我们可以看出消息的发送方式和寄送方式的区别:被发送的消息会被立即处理,处理完毕后函数才返回;被寄送的消息不会被立即处理,他被放到一个先进先出的队列中,按次序等候处理,而且函数放置消息后立即返回。
以寄送方式发送的消息通常是与用户输入事件相对应的,因为这些事件不是十分紧迫,可以进行缓冲处理,例如鼠标、键盘消息都是寄送消息。应用程序调用系统函
数,系统一般会发送非队列消息。例如,当程序调用SetWindowPos,系统会发送WM_WINDOWPOSCHANGED消息。
例程M4,测试消息队列的容量(见附带源码工程M4)
代码中已经作了注解,编译、运行程序,您就会发现消息队列的最大容量是10000。
例程M5,用记事本查看消息队列和窗口过程函数处理的消息
这个例程的出发点是利用记事本分别捕获消息队列中的消息和窗口过程函数处理过的消息。
该例程还演示了PostMessage和SendMessage的不同。
由于该例程相对复杂一些,例程中的注解也相对多一些。编译、运行程序,弹出如下窗口:

关闭该窗口,退出运行,检查M5例程所在的路径,您就会发现多了两个文件MessageQueue.txt和
MessageWndProc.txt,MessageQueue.txt文件中记录的是应用程序M5从运行到关闭消息队列中处理过的消
息;MessageWndProc.txt中记录的M5窗口过程函数处理过的消息。
打开MessageQueue.txt文件,如下图:

文件中记录了消息队列中的各个消息以及消息的ID号,其中有一条消息是WM_POSTMESSAGE,这说明PostMessage寄送的WM_POSTMESSAGE消息确实放到了消息队列中。
再打开MessageWndProc.txt文件,如下图:

文件中记录了窗口过程处理的各个消息和消息的ID号,其中有两条消息WM_POSTMESSAGE和WM_SENDMESSAGE,这说明了两个问
题:WM_POSTMESSAGE消息从消息队列取出,再次派发到窗口过程函数处理;SendMessage发送的WM_SENDMESSAGE消息,没
有经过消息队列,直接送到窗口过程函数处理。
5、WM_COMMAND和WM_NOTIFY
控件通知消息,是指这样一种消息,一个窗口内的控件发生了一些事情,需要通知父窗口。当用户与控件窗口 交互时,控件通知消息就会从控件窗口发送到它的主窗口,这种消息一般不是为了处理用户命令,而是为了让主窗口能够改变控件。
WM_COMMAND和WM_NOTIFY都是控件通知消息。
在最初的Windows
3.x中,还没有WM_NOTIFY,只存在WM_COMMAND消息,wParam参数中包含一个通知码和控件ID,lParam中包含控件句柄。这样
一来,wParam和lParam都被填充了,没有额外的空间来传递一些其它信息,如鼠标按下的位置和时间。
为了解决这个问题,Windows 3.x就提出了一个解决策略,那就是给一些消息添加一些附加消息,比如控件自画用到的DRAWITEMSTRUCT等,这样,不同的消息附加的内容不同,结果是非常混乱。
在Win32中,微软又提出了一个更好的解决方案,引进了NMHDR结构。这个结构的引进把消息统一起来,利用它可以传递各种复杂的消息。
NMHDR结构内容如下:
NMHDR
{
HWND hWndFrom;//相当于原WM_COMMAND消息的lParam
UINT idFrom; //相当于原WM_COMMAND消息的wParam(LOWORD)
UINT code; //相当于原WM_COMMAND消息的wParam(HIWORD)通知码
}
使用这个结构,WM_NOTIFY还可以附带更多的信息,您可以定义一个更大的结构,这个结构的第一个元素就是NMHDR结构,在该元素的后面您还可以放
置其它附加信息。由于在这个大结构中,第一个成员是NMHDR,这样一来,我们就可以利用指向NMHDR的指针来指向这个结构,不论后面有没有其它内容。
可见,WM_NOTIFY和WM_COMMAND相比,是一种更灵活的消息格式,lParam中放的是一个称为NMHDR结构的指针。在wParam中放
的则是控件的ID。最初Windows
3.x就有的控件,如Edit,Combo,List,Button等,发送的控件通知消息的格式是WM_COMMAND;而后期的Win32通用控件,
如List View,Image List,IP Address,Tree
View,Toolbar等,发送的都是WM_NOTIFY控件通知消息。
另外,当用户选择菜单的一个命令项,也会发送WM_COMMAND消息。
当用户选择菜单的一个命令项或控件给父窗口发送通知消息,都可以使用WM_COMMAND消息。为了区分这两种情况,规定它们有以下区别,如表2:
消息来源
wParam (high word)
wParam (low word)
lParam
菜单
0
菜单标识符 (IDM_*)
0
控件
控件定义的通知码
控件ID
控件窗口的句柄
表2
例程M6,演示菜单发出WM_COMMAND消息和子控件发送WM_COMMAND消息的区别(见附带源码工程M6)
打开VC++ 6.0,新建Win32 Application工程M6,然后在该工程中新建C++ Source File,文件名为M6,M6的文件内容具体见例程M6。
在例程M6所在的路径打开M6文件夹,新建一个文本文档,如下图:

将“新建文本文档.txt”改名为“M6.rc”,如下图:

右键单击M6.rc,在弹出的快捷菜单中使用“写字板”打开,如下图:

添加的内容具体见M6.rc,保存后退出。编译、运行工程M6,弹出如下窗口:

分别单击“FirstButton”按钮和“Menu1”菜单,会弹出相应的提示消息框。
M6中对于WM_COMMAND消息的处理,源代码如下:
case WM_COMMAND:
{
if(lParam == 0)
{
switch(LOWORD(wParam))
{
case IDM_MENU1:
MessageBox(NULL,"MENU1菜单被点击","M6",MB_OK);
break;
case IDM_EXIT:
DestroyWindow(hwnd);
break;
}
}
else //处理子控件触发的WM_COMMAND控件通知消息
{
//(LOWORD(wParam))是控件ID
switch(LOWORD(wParam))
{
case ButtonID1:
if(HIWORD(wParam) == BN_CLICKED)
{
MessageBox(NULL,"按钮被点击","M6",MB_OK);
}
break;
}
}
}
break;
对于WM_COMMAND消息,因为菜单和子控件都能触发。我们首先判断lParam,如果lParam为0,是菜单触发的WM_COMMAND消息;如
果lParam不为0,是子控件触发的WM_COMMAND控件通知消息。对于菜单触发的WM_COMMAND消息,我们再通过
(LOWORD(wParam))(菜单的标识ID)判断是哪个菜单触发的消息;对于控件触发的WM_COMMAND消息,我们通过
(LOWORD(wParam))(控件ID)知道是哪个控件触发的消息,而且通过(HIWORD(wParam))(控件定义的通知码)知道控件到底触
发了什么消息。
本例程我们纯手工添加并编辑资源文件M6.rc,之所以这样做是为了让您了解资源文件的实质。实际编程中,您完全可以利用资源编辑器更加方便地添加、编辑资源文件,后面的例程将会演示说明。
例程M7,演示WM_NOTIFY控件通知消息(见附带源码 工程M7)
WM_NOTIFY消息是通用控件发送给其父窗口的消息,其中参数wParam 是发送消息的通用控件的ID,参数lParam 是一个指针,这个指针指向一个 NMHDR 结构,该结构包含了通知码和其它附加信息。
下面我们看结构NMHDR:
typedef struct tagNMHDR {
//发送消息的控件的句柄,相当于原WM_COMMAND消息的lParam
HWND hwndFrom;
//发送消息的控件的ID,相当于原WM_COMMAND消息的wParam(LOWORD)
UINT idFrom;
//通知码,也就是发送的具体消息,相当于原WM_COMMAND消息的wParam(HIWORD)通知码
UINT code;
} NMHDR;
打开VC++ 6.0,新建Win32 Application工程M7,然后在该工程中新建C++ Source File,文件名为M7,M7的文件内容具体见例程M7。
下面,我们利用资源编辑器添加资源。单击“文件”->“新建”,在“新建”对话框中选中“Resource Script”,文件名为“M7”,如下图:

单击“确定”,添加M7资源文件。
右击“M7.RC”文件夹,选中“Insert…”菜单项,如下图:

弹出“插入资源”对话框,

选中“Dialog”,点击“新建”按钮,新建一个对话框资源。
右击新建的“IDD_DIALOG1”,在属性对话框中将ID改为“IDC_DIALOG”,关闭属性框。

双击“IDC_DIALOG”,打开该对话框,调整至合适大小,在对话框上添加一个列表控件(List Control),将该列表控件的ID设置为IDC_LIST,如下图:

并且把列表控件改为“Report”类型,如下图:

编辑并运行程序,程序运行会弹出如下对话框:

分别用鼠标双击第一行或第二行,会弹出相应消息框。
程序代码都有详细注释,您可以阅读代码,细细体会WM_NOTIFY控件通知消息。
6、MFC的消息映射
使用MFC编程时,消息发送和处理的本质和Win32相同,但是,它对消息处理进行了封装,简化了程序员编程时消息处理的复杂性,它通过消息映射机制来处理消息,程序员不必去设计和实现自己的窗口过程。
说白了,MFC中的消息映射机制实质是一张巨大的消息及其处理函数对应表。消息映射基本上分为两大部分:
在头文件(.h)中有一个宏DECLARE_MESSAGE_MAP(),它放在类的末尾,是一个public属性的;与之对应的是在实现部分(.cpp)增加了一个消息映射表,内容如下:
BEGIN_MASSAGE_MAP(当前类,当前类的基类)
//{{AFX_MSG_MAP(CMainFrame)
消息的入口项
//}}AFX_MSG_MAP
END_MESSAGE_MAP()
但是仅是这两项还不足以完成一条消息,要是一个消息工作,必须还有以下3个部分去协作:
1、在类的定义中加入相应的函数声明;
2、在类的消息映射表中加入相应的消息映射入口项;
3、在类的实现中加入相应的函数体;
消息的添加
(1)、利用Class Wizard实现自动添加
在菜单中选择View -> Class Wizard激活Class Wizard,选择Message Map标签,从Class
name组合框中选取我们想要添加消息的类。在Object
IDs列表框中,选取类的名称。此时,Messages列表框显示该类的可重载成员函数和窗口消息。可重载成员函数显示在列表的上部,以实际虚构成员函数
的大小写字母来表示。其他为窗口消息,以大写字母出现。选中我们要添加的消息,单击Add Funtion按钮,Class
Wizard自动将该消息添加进来。
有时候,我们想要添加的消息在Message列表中找不到,我们可以利用Class Wizard上Class Info标签以扩展消息列表。在该页中,找到Message Filter组合框,通过它可以改变首页中Messages列表框中的选项。
(2)、手动添加消息
如果Messages列表框中确实没有我们想要的消息,就需要我们手工添加:
1)在类的.h文件中添加处理函数的声明,紧接着在//}}AFX_MSG行之后加入声明,注意,一定要以afx_msg开头。
通常,添加处理函数声明的最好的地方是源代码中Class Wizard维护的表的下面,在它标记其领域的{{ }}括弧外面。这些括弧中的任何东西都有可能会被Class Wizard销毁。
2)接着,在用户类的.cpp文件中找到//}}AFX_MSG_MAP行,紧接在它之后加入消息入口项。同样,也放在{{ }}外面。
3)最后,在该文件中添加消息处理函数的实体。
对于能够使用Class Wizard添加的消息,尽量使用Class Wizard添加,以减少我们的工作量;对于不能使用Class Wizard添加的消息和自定义消息,需要手动添加。总体说来,MFC的消息编程对用户来说,相对比较简单,在此不再使用实例演示。
7、消息反射机制
什么叫消息反射?
父窗口将控件发给它的通知消息,反射回控件进行处理(即让控件处理这个消息),这种通知消息让控件自己处理的机制叫做消息反射机制。
通过前面的学习我们知道,一般情况下,控件向父窗口发送通知消息,由父窗口处理这些通知消息。这样,父窗口(通常是一个对话框)会对这些消息进行处理,换
句话说,控件的这些消息处理必须在父窗口类体内,每当我们添加子控件的时候,就要在父窗口类中复制这些代码。很明显,这对代码的维护和移植带来了不便,而
且,明显背离C++的对象编程原则。
从4.0版开始,MFC提供了一种消息反射机制(Message
Reflection),可以把控件通知消息反射回控件。具体地讲,对于反射消息,如果控件有该消息的处理函数,那么就由控件自己处理该消息,如果控件不
处理该消息,则框架会把该消息继续送给父窗口,这样父窗口继续处理该消息。可见,新的消息反射机制并不破坏原来的通知消息处理机制。
消息反射机制为控件提供了处理通知消息的机会,这是很有用的。如果按传统的方法,由父窗口来处理这个消息,则加重了控件对象对父窗口的依赖程度,这显然违背了面向对象的原则。若由控件自己处理消息,则使得控件对象具有更大的独立性,大大方便了代码的维护和移植。
实例M8:简单地演示MFC的消息反射机制。(见附带源码 工程M8)
打开VC++ 6.0,新建一个基于对话框的工程M8。
在该工程中,新建一个CMyEdit类,基类是CEdit。接着,在该类中添加三个变量,如下:
private:
CBrush m_brBkgnd;
COLORREF m_clrBkgnd;
COLORREF m_clrText;
在CMyEdit::CMyEdit()中,给这三个变量赋初值:
{
m_clrBkgnd = RGB( 255, 255, 0 );
m_clrText = RGB( 0, 0, 0 );
m_brBkgnd.CreateSolidBrush(RGB( 150, 150, 150) );
}
打开ClassWizard,类名为CMyEdit,Messages处选中“=WM_CTLCOLOR”,您是否发现,WM_CTLCOLOR消息前面有一个等号,它表示该消息是反射消息,也就是说,前面有等号的消息是可以反射的消息。

消息反射函数代码如下:
HBRUSH CMyEdit::CtlColor(CDC* pDC, UINT nCtlColor)
{
// TODO: Change any attributes of the DC here
pDC->SetTextColor( m_clrText );//设置文本颜色
pDC->SetBkColor( m_clrBkgnd );//设置背景颜色
//请注意,在我们改写该函数的内容前,函数返回NULL,即return NULL;
//函数返回NULL将会执行父窗口的CtlColor函数,而不执行控件的CtlColor函数
//所以,我们让函数返回背景刷,而不返回NULL,目的就是为了实现消息反射
return m_brBkgnd; //返回背景刷
}
在IDD_M8_DIALOG对话框中添加一个Edit控件,使用ClassWizard给该Edit控件添加一个CMyEdit类型的变量m_edit1,把Edit控件和CMyEdit关联起来。
浙公网安备 33010602011771号