消息和消息队列概述
基于windows的应用程序是基于事件驱动的。他们不会显式地调用函数获得输入,而是等待操作系统把输入传给应用程序。操作系统把所有针对应用程序的输入都会传给应用程序内的每个窗口,每个窗口都有一个叫做窗口过程的函数,在任何时候只要窗口有输入,操作系统就会调用这个窗口过程函数。窗口过程函数处理完输入就会把控制权转给操作系统。
Windows XP:假如顶层窗口停止消息响应超过几秒钟,系统就会认为窗口不会响应了。在这种情况下,系统会把这个窗口隐藏掉并且用一个具有相同Z坐标、位置、大小和可视化属性的克隆窗口替换停止响应的窗口。而且这个窗口还可以让用户移动,改变大小甚至关闭这个应用程序。然而,这些只是一种无奈的补救行为,因为应用程序实际上没有响应。在调试模式下,操作系统就不会创建克隆窗口。
一、Windows消息
系统传送输入给窗口过程是通过消息的形式。消息可以是系统产生,也可以是应用程序产生。对每一个输入事件,系统都会产生一个消息。例如,当用户敲击键盘,移动鼠标或者单击像滚动条这样的控件的时候,系统都会产生消息。当应用程序导致系统发生改变的时候,系统也会产生消息,例如当应用程序改变了系统字体资源库或者改变了自己窗口大小的时候,系统也会产生消息以便于适应这种改变。当应用程序想让他自己的窗口完成某个任务或者想和其他应用程序的窗口进行通讯的时候,应用程序也可以产生消息。
系统发送给窗口过程的消息中包含了6个参数。其中前四个分别是窗口句柄,消息标识符和两个数值,这两个数值一般被称为消息参数。窗口句柄表示这个消息的目标窗口,系统使用窗口句柄来确定哪一个窗口过程函数会接收这个消息。
消息标识符是代表这个消息的作用的一个常量名称。当窗口过程接受到一个消息的时候,他会使用消息标识符来决定如何处理这个消息。例如,如果消息标识符是WM_PAINT,意思就是告诉窗口过程函数,窗口的客户区已经发生了改变,必须重新绘制了。
消息参数指定了窗口过程函数在处理消息过程中可能用到的数据或者数据的存放位置。这连个参数的含义和具体的取值要根据消息而定。消息参数可以包含一个整数,也可以是一组位标志,或者是指向结构的指针,结构里面可能是附加的一些数据,等等。如果消息不需要使用到参数,那么这些参数数据会被设置为NULL。窗口过程函数会检查消息标识符,根据不同的消息标识符来决定符合解释消息参数。
二、消息的类型
(1)系统定义消息
当系统要和应用程序通讯的时候,他就会发出或者投递一个系统定义消息,他使用这些消息来控制应用程序的操作和为应用程序提供输入或者其他信息。应用程序也可以发动和投递系统定义消息,应用程序一般使用这些消息来控制那些通过注册窗口类而创建的控件窗口的动作。
每一个系统定义消息都有一个唯一的消息标识符和相对应的符号常量(在系统SDK的头文件中定义的),这些标识符和常量代表着消息意图。例如,WM_PAINT就是一个消息的符号常量,他代表消息意图就是让窗口重画客户区。符号常量也指定了系统定义消息属于哪种类别。常量的前缀代表着可以解释处理消息的窗口的类型,下面列出系统定义消息的前缀所代表的消息类别的清单。
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
其中普通窗口消息占信息和请求的大部分,包括鼠标和键盘输入消息,菜单和对话框输入消息,窗口创建建和管理,动态数据交换(DDE)。
(2)应用程序定义消息
应用程序可以创建消息以供他自己的窗口或者和其他进程的窗口通讯而用。如果应用程序创建了自己的消息,那么接收这个消息的窗口过程函数必须能解释这个消息并提供适当的处理过程。
应用程序定义消息的消息标识符取值定义规则应该遵循如下原则
A.系统对0x0000到0x03ff(WM_USER-1)都保留为系统定义消息,因此应用程序不能使用这些值来定义应用程序定义消息。
B.取值范围为0x0400(WM_USER)-0x7FFF,可以用来定义自定义消息的消息标识符取值。
C.假如你的应用程序是在4.0上的系统运行,那么你可以使用0x8000(WM_APP)-0xBFFF范围的值作为私有消息的标识符取值。
D.当应用程序调用RegisterWindowMessage函数来注册一个消息的时候,系统会返回一个消息标识符给你,范围在0xC000-0xFFF之间。这个函数返回的标识符可以确保在整个系统是唯一的。这个函数的使用可以防止和其他应用程序使用的自定义消息发生冲突。
三、消息传递
系统使用两个方法来传递消息给窗口过程:一种是投递到消息队列,还有一种就是投递消息到系统定义的一个内存对象临时存储,并且直接把这个消息发送给窗口过程(这个方法不会进入消息队列)。
需要投递到消息队列的消息称为队列化消息。他们主要是用户通过键盘或者鼠标输入,例如WM_MOUSEMOVE,WM_LBUTTONDOWN,WM_KEYDOWN和WM_CHAR消息。其他队列化消息还包括时钟,绘制和退出消息:WM_TIMER,WM_PAINT,WM_QUIT.
其他直接发送给窗口过程的消息称为非队列化消息。
(1)队列化消息
系统在某个时刻可以显示任意数量的窗口。为了把鼠标和键盘输入传递给对应的窗口,系统使用了消息队列的机制。
系统维护着一个系统级的消息队列同时为每个GUI线程维护着一个线程级的消息队列。为了避免为那些非GUI线程也创建消息队列导致的系统开销,所有线程在最初创建的时候是没有消息队列的。只有当线程首次调用用户用户函数或者图形设备接口函数的时候系统才会为线程创建一个消息队列。
只要用户移动鼠标,单击鼠标按钮或者敲击键盘的时候,设备驱动程序都会为鼠标或者键盘把输入转换成消息,并且把这个消息放在系统消息队列中。系统一次一个的从系统消息队列中取出消息,分析确定目标窗口,接着把这个消息投递到创建目标窗口的线程消息队列中。线程的消息队列为这个线程创建的窗口接收所有的鼠标和键盘消息。线程从他的消息队列中取出消息,让系统发送这个消息给对应的窗口过程函数进行处理。
系统总是把消息投递到队列的尾部,这样可以确保窗体遵循先入先出的顺序接收到输入消息,但是WM_PAINT,WM_TIMER,WM_QUIT这几个消息是例外的。这三个消息会被一直保留在队列中,直到队列队列中已经没有其他消息之后才会传送给窗口过程函数。另外,多个具有相同目标窗口的WM_PAINT消息会被合并成一个消息,把所有客户区域无效的部分都合并成一个区域,WM_PAINT的合并能够减少窗体重画客户区域的次数。
系统投递消息到线程消息队列的方式是先填写一个MSG结构体,然后把这个结构体COPY到消息队列中。在MSG中的信息包括:消息目标窗口的句柄,消息标识符,两个消息参数,消息投递的时间,鼠标光标的位置。线程也可以投递消息到他自己的消息队列或者其他线程的消息队列,使用PostMessage或者PostThreadMessage函数。
应用程序通过调用GetMessage函数从他的消息队列中取出一个消息,如果想分析一个消息,但是又不想从消息队列中移除,那么可以使用PeekMessage函数,这个函数会根据消息的内容重新给你填写一个MSG返回给你,队列中的消息不受影响。
当从消息队列中取出消息之后,应用程序可以使用DispatchMessage函数触发系统把这个消息发送给窗口过程函数进行处理。DispatchMessage函数需要输入一个指向MSG结构的指针(当然这个MSG结构可以是以前GetMessage或者PeekMessage获得的)。DispatchMessage会传送窗口句柄,消息标识符,两个消息参数给窗口过程函数,但是他不会传送时间和鼠标位置信息。应用程序在处理消息的时候可以通过调用GetMessageTime和GetMessagePos函数获得这两个信息。
当消息队列中没有消息的时候,线程可以使用WaitMessage函数来把控制权让给其他线程。这个函数可以把当前线程挂起,直到线程消息队列中有新的消息被放置进来,那么线程才会继续执行。
你可以调用SetMessageExtraInfo函数来把一个数值和当前线程的消息队列进行关联。然后调用GetMessageExtraInfo函数来获得这个关联值,这个关联值实际上就是通过GetMessage或者PeekMessage函数获得的最后一条消息的关联值。
(2)非队列化消息
非队列化消息会绕过系统消息队列和线程消息队列直接发动给目标窗口过程函数,一般系统发送非队列化消息是为了通知受到事件影响的窗体。例如,当用户激活了一个新的应用程序窗口,系统会发送一系列的消息,包括WM_ACTIVATE,WM_SETFOCUS,WM_SETCURSOR。这些消息通知窗口他已经被激活了,键盘输入会直接发给窗口,并且鼠标光标已经在窗口范围内移动了。非队列化消息也可能是应用程序调用某个系统函数导致的结果,例如,应用程序调用了SetWindowPos函数移动窗口之后,系统就会发出一个WM_WINDOWPOSCHANGED消息,这个消息也是非队列化的。
还有一些能发送非队列化消息的函数有:BroadcastSystemMessage, BroadcastSystemMessageEx, SendMessage, SendMessageTimeout, and SendNotifyMessage.
四、消息处理
应用程序必须对投递在他的线程队列中的消息进行取出和处理。单线程的应用程序通常在WinMain函数使用一个消息循环来取出消息,发送消息给对应的窗口过程函数来对消息进行处理。多线程的应用程序在每个创建窗口的线程中都可以包含一个消息循环。
(1)消息循环
一个简单的消息循环一般是需要调用三个函数: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);
}
}
很多人常犯的一个错误是把循环判断写成:while(GetMessage( &msg, NULL, 0, 0 )),这样写是不严谨的。因为GetMessage可能返回-1,0,非0三种状态。-1表示函数调用出错,如果这样写,当返回-1的时候也会进入消息循环,而里面无法对返回值为-1的情况作出错误处理,会导致出错。