Windows的应用程序一般包含窗口(Window),它主要为用户提供一种可视化的交互方式,窗口是由线程(Thread)创建的。Windows系统通过消息机制来管理交互,消息(Message)被发送,保存,处理,一个线程会维护自己的一套消息队列(Message Queue),以保持线程间的独占性。队列的特点无非是先进先出,这种机制可以实现一种异步的需求响应过程。
消息的是什么样子的?
消息由一个叫MSG的结构体定义,包括窗口句柄(HWND),消息ID(UINT),参数(WPARAM, LPARAM)等等:
- struct MSG
- {
- HWND hwnd;
- UINT message;
- WPARAM wParam;
- LPARAM lParam;
- DWORD time;
- POINT pt;
- };
消息ID是消息的类型标识符,由系统或应用程序定义,消息ID为消息划分了类型。同时,也可以看出消息是对应于特定的窗口(窗口句柄)的。
消息是如何分类的?其前缀都代表什么含义?
消息ID只是一个整数,Windows系统预定义了很多消息ID,以不同的前缀来划分,比如WM_*,CB_*等等。
具体见下表:
Prefix | Message category |
---|---|
ABM | Application desktop toolbar |
BM | Button control |
CB | Combo box control |
CBEM | Extended combo box control |
CDM | Common dialog box |
DBT | Device |
DL | Drag list box |
DM | Default push button control |
DTM | Date and time picker control |
EM | Edit control |
HDM | Header control |
HKM | Hot key control |
IPM | IP address control |
LB | List box control |
LVM | List view control |
MCM | Month calendar control |
PBM | Progress bar |
PGM | Pager control |
PSM | Property sheet |
RB | Rebar control |
SB | Status bar window |
SBM | Scroll bar control |
STM | Static control |
TB | Toolbar |
TBM | Trackbar |
TCM | Tab control |
TTM | Tooltip control |
TVM | Tree-view control |
UDM | Up-down control |
WM | General window |
应用程序可以定义自己的消息,其取值范围必须大于WM_USER。
如何通过消息传递任何参数?
Windows系统的消息机制都包含2个长整型的参数:WPARAM, LPARAM,可以存放指针,也就是说可以指向任何内容了。
传递的内容因消息各异,消息处理函数会根据消息的类型进行特别的处理,它知道传递的参数是什么含义。
消息在线程内传递时,由于在同一个地址空间中,指针的值是有效的。但是夸线程的情况就不能直接使用指针了,所以Windows系统提供了 WM_SETTEXT, WM_GETTEXT, WM_COPYDATA等消息,用来特殊处理,指针的内容会被放到一个临时的内存映射文件(Memory-mapped File)里面,通过它实现线程间的共享数据。
消息队列和线程的关系是什么?消息队列的结构是什么样子的?
Windows系统本身会维护一个唯一的消息队列,以便于发送给各个线程,这是系统内部的实现方式。
而对于线程来说,每个线程可以拥有自己的消息队列,它和线程一一对应。在线程刚创建时,消息队列并不会被创建,而是当GDI的函数调用发生时,Windows系统才认为有必要为线程创建消息队列。
消息队列包含在一个叫THREADINFO的结构中,有四个队列:
Sent Message Queue
Posted Message Queue
Visualized Input Queue
Reply Message Queue
之所以维护多个队列,是因为不同消息的处理方式和处理顺序是不同的。
线程和窗口是一一对应的吗?如果想要有两个不同的窗口对消息作出不同反应,但是他们属于同一个线程,可能吗?
窗口由线程创建,一个线程可以创建多个窗口。窗口可由CreateWindow()函数创建,但前提是需要提供一个已注册的窗口类(Window Class),每一个窗口类在注册时需要指定一个窗口处理函数(Window Procedure),这个函数是一个回调函数,就是用来处理消息的。而由一个线程来创建对应于不同的窗口类的窗口是可以的。
由此可见,只要注册多个窗口类,每个窗口都可以拥有自己的消息处理函数,而同时,他们属于同一个线程。
如何发送消息?
消息的发送终归通过函数调用,比较常用的有PostMessage(),SendMessage(),另外还有一些Post*或Send*的函数。函数的调用者即发送消息的人。
这二者有什么不同呢?SendMessage()要求接收者立即处理消息,等处理完毕后才返回。而PostMessage()将消息发送到接收者队列中以后,立即返回,调用者不知道消息的处理情况。
他们的的原型如下:
- LRESULT SendMessage(
- HWND hwnd,
- UINT uMsg,
- WPARAM wParam,
- LPARAM lParam);
- LRESULT PostMessage(
- HWND hwnd,
- UINT uMsg,
- WPARAM wParam,
- LPARAM lParam);
但这仅限于线程内的情况,夸线程时它调不到处理函数,只能把消息发送到接收线程的队列Sent Message Queue里。如果接收线程正在处理别的消息,那么它不会被打断,直到它主动去获取队列里的下一条消息时,它会拿到这一条消息,并开始处理,完成后他会通知发送线程结果(猜测是通过ReplyMessage()函数)。
在接收线程处理的过程中,发送线程会挂起等待SendMessage()返回。但是如果这时有其他线程发消息给这个发送线程,它可以响应,但仅限于非队列型(Non-queued)消息。
这种机制可能引起死锁,所以有其他函数比如SendMessageTimeout(), SendMessageCallback()等函数来避免这种情况。
PostMessage()并不需要同步,所以比较简单,它只是负责把消息发送到队列里面,然后马上返回发送者,之后消息的处理则再受控制。
消息可以不进队列吗?什么消息不进队列?
可以。实际上MSDN把消息分为队列型(Queued Message)和非队列型(Non-queued Message),这只是不同的路由方式,但最终都会由消息处理函数来处理。
队列型消息包括硬件的输入(WM_KEY*等)、WM_TIMER消息、WM_PAINT消息等;非队列型的一些例子有WM_SETFOCUS, WM_ACTIVE, WM_SETCURSOR等,它们被直接发送给处理函数。
其实,按照MSDN的说法和消息的路由过程可以理解为,Posted Message Queue里的消息是真正的队列型消息,而通过SendMessage()发送到消息,即使它进入了Sent Message Queue,由于SendMessage要求的同步处理,这些消息也应该算非队列型消息。也许,Windows系统会特殊处理,使消息强行绕过队列。
谁来发送消息?硬件输入是如何被响应的?
消息可以由Windows系统发送,也可以由应用程序本身;可以向线程内发送,也可以夸线程。主要是看发送函数的调用者。
对于硬件消息,Windows系统启动时会运行一个叫Raw Input Thread的线程,简称RIT。这个线程负责处理System Hardware Input Queue(SHIQ)里面的消息,这些消息由硬件驱动发送。RIT负责把SHIQ里的消息分发到线程的消息队列里面,那RIT是如何知道该发给谁呢?如果是鼠标事件,那就看鼠标指针所指的窗口属于哪个线程,如果是键盘那就看哪个窗口当前是激活的。一些特殊的按键会有所不同,比如 Alt+Tab,Ctrl+Alt+Del等,RIT能保证他们不受当前线程的影响而死锁。RIT只能同时和一个线程关联起来。
有可能,Windows系统还维护了除SHIQ外地其他队列,分发给线程的队列,或者直接发给窗口的处理函数。
消息循环是什么样子?线程何时挂起?何时醒来?
想象一个通常的Windows应用程序启动后,会显示一个窗口,它在等待用户的操作,并作出反应。
它其实是在一个不断等待消息的循环中,这个循环会不断去获取消息并作出处理,当没有消息的时候线程会挂起进入等待状态。这就是通常所说的消息循环。
一个典型的消息循环如下所示(注意这里没有处理GetMessage出错的情况):
- while(GetMessage(&msg, NULL, 0, 0 ) != FALSE)
- {
- TranslateMessage(&msg);
- DispatchMessage(&msg);
- }
DispatchMessage()将调用消息处理函数。这里有一个灵活性,消息从队列拿出之后,也可以不分发,进行一些别的特殊操作。
下面在看看GetMessage()的细节:
- BOOL GetMessage(
- LPMSG lpMsg,
- HWND hWnd,
- UINT wMsgFilterMin,
- UINT wMsgFilterMax
- );
其他几个参数是用来过滤消息的,可以指定接收消息的窗口,以及确定消息的类型范围。
这里还需要提到一个概念是线程的Wake Flag,这是一个整型值,保存在THREADINFO里面和4个消息队列平级的位置。它的每一位(bit)代表一个开关,比如QS_QUIT, QS_SENDMESSAGE等等,这些开关根据不同的情况会被打开或关闭。GetMessage()在处理的时候会依赖这些开关。
GetMessage()的处理流程如下:
1. 处理Sent Message Queue里的消息,这些消息主要是由其他线程的SendMessage()发送,因为他们不能直接调用本线程的处理函数,而本线程调用 SendMessage()时会直接调用处理函数。一旦调用GetMessage(),所有的Sent Message都会被处理掉,并且GetMessage()不会返回;
2. 处理Posted Message Queue里的消息,这里拿到一个消息后,GetMessage()将它拷贝到MSG结构中并返回TRUE。注意有三个消息WM_QUIT, WM_PAINT, WM_TIMER会被特殊处理,他们总是放在队列的最后面,直到没有其他消息的时候才被处理,连续的WM_PAINT消息甚至会被合并成一个以提高效率。从后面讨论的这三个消息的发送方式可以看出,使用Send或Post消息到队列里情况不多。
3. 处理QS_QUIT开关,这个开关由PostQuitMessage()函数设置,表示线程需要结束。这里为什么不用Send或Post一个 WM_QUIT消息呢?据称:一个原因是处理内存紧缺的特殊情况,在这种情况下Send和Post很可能失败;其次是可以保证线程结束之前,所有Sent 和Posted消息都得到了处理,这是因为要保证程序运行的正确性,或者数据丢失?不得而知。
如果QS_QUIT打开,GetMessage()会填充一个WM_QUIT消息并返回FALSE。
4. 处理Virtualized Input Queue里的消息,主要包括硬件输入和系统内部消息,并返回TRUE;
5. 再次处理Sent Message Queue,来自MSDN却没有解释。难道在检查2、3、4步骤的时候可能出现新的Sent Message?或者是要保证推后处理后面两个消息;
6. 处理QS_PAINT开关,这个开关只和线程拥有的窗口的有效性(Validated)有关,不受WM_PAINT的影响,当窗口无效需要重画的时候这个开关就会打开。当QS_PAINT打开的时候,GetMessage()会返回一个WM_PAINT消息。处理QS_PAINT放在后面,因为重绘一般比较慢,这样有助于提高效率;
7. 处理QS_TIMER开关,和QS_PAINT类似,返回WM_TIMER消息,之所以它放在QS_PAINT之后是因为其优先级更低,如果Timer消息要求重绘但优先级又比Paint高,那么Paint就没有机会运行了。
如果GetMessage()中任何消息可处理,GetMessage()不会返回,而是将线程挂起,也就不会占用CPU时间了。
类似的WaitMessage()函数也是这个作用。
还有一个PeekMessage(),其原型为:
- BOOL PeekMessage(
- LPMSG lpMsg,
- HWND hWnd,
- UINT wMsgFilterMin,
- UINT wMsgFilterMax,
- UINT wRemoveMsg
- );
WM_DESTROY, WM_QUIT, WM_CLOSE消息有什么不同?
而其他两个消息是关于窗口的,WM_CLOSE会首先发送,一般情况程序接到该消息后可以有机会询问用户是否确认关闭窗口,如果用户确认后才调用 DestroyWindow()销毁窗口,此时会发送WM_DESTROY消息,这时窗口已经不显示了,在处理WM_DESTROY消息是可以发送 PostQuitMessage()来设置QS_QUIT开关,WM_QUIT消息会由GetMessage()函数返回,不过此时线程的消息循环可能也即将结束。
窗口内的消息的路由是怎样的?窗口和其控件的关系是什么?
一个窗口(Window)可以有一个Parent属性,对一个Parent窗口来说,属于它的窗口被称为子窗口(Child Window)。控件(Control)或对话框(Dialog)也是窗口,他们一般属于某个父窗口。
所有的窗口都有自己的句柄(HWND),消息被发送时,这个句柄就已经被指定了。所以当子窗口收到一个消息时,其父窗口不会也收到这个消息,除非子窗口手动的转发。
关于更详细的窗口和控件,会在另一篇中讨论。
谁来处理消息?消息处理函数能发送消息么?
由消息处理函数(Window Procedure)来处理。消息处理函数是一个回调函数,其地址在注册窗口类的时候注册,只有在线程内才能调用。
其原型为:
- typedef LRESULT (CALLBACK* WNDPROC)(HWND hWnd, UINT uMsg, WPARAM wParam, LPARAM lParam);
处理函数内部一般是一个switch-case结构,来针对不同的消息类型进行处理。Windows系统还为所有窗口预定义了一个默认的处理函数 DefWindowProc(),它提供了最基本的消息处理,一般在不需要特殊处理的时候(即在switch的default分支)会调用这个函数。
由同一个窗口类创建的一组窗口共享一个消息处理函数,所以在编写处理函数的时候要小心处理窗口实例的局部变量。
处理函数里可以发送消息,但是可以想象有可能出现循环。另外处理函数还常常被递归调用,所以要减少局部变量的使用,以避免递归过深是栈溢出。
最后关于处理函数特化的问题将在另外的文章讨论。
--------------------------------------------------
参考资料:
Windows 游戏编程大师技巧 (第一卷)[André LaMothe]
Windows 核心编程 [Jeffery Richter]
Win32 and COM Development: User Interface: Windows User Interface: Windowing [MSDN]