Windows消息编程

Windows消息编程
Windows消息编程

本文主要包括以下内容:

1、简单理解Windows的消息

2、通过一个简单的Win32程序理解Windows消息

3、通过几个Win32程序实例进一步深入理解Windows消息

4、队列消息和非队列消息

5、WM_COMMAND和WM_NOTIFY

6、MFC的消息映射

7、消息反射机制

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

//声明窗口过程函数

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 ::DefWindowProc (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(CONST 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(CONST 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、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

菜单

菜单标识符 (IDM_*)

控件

控件定义的通知码

控件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”,如下图:

分别单击“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 结构,该结构包含了通知码和其它附加信息。

相关主题
相关文档
最新文档