Windows的应用程序一般包含窗口(Window),它主要为用户提供一种可视化的交互方式,窗口是由线程(Thread)创建的。Windows系统通过消息机制来管理交互,消息(Message)被发送,保存,处理,一个线程会维护自己的一套消息队列(Message Queue),以保持线程间的独占性。队列的特点无非是先进先出,这种机制可以实现一种异步的需求响应过程。
消息由一个叫MSG的结构体定义,包括窗口句柄(HWND),消息ID(UINT),参数(WPARAM, LPARAM)等等:
消息是如何分类的?其前缀都代表什么含义? 消息ID只是一个整数,Windows系统预定义了很多消息ID,以不同的前缀来划分,比如WM_*,CB_*等等。 Prefix Message category
如何通过消息传递任何参数? Windows系统的消息机制都包含2个长整型的参数:WPARAM, LPARAM,可以存放指针,也就是说可以指向任何内容了。 消息在线程内传递时,由于在同一个地址空间中,指针的值是有效的。但是夸线程的情况就不能直接使用指针了,所以Windows系统提供了 WM_SETTEXT, WM_GETTEXT, WM_COPYDATA等消息,用来特殊处理,指针的内容会被放到一个临时的内存映射文件(Memory-mapped File)里面,通过它实现线程间的共享数据。
Windows系统本身会维护一个唯一的消息队列,以便于发送给各个线程,这是系统内部的实现方式。 Sent Message Queue 之所以维护多个队列,是因为不同消息的处理方式和处理顺序是不同的。 线程和窗口是一一对应的吗?如果想要有两个不同的窗口对消息作出不同反应,但是他们属于同一个线程,可能吗? 窗口由线程创建,一个线程可以创建多个窗口。窗口可由CreateWindow()函数创建,但前提是需要提供一个已注册的窗口类(Window Class),每一个窗口类在注册时需要指定一个窗口处理函数(Window Procedure),这个函数是一个回调函数,就是用来处理消息的。而由一个线程来创建对应于不同的窗口类的窗口是可以的。
消息的发送终归通过函数调用,比较常用的有PostMessage(),SendMessage(),另外还有一些Post*或Send*的函数。函数的调用者即发送消息的人。 他们的的原型如下: LRESULT SendMessage( 这种机制可能引起死锁,所以有其他函数比如SendMessageTimeout(), SendMessageCallback()等函数来避免这种情况。 PostMessage()并不需要同步,所以比较简单,它只是负责把消息发送到队列里面,然后马上返回发送者,之后消息的处理则再受控制。 消息可以不进队列吗?什么消息不进队列? 可以。实际上MSDN把消息分为队列型(Queued Message)和非队列型(Non-queued Message),这只是不同的路由方式,但最终都会由消息处理函数来处理。 其实,按照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应用程序启动后,会显示一个窗口,它在等待用户的操作,并作出反应。 一个典型的消息循环如下所示(注意这里没有处理GetMessage出错的情况): while(GetMessage(&msg, NULL, 0, 0 ) != FALSE) 下面在看看GetMessage()的细节: BOOL GetMessage( 其他几个参数是用来过滤消息的,可以指定接收消息的窗口,以及确定消息的类型范围。 这里还需要提到一个概念是线程的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消息都得到了处理,这是因为要保证程序运行的正确性,或者数据丢失?不得而知。 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时间了。 还有一个PeekMessage(),其原型为: BOOL PeekMessage( 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)也是窗口,他们一般属于某个父窗口。
由消息处理函数(Window Procedure)来处理。消息处理函数是一个回调函数,其地址在注册窗口类的时候注册,只有在线程内才能调用。 其原型为: typedef LRESULT (CALLBACK* WNDPROC)(HWND hWnd, UINT uMsg, WPARAM wParam, LPARAM lParam); 处理函数内部一般是一个switch-case结构,来针对不同的消息类型进行处理。Windows系统还为所有窗口预定义了一个默认的处理函数 DefWindowProc(),它提供了最基本的消息处理,一般在不需要特殊处理的时候(即在switch的default分支)会调用这个函数。 处理函数里可以发送消息,但是可以想象有可能出现循环。另外处理函数还常常被递归调用,所以要减少局部变量的使用,以避免递归过深是栈溢出。 最后关于处理函数特化的问题将在另外的文章讨论。 |
|