Window消息循环补遗

来源:互联网 发布:sql树结构 所以父节点 编辑:程序博客网 时间:2024/05/22 14:46

一、消息传递

        系统使用两个方法来传递消息给窗口过程:一种是投递到消息队列,还有一种就是投递消息到系统定义的一个内存对象临时存储,并且直接把这个消息发送给窗口过程(这个方法不会进入消息队列)。

        需要投递到消息队列的消息称为队列化消息。他们主要是用户通过键盘或者鼠标输入,例如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,endMessageTimeout,endNotifyMessage。
GetMessage函数从消息队列中取出消息并且填充到一个MSG结构中。这个函数除非遇到WM_QUIT消息,就会返回0(FALSE),如果不是就返回一个非0的值。在一个单线程的应用程序中,要关闭应用程序的第一步一般都是终止消息循环。应用程序也可以通过使用PostQuitMessage函数来终止自己的消息循环,一般在应用程序的主窗口的窗口过程函数中对WM_DESTROY消息的处理就是采用这种方式。

        如果你在使用GetMessage函数的时候,指定了第二个参数的窗口句柄,那么就只会取出以指定窗口句柄为目标的消息,所以GetMessage也可以过滤消息队列中的消息的,可以只是取出那些指定范围内的消息。

        如果线程要从键盘上接收字符输入的话,线程消息循环中就必须要包括TranslateMessage函数。当用户在键盘上按一个键的时候,系统会产生虚拟键消息(WM_KEYDOWN和WM_KEYUP)。虚拟键消息包含了你按下的键的虚拟标识码,但是虚拟标识码不是表示键的ASCII码,要获得ASCII码,消息循环必须采用TranslateMessage函数把虚拟键消息转换成字符消息(WM_CHAR)并且把这个字符消息重新放回到应用程序的消息队列。然后这个字符消息会按照输入的顺序被取出来,并且发给窗口过程函数进行处理。

        DispatchMessage函数会将消息发送给MSG结构中指定的窗口句柄所表示的窗口过程函数。如果窗口句柄的值是HWND_TOPMOST,那么这个函数就会把该消息发送给系统中的所有顶层窗口的窗口过程函数。假如窗口句柄的值是NULL,那么该函数对消息就不做任何处理。

        应用程序的主线程在初始化和创建至少一个窗口之后开始启动他的消息循环。一旦开始,消息循环就会一直持续从消息队列中取出消息并且把它们发送到对应的窗口。当GetMessage函数取出WM_QUIT消息之后,消息循环就会终止了。

        即使应用程序包含着多个窗口,也只需要一个消息循环。DispatchMessage总是会把消息发送给正确的窗口,因为在队列中的每个消息都是一个MSG结构,而这个结构里面包含了这个消息所属的窗口的句柄。

        你可以通过各种方式去修改消息循环。例如,你可以从消息队列中取出消息,但是不把消息发送给窗口。这在某些应用程序投递不指定窗口的消息的时候很有用。你也可以采用GetMessage只取出指定的消息,让其他消息继续保留在队列中,这对于你有时候必须临时要绕过先入先出顺序的时候比较有用。

        如果应用程序要使用加速键的话,必须要能够把键盘消息转换成命令消息。要实现这个,应用程序的消息循环必须包含一个TranslateAccelerator函数的调用。

假如线程使用了非模态的对话框的话,消息循环必须包含IsDialogMessage函数调用使得对话框能够接收键盘输入。 


二、投递和发送消息

        任何应用程序都可以投递和发送消息。和系统的机制一样,投递消息是把消息结构COPY到消息队列中,发送消息是给窗口过程传递一个消息参数的方式进行。为了投递消息,应用程序使用PostMessage函数。发送消息,一般是用SendMessage,BroadcastSystemMessage, SendMessageCallback,SendMessageTimeout,SendNotifyMessage和SendDlgItemMessage 函数。

1.投递消息

        应用程序一般是投递一个消息来通知指定窗口完成某个任务。PostMessage创建一个MSG结构,并COPY到消息队列中。应用程序的消息循环最终都会取出消息并且发送给对应的窗口过程。

        如果PostMessage中消息没有指定窗口句柄(为NULL),那么消息会被投递到当前线程的消息队列中。因为没有窗口句柄,所以应用程序必须处理这个消息,这是创建一个供整个应用程序使用的消息的一个方法。

        如果PostMessage中窗口句柄为HWND_TOPMOST,那么就表示将消息投递给所有的顶层窗口。

        一般程序常见的一个错误是总认为PostMessage函数投递一定会成功,其实当消息队列队列满之后就不成立了。所以应用程序应该检查PostMessage函数的返回值来确定消息是否投递成功,如果不成功,就应该重新投递。


2.发送消息

        应用程序一般发送一个消息来通知窗口过程立刻完成某个任务。通过SendMessage函数发送消息对应的窗口过程。这个函数会等到窗口过程物理完毕之后才返回消息结果。父窗口和子窗口经常使用这个发送消息的方式互相通讯。例如,一个父亲窗体里面有一个编辑控件作为他的子窗体,父窗体可以通过发动一个消息给编辑控件方式来设置控件的文本。而控件也可以通过给父窗体发回一个消息的方式来通知父窗体用户改变了控件文本。

        SendMessageCallback函数也会发送一个消息给指定窗口,但是这个函数是立刻返回的。在窗口过程处理完毕消息之后,系统会调用指定的回调函数。

        有时候,你可能会想发送一个消息给所有系统里面的顶层窗口。例如,如果应用程序改变了系统时间,他必须通知所有顶层窗口时间发生了改变,方式就是发一个WM_TIMECHANGE消息。应用程序也可以通过调用SendMessage发送一个消息给所有顶层窗口(指定窗口句柄为HWND_TOPMOST)。还可以调用BroadcastSystemMessage函数广播这个消息给所有的应用程序(在pdwRecipients参数中设定为BSM_APPLICATIONS)。

        通过使用 InSendMessage 和 InSendMessageEx 函数,窗口过程可以判断是否正在处理其他线程发来的消息。这个能力当需要根据消息来源而决定如何处理的时候非常有用。


三、消息死锁

        如果一个线程通过调用SendMessage函数给其他线程发送消息,那么这个线程要等到窗口处理函数处理完之后,SendMessage函数才会返回,这个线程才能继续执行。假如接收线程在处理消息过程中让出来控制权,那么发送方线程就不能继续执行,因为他需要等待SendMessage函数返回。加入接收线程被挂接到了和发送线程相同的消息队列上,这就有可能导致应用程序死锁。

        注意接收线程不一定只有显式的让出控制权,调用下列任意一个函数都会隐式的导致控制权的让出。DialogBox,DialogBoxIndirect,DialogBoxIndirectParam,DialogBoxParam,GetMessage,MessageBox,PeekMessage,SendMessage。

        为了避免应用程序永久死锁,应该考虑使用SendNotifyMessage 或者 SendMessageTimeout 函数。否则的话,窗口过程应该通过调用InSendMessage或者 InSendMessageEx 函数来检查是否有其他线程发过来的消息,如果有的话,应该在调用上面那些可能让出控制权的函数之前先处理消息。如果函数返回真,那么窗口过程必须在让出控制权之前调用ReplyMessage函数,以便于让对方能够继续执行,从而避免死锁。 


四、广播消息

        消息广播简化了系统中需要向多个接收者发送消息的情况。应用程序要广播消息,可以使用BroadcastSystemMessage 函数,定义好消息的接收者就是了。这个时候只需要定义一个或者多个接收者类型就行了。这些类型指的是应用程序,安装型驱动程序,网络驱动程序和系统级的设备驱动程序,系统会发送广播消息给每个指定类型的所有成员。

        系统发送广播消息一般是在系统级设备驱动程序或者相关组建内发生变化的时候而做出的响应。驱动程序或者相关组件也可以广播消息给应用程序和其他组件来通知他们发生了改变。例如,当用户在软驱里面插入磁盘的时候,软驱的驱动程序就会广播一个消息,相关组件就会对这个消息作出反应。

        系统广播消息的接收方接受的顺序是:系统级设备驱动程序,网络驱动程序,安装性驱动程序和应用程序。也就是说,如果系统级设备驱动作为接收方,他总是最先有机会接收到消息并作出响应。

        在给定的接收类型中,没有哪个驱动程序能保证一定会在别的驱动程序之前接受消息。也就是说,发给指定驱动程序的消息必须要有一个全局唯一消息标识符,从而避免其他驱动程序无意中处理他。

        你可以通过对SendMessage, SendMessageCallback, SendMessageTimeout,SendNotifyMessage函数中的窗口句柄设置为HWND_BROADCAST来吧消息广播给所有的顶层窗口。

        应用程序会通过他们的顶层窗口的窗口过程函数来接收消息。消息不会发给子窗口。服务可以通过窗口过程或者服务控制处理程序来接收消息。

PS:系统级的设备驱动程序使用相关的系统级的函数来广播消息。


五、查询消息

        你可以定义自己的自定义消息用来在你的应用程序和系统组件之间进行协作活动。加入你创建了你自己的安装性驱动程序或者系统级设备驱动程序的时候特别有用。你的自定义消息可以运载信息在你的驱动程序和你的应用程序之间进行交互。

        要想查询接收方执行给定动作的权限,可以使用查询消息。你可以在调用BroadcastSystemMessage函数的时候设置dwFlags参数为BSF_QUERY来产生你自己的查询消息。没一个查询消息接收方都必须为这个函数返回True并且把消息发送给下一个接收者。如果任何一个接收方返回BROADCAST_QUERY_DENY,这个广播过程就会立刻终止并且函数返回一个0值。

PS:你可以创建安装型驱动程序来广播和处理消息。一个安装型的驱动程序是一个动态连接库,他导出了 DriverProc 函数。这个驱动程序通过 DriverProc 函数来接收消息和通过使用BroadcastSystemMessage来广播消息。安装性驱动一般是用来支持多媒体设备,例如声卡,但是也可以用在其他设备和其他目的上。 


原创粉丝点击