参考:https://learn.microsoft.com/en-us/windows/win32/winmsg/about-messages-and-message-queues
概述
windows应用程序中基于窗口的程序都是事件驱动程序,在这类基于窗口的程序中,他们不会明确在代码中调用函数(例如,C运行时库中的相关函数)来获取用户输入内容。他们会等操作系统将用户输入内容传给他们。
操作系统会将属于一个应用程序的所有输入传给此应用的窗口。每个窗口都拥有一个叫window procedure的函数,当有某个窗口的输入时,系统将会调用此窗口的window procedure函数。window procedure会对用户输入内容进行处理。
我们平常使用windows时,一个应用程序打开的窗口往往会覆盖在旧应用程序打开的窗口之上。如果最上方的窗口在几秒钟之内对系统发送的消息没有相应,那么,系统就认为此窗口不会响应。此时,系统将会隐藏此窗口,并使用ghost window来替代此窗口进行相应。ghost window与被替换的窗口有相同的Z order,相同的屏幕坐标,相同的可视属性。由于这三个相同的指标,用户可以移动、缩放应用窗口,甚至关闭应用程序。同时,这也是用户对此应用所能进行的所有操作,毕竟应用已经不响应了。然而当系统处在debug模式,ghost window就不会存在。
windows messages
系统以message的形式将用户输入传递到窗口的window procedure程序。Message 可以由系统产生也可以由应用程序生成。系统在每次input event的时候都会产生一条消息。例如,当用户输入、移动鼠标、点击scroll bar时,都会产生消息。当应用程序给系统带来变化时,系统同样会产生消息来对此变化进行回应。例如,当应用程序改变了系统的字体集或者当应用程序改变了它的一个窗口的大小。应用程序可以自己生成消息来指示自己的窗口来执行某些操作或者与其它应用的窗口进行通信。
系统发送消息给window procedure时,会使用四个参数,分别为 窗口handle, 消息id和两个消息参数。window handle确定消息的目的窗口。消息id是一个有名字的常量,用于说明消息的功能。当window procedure收到消息时,会通过消息id来确定对此消息执行什么操作。例如, 消息id WM_PAINT告诉window procedure 窗口的客户区已发生改变,必须要重新绘图。当window procedure 在处理消息内容时,两个消息参数用于指示与此消息相关的数据或者数据所在的位置。当然数据的含义是取决与消息的。具体来说是取决于消息id。消息参数可以是一个整数值、packed bit flags、一个指针等等。 不使用消息参数时,消息参数通常设置为NULL.
message types
1.system-defined messages
系统与应用程序交互时,系统会send或者post system-defined message。系统生成此类型的消息来控制应用程序的操作、为应用程序提供输入、为应用程序提供用于处理的其它信息。应用程序一般也能生成此类型的消息。应用程序生成此类型的消息来控制control window的操作,这些control window由 preregistered window class生成。
每个system-defined 消息都有一个独一无二的消息id 和一个在SDK头文件中定义的符号常量。以此来显示消息的功能。 例如, WM_PAINT的功能是要求窗口刷新窗口的显示功能。符号常量用以说明system-defined消息属于什么类型。消息常量的前缀说明了能够处理此消息的窗口的类型。部分前缀参考如下
其它前缀参考
https://learn.microsoft.com/en-us/windows/win32/winmsg/about-messages-and-message-queues
普通的window message 涵盖了广泛的信息内容和request,包含鼠标键盘输入、菜单和对话框输入、窗口创建request、窗口管理、Dynamic Data Exchange(DDE)。
2.application-defined messages
应用能生成用于自身窗口的消息也能生成与其它进程中的窗口进行交互的消息。
消息id的规定如下:
- 系统预留0x0000——0x03FF为 system-defined 消息。应用不能使用这些值来表示其私有messge.
- 0x400——0x7FFF可以用作窗口私有消息id.
- If your application is marked version 4.0, you can use message-identifier values in the range 0x8000 (WM_APP) through 0xBFFF for private messages.
- 当应用调用函数RegisterWindowMessage来注册消息时,系统会返回一个值在0xC000——0xFFFF的范围内的message id。此函数的返回值在整个系统内是独一无二的。此函数会避免多个应用程序使用同一个消息id来处理不同的问题.
message routing
系统使用两个方法将消息传送到window procedure。一个是post消息到一个叫消息队列的FIFO队列。此队列是一个system-defined内存对象,用于临时存储消息。另一个方式是直接将消息send 到window procedure.
被post到消息队列的消息叫做queued message。这些消息主要来源于用户通过键盘、鼠标进行输入的事件。具体包括WM_MOUSEMOVE、 WM_LBUTTONDOWN、WM_KEYDOWN和WM_CHAR。其它的还有WM_TIMER、 WM_PAINT和WM_QUIT。其它大部分消息都是直接发送到window procedure,这些消息被称为 nonqueued message.
queued messages
系统可以同时显示任意数量的窗口。 为了将鼠标和键盘的输入内容传递到正确的窗口,系统使用的是消息队列。
系统维护了一个系统消息队列,还为每个GUI thread都维护了一个特用于线程的消息队列。为了避免为non-GUI thread 创建消息队列的开销,所有线程在创建时都是没有消息队列的。当线程第一次调用一些特定的用户态函数时,系统才会为此线程创建线程相关的消息队列。调用GUI 函数时,不会创建消息队列。
当用户移动鼠标、点击鼠标按钮或者敲击键盘时,鼠标或者键盘的设备驱动会将input 转换为消息,并将消息放入系统消息队列。系统每次从系统消息队列中将一个消息移出,分析确认该消息的目的窗口,然后将消息放入创建消息目的窗口的线程的消息队列。所有由一个线程创建的窗口的键盘、鼠标消息都由此线程的消息队列接收。线程将消息从线程消息队列取出并引导系统将消息send到正确的window procedure。注意:此时用的是send,此方式与post是不同的。除了WM_PAINT、WM_TIMER和WM_QUIT外,系统总是将消息post在消息队列的尾部。这样保证了窗口都是以FIFO的顺序收到input mssage。WM_PAINT、WM_TIMER和WM_QIUT会留在队列里,并且只有在队列中不存在其它消息时,这三个消息才会到达window procedure.另外, 对于相同窗口的多个WM_PAINT消息会被合并为一个,此举减少了窗口绘制其客户区的次数。
系统post 一条消息到消息队列的流程是,先填充一个叫MSG的结构体,然后将此结构体复制到消息队列。MSG结构体中包含以下内容:消息所属窗口的handle,消息Id,两个消息参数,post消息的时间,鼠标光标位置。线程可以将消息post到自己的消息队列,也可以通过函数PostMessage和PostThreadMessage将消息post到其他线程的消息队列。
应用通过GetMessage将消息从消息队列中移出,通过PeekMessage测试消息队列中是否有某个消息,而不将消息从消息队列中移出。在将消息从消息队列中移出后,应用程序调用DispatchMessage来指示系统将消息发送到哪个具体的window procedure进行处理。DispatchMessage会使用经GetMessage和PeekMeesage填充过的MSG结构。DispatchMessge会将MSG结构中的窗口handle,消息id,两个消息参数传递给window procedure,而不传递post消息的时间和鼠标光标位置。应用在处理消息时,可以通过GetMessageTime和GetMessagePos来获取时间和鼠标光标位置这两个MSG结构体成员。
当线程的消息队列中无消息时,应用可调用WaitMessage来将可控制权转让给其他线程。此函数会导致线程挂起,直到线程的消息队列中有消息才会被唤醒。
应用可以调用SetMessageExtraInfo函数来关联一个数据到消息队列。通过GetMessageExtraInfo来获取此关联的数据。
nonqueued message
nonqueued message 被立即送往目的window procedure,而绕过系统消息队列和线程消息队列。系统通常会发送nonqueued message 来通知对窗口的有影响的事件。例如,当用户激活一个新的引用窗口,此时,系统会给窗口发送一系列的消息。如WM_ACTIVE、WM_SETFOCUS、WM_SETCURSOR,这些消息告知窗口,它被激活,键盘输入的内容会发送给它,鼠标光标已经进入窗口的边界内。当应用程序调用某些函数时,也会触发nonqueued message 的生成。例如,当应用程序调用SetWindowPos时,系统会send WM_WINDOWPOSCHANGED.其它能send nonqueued messsage的函数还有BroadcastSystemMessage,BraodcastSystemMessageEx,SendMessage,SendMessageTimeout,SendNotifyMessage。
Message Handling
应用程序必须将其线程消息队列中的消息移出并进行处理。单线程应用程序通常在WinMain函数中使用message loop将消息从消息队列中移出并发送到响应的window procedure进行处理。多线程应用程序可以在每个创建窗口的线程中都使用一个 message loop。
message loop
一个简单的message loop 通常包含对GetMessage、TranslateMessage、DispatchMessage这三个函数的调用。
MSG msg;
BOOL bRet;
while( (bRet = GetMessage( &msg, NULL, 0, 0 )) != 0)
{
if (bRet == -1)
{
// handle the error and possibly exit
}
else
{
TranslateMessage(&msg);
DispatchMessage(&msg);
}
}
GetMessage从消息队列中获取消息,并将消息复制到MSG类型的数据中,此函数的正常返回值是一个非零数据,但当获取到WM_QUIT消息时,返回值为0,且此时会退出循环。在单线程应用程序中,结束message loop 是关闭应用程序的第一步。应用 程序可以调用PostQuitMessage函数来结束自己的message loop,此举通常发生在应用程序主窗口的window procedure 在收到WM_DESTROY时,才会执行的操作。
如果你在GetMessage的第二个参数上指定了一个窗口的handle,那么,此函数只会获取消息队列中发给此窗口的消息。GetMessage 还能对消息队列中的消息进行过滤,它可以仅获取特定范围内的消息。
如果线程将会受到来自键盘的字符输入,那么此线程的message loop必须调用TranslateMessage。每次用户按键时,系统都会生成虚拟键消息(WM_KEYDOWM/WM_KEYUP). 虚拟键消息包含用于识别键盘上哪个键按下的虚拟键码,这个虚拟键码不是按键的字符值。为了获取按键的字符值,message loop 必须调用TranslateMessage 来进行虚拟键码消息到字符消息(WM_CHAR)的转换,之后,将转换后的消息放回应用消息队列。在之后的message loop中会获取此消息并将消息发送到window procedure。
DispatchMessage会将消息发送到MSG结构中所指示的窗口的window procedure。如果MSG中的消息handle 值为HWMD_TOPMOST,那么,函数会将消息传给系统中所有处在top level的窗口的window procedure。如果handle 为NULL, 那么函数啥事都不做。
应用程序的主线程在应用初始化完成及至少创建一个窗口后,就开始它的 message loop。之后,此message loop 持续从线程的消息队列中获取message,并将消息发送到各个相应的window procedure。message loop 结束循环的条件是将WM_QUIT 从消息队列移除。
即使应用包含很多窗口,对于一个消息队列只需要一个message loop。
window procedure
window procedure 用于获取并处理窗口的所有消息。每个窗口类都有一个 window procedure,使用相同窗口类创建的窗口都有相同的 window procedure。
系统将消息发送到window procedure的方式是将消息当做 window procedure的参数。通常 window procedure不会忽略消息,而是将消息发送到系统进行default处理。此功能是DefWindowProc函数实现的。此时DefWindowProc的返回值就是window proceure 对此消息的处理结果。大部分window procedure只处理少数消息,大部分消息都由DefWindowProc进行处理。
由于window procedure 是共享的,那么区分消息影响的window是通过MSG中的窗口handle 来实现的。
Message Filtering
应用可以选择获取消息队列 中的特定消息,而忽略其他消息。为达到此目的,需要通过GetMessage或PeekMessage函数设置message filter 来实现。filter可以是一组消息id(有第一个和最后一个id来确定范围),也可以试一个 window handle, 还可以是两者的组合。