翻译自MSDN(https://docs.microsoft.com/zh-cn/windows/desktop/winmsg/about-messages-and-message-queues)
与基于MS-DOS的程序不同,基于Windows的程序是事件驱动(event-driven)的,这些应用程序不是通过显式地函数调用来获取输入,而是等待系统将输入传递给它们。
系统将属于某个应用程序的输入传递给这个应用程序的窗口。每个窗口都有一个叫“窗口过程(window procedure)”的函数,当窗口有输入时系统便会调用这个“窗口过程”函数。“窗口过程”函数处理输入然后将控制返回系统。
如果一个顶层窗口(top-level window)在几秒内没有响应消息时(可能是“窗口过程”函数执行太久),系统将认为这个窗口无响应。这时,系统将会隐藏这个窗口,并用一个“幽灵窗口(ghost window)”替代它。“幽灵窗口”与被替代的窗口有相同的Z顺序(Z order)、位置、大小和显示属性。用户可以拖动、缩放它,设置可以关闭应用程序。用户只能对它施加上述操作,因为应用程序实际上处于无响应状态。但在调试模式下,系统不会产生“幽灵窗口”。
Windows消息(Windows Messages)
系统通过消息(message)的方式将输入传递给“窗口过程”,应用程序和系统都可以产生消息。系统会在每个输入事件产生消息(例如,用户敲击键盘,移动鼠标,点击控件)。系统也会为由应用程序引起的系统改变(像应用程序改变了系统的字体资源池或者调整了窗口大小)而产生消息。应用程序产生的消息,可以传递给自己的窗口以执行一些任务,也可以用来与其他应用程序的窗口通信。
系统发送给“窗口过程”的消息包含4个参数:一个窗口句柄,一个消息标识,以及两个称为消息参数(message parameters)的参数值。
- 窗口句柄标识了这个消息是要传递给哪个窗口的,系统通过它来决定调用哪个“窗口过程”。
- 消息标识是一个用来表示消息的用途的常量。“窗口过程”收到消息后,通过消息标识来决定如何处理消息。例如,消息标识WM_PAINT告诉“窗口过程”窗口客户区改变了,窗口需要重绘。
- 消息参数指出“窗口过程”处理消息使用的数据或数据地址,参数值的意义决定于消息标识。参数值可以是整数,位掩码,指向结构体的指针等。当消息不需要参数值时,参数值通常设置为NULL。“窗口过程”必须通过消息标识来确定如何解析消息参数。
消息类型(Message Types)
消息类型有两种,系统消息和应用程序消息。
系统消息(System-Defined Messages)
系统发送(send)和邮递(post)系统消息来以跟应用程序通信,它通过这些消息来控制应用程序的行为,并提供输入和其他信息给应用程序进行处理。应用程序也可以send和post系统消息,应用程序通常用这些消息来控制使用注册窗口类(preregistered window classes)创建的控件的行为。
每个系统消息都有一个唯一消息标识以及对应的符号常量,来声明消息的用途。例如,WM_PAINT要求窗口进行重绘。
符号常量指明了系统消息的类别,它的前缀标识了可以解析和处理此消息的窗口类型。
窗口消息涵盖许多信息和要求,包括光标和键盘输入、菜单和对话框输入、窗口创建和管理,以及动态数据交换(DDE, Dynamic Data Exchange)。
应用程序消息(Application-Defined Messages)
应用程序可以创建消息来给自身的窗口使用或者用来与其他应用程序的窗口通信。如果应用程序创建了自身使用的消息,接受此消息的“窗口过程”必须解析和处理它。
消息标识按如下方式使用:
系统保留了0x0000到0x03FF(WM_USER - 1)的消息标识作为系统消息,应用程序不能把这些值作为自定义消息。
0x0400(WM_USER)到0x7FFF的值可作为应用程序的自定义消息。
如果你的应用程序是4.0版本的(没搞懂什么4.0版本),可以使用0x8000(WM_APP)到0xBFFF的值作为自定义消息。
注:WM_USER和WM_APP的使用规则参考https://stackoverflow.com/questions/4406909/using-wm-user-wm-app-or-registerwindowmessage
当应用程序调用RegisterWindowMessage函数来注册消息时,系统会返回0xC000到0xFFFF之间的一个值作为消息标识,这个函数返回的消息标识在整个系统中是唯一的,用这个函数返回的消息标识可以避免因其他应用程序使用与本应用程序相同的消息标识但用途却不一样而导致的冲突。
消息路由(Message Routing)
消息路由有两种方式:
- 邮递(post)消息到一个先入先出消息队列(message queue),消息队列是系统定义的内存对象,用来临时存放消息。
- 直接把消息发送(send)给“窗口过程”。
邮递(post)到消息队列的消息称为队列消息(queued message),主要有鼠标和键盘输入消息,像WM_MOUSEMOVE、WM_LBUTTONDOWN、WM_KEYDOWN和WM_CHAR。其他一些队列消息包括定时器消息WM_TIMER、绘图消息WM_PAINT和退出消息WM_QUIT。剩下的大部分消息,都是直接发送(send)到“窗口过程”的,称为非队列消息(nonqueued message)。
队列消息(Queued Messages)
系统可以同时显示任意数量的窗口,为了将鼠标可键盘输入路由到正确的窗口,系统使用了消息队列。
系统维护着一个系统消息队列,并为每个GUI线程维护着一个线程消息队列。为了避免给非GUI线程创建消息队列,所有线程刚创建时都是没有消息队列的。只有当这个线程调用了一个特定的用户函数时系统才会为它创建一个线程消息队列,调用GUI函数并不会导致系统为线程创建消息队列。
当用户移动、点击鼠标或者敲键盘时,鼠标和键盘的驱动会将这些输入信息转换成消息放置于系统消息队列。系统从系统消息队列取出一个消息(同时会将其从消息队列移除),然后检查这个消息,确认目标窗口,再将消息邮递(post)到创建目标窗口的线程的消息队列中。线程消息队列会接收到所有需要传递给该线程创建的窗口的鼠标和键盘消息。线程从消息队列取出消息,指引系统将消息送给正确的“窗口过程”进行处理。
除了WM_PAINT、WM_TIMER、WM_QUIT之外,系统总是将消息置于消息队列的末尾,这保证了窗口以先进先出的方式接收到输入消息。对于WM_PAINT、WM_TIMER、WM_QUIT消息,只有当消息队列里没有其它消息时,才会被传递给“窗口过程”,否则将一直留在消息队列里。此外,属于同一个窗口的多个WM_PAINT消息会被合并成一个消息,所有无效的客户区也会被合并成一个。合并WM_PAINT消息可以减少窗口重绘客户区的次数。
系统邮递(post)消息到线程消息队列时,先填充一个MSG结构体,然后将这个结构体拷贝到消息队列。MSG中的信息包括:目标窗口的句柄,消息标识,两个消息参数,消息邮递(post)的时间,以及光标位置。线程可以调用PostMessage来将一个消息邮递(post)到自己消息队列,或者dao调用PostThreadMessage来将消息邮递(post)到其它线程的消息队列。
应用程序可以调用调用GetMessage将一个消息取出消息队列。如果只想获取队列中的消息而不将消息从队列移除,可以用PeekMessage,这个函数会用消息队列中的消息信息来填充MSG结构体。
从消息队列中取出一个消息之后,应用程序可以调用DispatchMessage来引导系统将消息传递给“窗口过程”。DispatchMessage接收之前用GetMessage或PeekMessage填充的结构体MSG的指针,将窗口句柄、消息标识以及两个消息参数传递给“窗口过程”,但不传递消息邮递时间和光标位置,应用程序可以调用 GetMessageTime和GetMessagePos来获取这两个信息。
当消息队列中没有消息时,线程可以调用WaitMessage来将控制权移交给其它线程,WaitMessage会一直阻塞直到有新消息被放入到消息队列。
如果想要给线程消息队列绑定额外的数据,可以调用SetMessageExtraInfo,然后调用GetMessageExtraInfo来获取与上一次调用GetMessage或PeekMessage获取到的消息绑定在一起的数据。
非队列消息(Nonqueued Messages)
非队列消息绕过系统消息队列和线程消息队列直接传给目标“窗口过程”,系统通常在要通知窗口有影响它的事件时发送非队列消息。例如,当用户激活一个应用程序窗口时,系统会给窗口发送一系列消息,包括 WM_ACTIVATE、WM_SETFOCUS、WM_SETCURSOR。这些消息分别通知窗口被激活了,键盘输入会传递给窗口,光标在窗口边界内移动。应用程序调用特定的系统函数时也会产生非队列消息。例如,应用程序调用SetWindowPos来移动窗口,系统会发送WM_WINDOWPOSCHANGED消息。
发送非队列消息函数有BroadcastSystemMessage、BroadcastSystemMessageEx、SendMessage、SendMessageTimeout、SendNotifyMessage等。
消息处理(Message Handling)
应用程序必须取出和处理线程邮递到消息队列中的消息。单线程的应用程序通常在它的WinMain函数中使用消息循环(message loop),来取出消息并把消息传递给正确的“窗口过程”进行处理。多线程的应用程序可以在每个创建了窗口的线程中都使用一个消息循环。下面将描述消息循环的工作机制并解释“窗口过程”的作用。
消息循环(Message Loop)
一个简单的消息循环函数需要调用GetMessage, TranslateMessage, and DispatchMessage。注意,GetMessage如果发生错误会返回-1,因此需要特别判断。
1 MSG msg;
2 BOOL bRet;
3
4 while( (bRet = GetMessage( &msg, NULL, 0, 0 )) != 0)
5 {
6 if (bRet == -1)
7 {
8 // handle the error and possibly exit
9 }
10 else
11 {
12 TranslateMessage(&msg);
13 DispatchMessage(&msg);
14 }
15 }
GetMessage从消息队列取出一个消息并将它拷贝到MSG结构体。GetMessage返回非0值,除非遇到WM_QUIT消息,这时返回FALSE,然后结束循环。在但线程应用程序中,结束消息循环通常是关系应用程序的第一步。应用程序可以调用PostQuitMessage来结束自己的循环,通常在“窗口过程”中响应WM_DESTROY时会这么做。
如果调用GetMessage时传递了窗口句柄参数,那么只有属于指定窗口的消息会被取出来。GetMessage还可以过滤消息,取出那些在指定范围的消息,,详见Message Filtering主题。
如果一个线程要接收键盘的字符输入,就必须包含TranslateMessage。每当用户按下键盘按键时,系统产生一个虚拟键(virtual-key)消息(WM_KEYDOWN和WM_KEYUP),虚拟键消息包含一个标识哪个键被按下的虚拟键码,但不包含字符值。为了得到字符值,消息循环必须包含TranslateMessage,这个函数将虚拟键消息转换成字符消息(WM_CHAR)并将字符消息放到消息队列末尾,然后字符消息就可以在消息循环中被取出来分发给“窗口过程”。
DispatchMessage将消息传递给与MSG结构体中指定的窗口句柄关联的“窗口过程”。如果窗口句柄是HWND_TOPMOST,DispatchMessage把消息发给系统中所有的顶层窗口(top-level window)。如果窗口句柄是NULL,那么DispatchMessage什么也不干。
应用程序的住线程在初始化完成并创建了至少一个窗口后启动消息循环。消息循环启动后便持续地从消息队列中取出消息并分发给对应的窗口。当GetMessage从消息队列中取出WM_QUIT时,消息循环结束。
一个消息队列值需要一个消息循环,即使一个应用程序有多个窗口,DispatchMessage总会把消息分发给正确的窗口。因为消息队列中的每个消息都是一个MSG结构体,其中包含了消息所属的窗口的句柄。
可以调整消息循环的执行方式。例如,可以从消息队列中取出一个消息但是不分发它,这在应用程序需要邮递(post)没有指定窗口的消息时很有用。也可以指引GetMessage查找指定的消息,把其它消息留在队列里,如果你需要临时绕过消息队列先入先出(FIFO)的方式,这就很有用了。
应用程序如果用到了加速键(accelerator keys),就必须把键盘消息转换成命令消息,为了完成这个任务,应用程序的消息循环需要调用TranslateAccelerator。加速键的更多信息见Keyboard Accelerators主题。
如果线程用到了非模态对话框(modeless dialog box),消息循环必须调用IsDialogMessage才能让对话框接收到键盘输入。
“窗口过程”(Window Procedure)
“窗口过程”是接收并处理所有传递给窗口的消息的函数。每个窗口类(Windows Class)都有一个“窗口过程”,每个用窗口类创建的窗口都使用相同的“窗口过程”来处理消息。
系统通过把消息数据以参数的形式传给“窗口过程”来传递消息给“窗口过程”,接着“窗口过程”根据消息执行相应的操作。“窗口过程”检查消息标识,在处理消息时使用消息参数指示的信息。
“窗口过程”一般不会忽略消息,如果它不处理消息,则必须把消息返回给系统以进行默认处理,这是通过调用DefWindowProc来实现的。DefWindowProc进行默认处理并返回结果,“窗口过程”必须把这个结果返回。大部分“窗口过程”只处理一些消息,通过调用DefWindowProc把其它的消息都交给系统处理。
因为一个“窗口过程”是被所有由同一个窗口类创建的窗口共用的,所以它能够处理几种不同窗口的消息。“窗口过程”可以通过检查消息中的窗口句柄来识别消息所影响的窗口。更多信息见Window Procedures主题。
消息过滤(Message Filtering)
应用程序调用GetMessage和PeekMessage时可以指定消息过滤器以从消息队列中选择指定的消息。消息过滤器是可以是一组消息标识(由首位标识和末位标识限定),一个窗口句柄,或者两者。GetMessage和PeekMessage使用消息过滤器来决定从消息队列取出哪些消息。消息过滤在应用程序需要查找后面达到消息队列的消息时很有用。当应用程序需要在处理邮递的消息之前处理硬件输入,消息处理也很有用。
WM_KEYFIRST和WM_KEYLAST可以用来获取所有键盘消息;WM_MOUSEFIRST和 WM_MOUSELAST可以用来获取所有的鼠标消息。
应用程序过滤消息时必须确保想要获取的消息时能够接收到的。比如,如果应用程序要获取WM_CHAR消息,但该应用程序的窗口却无法接收键盘收入,那么GetMessage将会造成阻塞。
邮递消息和发送消息(Posting and Sending Messages)
所有应用程序都可以邮递(post)和发送(send)消息。跟系统一样,应用程序通过把消息拷贝到消息队列来邮递消息,通过把消息数据作为参数传递给“窗口过程”来发送消息。应用程序可以调用PostMessage来邮递消息,调用SendMessage, BroadcastSystemMessage, SendMessageCallback, SendMessageTimeout, SendNotifyMessage, SendDlgItemMessage来发送消息。
邮递消息(Posting Messages)
应用程序一般通过邮递消息来通知特定的窗口执行某项任务, PostMessage会为消息创建一个MSG结构体并将这个结构体拷贝到消息队列,消息循环把消息取出来分发到对应的“窗口过程”。
应用程序可以邮递不指定窗口的消息。如果调用PostMessage时传递一个NULL窗口句柄,消息将被放到当前线程的消息队列。由于没有指定窗口,必须在消息循环中处理消息。这也是用来创建一个影响整个应用程序而非某特定窗口的消息的方法。
有时你可能想邮递消息给系统中所有的顶层窗口(top-level window),在调用PostMessage时传递HWND_TOPMOST作为hwnd的实参就可以完成这个功能。
一个常见的编程错误是假定PostMessage总是能邮递出一个消息,事实是当消息队列满了PostMessage就无法邮递消息了。应用程序应该检查PostMessage的返回值来确认消息是否被邮递出去了,如果没有,重新邮递。
发送消息(Sending Messages)
应用程序通常发送消息来通知“窗口过程”立即执行某项任务。SendMessage把消息发送过窗口对应的“窗口过程”,这个函数会一直等待直到“窗口过程”完成了处理并返回消息处理结果。父窗口和子窗口经常通过互发消息来通信。例如,拥有一个编辑控件(edit control)的父窗口可以通过发消息来设置该控件的文本,编辑控件可以通过发消息给父窗口来通知父窗口用户改变了编辑控件的文本。
SendMessageCallback也可以把消息发给窗口对应的“窗口过程”,但是,这个函数会立即返回。当“窗口过程”处理完消息之后,系统调用指定的回调函数(callback function)。更多详情见SendAsyncProc函数相关内容。
有时你可能想法消息给系统中的所有顶层窗口。例如,如果应用程序修改了系统时间,则必须发送WM_TIMECHANGE消息来通知所有的顶层窗口。在调用 SendMessage时,把HWND_TOPMOST作为hwnd的实参可以将消息发给所有顶层窗口。你还可以调用BroadcastSystemMessage来将消息广播给所有应用程序,只要把BSM_APPLICATIONS作为 lpdwRecipients的实参就行了。
“窗口过程”可以调用InSendMessage和InSendMessageEx来判断消息是否来自其他线程,当消息处理需要依赖消息来源时这个功能就很有用了。
消息死锁 (Message Deadlocks)
线程调用SendMessage发消息给其它线程将会阻塞直到接收这个消息的“窗口过程”返回。如果接收消息的线程在处理消息时将控制权移交走了,发送消息的线程就不能继续执行,因为它正等待SendMessage返回。如果接收消息的线程附加到了与发送消息的线程相同的消息队列,可能会引起应用程序的死锁。
注意,接收消息的线程不一定显式移交控制权,调用下面几个函数会导致线程隐式移交控制权。
DialogBox
DialogBoxIndirect
DialogBoxIndirectParam
DialogBoxParam
GetMessage
MessageBox
PeekMessage
SendMessage
为了防止应用程序中可能的死锁,考虑使用SendNotifyMessage或SendMessageTimeout。另外,“窗口过程”可以调用InSendMessage或InSendMessageEx来判断判断接收到的消息是否来自其它线程。“窗口过程”处理消息时,在调用上面列出的任何函数之前,应该先调用InSendMessage或InSendMessageEx,如果这两个函数返回TRUE,“窗口过程”必须在调用任何会移交控制权的函数前调用ReplyMessage。
广播消息(Broadcasting Messages)
每个消息里都包含消息标识,以及两个参数,wParam,lParam。消息标识是指明消息用途的唯一标识,参数提供特定于消息的额外信息,但wParam通常用来作为类型以提供关于消息的更多信息。
消息广播(message broadcast)简单来说就是把一个消息发给系统中的多个接收者。应用程序调用BroadcastSystemMessage并指定消息接受者来广播消息,你必须指定一类或多类消息接受者,而不是单个,接受者类别包括应用程序, 可安装驱动(installable drivers), 网络驱动(network drivers), 系统层设备驱动(system-level device drivers)。系统发送广播消息给指定类别的所有成员。
系统一般在系统层的设备驱动(system-level device drivers)或相关组件反生变化时广播消息。驱动或相关组件通过广播消息来把自身的变化通知应用程序和其他组件。例如,负责磁盘驱动的组件在软盘驱动器(floppy disk)的设备驱动程序检测到介质变动时(像用户在驱动器中插入了磁盘),将会广播消息。
系统按如下顺序广播消息到接受者:系统层设备驱动,网络驱动,可安装驱动,应用程序。这意味着系统层设备驱动如果被选为接受者的一员,将总是第一个收到消息做出响应。在同一类接收者中,不保证接收顺序。这意味着,如果一个消息是特定于某个驱动的,为了避免其它驱动意外地处理了这个消息,这个消息必须有一个全局唯一消息标识。
你也可以调用SendMessage, SendMessageCallback, SendMessageTimeout, 或 SendNotifyMessage,传入HWND_BROADCAST参数,来广播消息给所有顶层窗口。
应用程序在它们的顶层窗口的“窗口过程”中接收到消息。服务可以通过“窗口过程”或其服务控制处理程序接收消息。
查询消息( Query Messages)
你可以创建自定义消息,用它来协调处理应用程序和系统中其它组件的活动。如果你创建了自己的可安装驱动或者系统层设备驱动,自定义消息会特别有用。自定义消息可以在你的驱动和使用驱动的应用程序之间传递消息。
可以用消息来轮询接收者是否允许执行给定操作。调用BroadcastSystemMessage时,将BSF_QUERY值设置到参数dwFlags,就可以生成自己的查询消息。每个查询消息的接收者都必须返回TRUE,BroadcastSystemMessage才能继续讲消息发给下一个接收者。一旦有一个接受者返回了BROADCAST_QUERY_DENY,广播就会立即停止,BroadcastSystemMessage将返回0;
翻译自MSDN(https://docs.microsoft.com/zh-cn/windows/desktop/winmsg/about-messages-and-message-queues)