这篇文章解释了GetMessage和PeekMessage的内部运作方式,同时也是一类与“消息及消息在16位 MS-DOS?/Microsoft? Windows?环境之下的影响”相关文章的基础。我们将讨论下面这些主题:
·系统和应用程序队列(译者注:以下简称为“程序队列”)
·GetMessage和PeekMessage函数
·消息过滤
·WM_QUIT消息
·让步和休眠
·让步的问题
·WaitMessage
16位MS-DOS/Windows环境和32位Win32?/Windows NT?环境有些很重要的不同之处。虽然这些不同之处在这儿无法被忽视,但我们还是把它们做为遗留问题,由以后的文章去解释吧。
队列
要理解GetMessage和PeekMessage的运作,必须首先明白Microsoft? Windows?操作系统是如何储存事件和消息的。在Windows中有两种类型的队列为此目的工作,它们分别是系统队列和消息队列。
硬件输入:系统队列
Windows有一些驱动程序,它们负责响应来自于键盘和鼠标等硬件的中断服务。在中断时间中,键盘和鼠标驱动程序会调用USER.EXE中指定的一些入口点去报告一个事件的发生。在Windows中服务于光笔计算的光笔驱动程序,同样会在原始的光笔事件中调用这些入口点。
在Windows3.1中,系统队列是一个有着120个入口空间的定长的队列。在一般情形下这些“小房间”是足够了,但如果应用程序挂起了或者在一段长的时间里没有及时处理任何消息就可能导致系统队列被填满。如果真的发生了,任何尝试添加到系统队列的新事件都将会引起系统蜂鸣。(译者注:在DOS中,如果一个程序在一段时间内占用了所有的系统资源,使机器无法响应,这时如果你按住一个键不放,你就会听到机箱喇叭嘀嘀作响)
发送的消息和程序队列
当一个应用程序开始时,一个队列将会因此而被创建。程序队列(有时会称为任务队列)常常用于储存“正在被发往应用程序的一个窗口” 的消息。唯一常驻程序队列的消息是那些由PostMessage或PostAppMessage明确发送的消息。(SendMessage从不使用系统队列)PostQuitMessage函数不会发送一个消息到程序队列。(WM_QUIT消息将在下文中论讨)
默认的,每个程序队列可以保持八个消息。一般情况下这是相当足够的,因为PostMessage极少被使用。但是如果一个应用程序试图强制调用很多的PostMessage到某个应用程序时,那么这类应用程序将会用使用SetMessageQueue函数来增加消息队列的长度。你必须小心的使用SetMessageQueue函数,因为它无论何时都会先删掉当前的程序队列,并创建一个预期大小的新队列,此时任何在旧队列中的消息都会被销毁。因此,它必须在你的WinMain例程中在所有其它的应用程序编程接口(API)之前调用或在应用程序用PeekMessage明确的清除队列之后调用。
GetMessage和PeekMessage是怎样工作的
在Windows的内部,GetMessage和PeekMessage执行着相同的代码。而两者最大的不同之处则体现在没有任何消息返回到应用程序的情况下。在此种情况下,PeekMessage会返回一个空值到应用程序,GetMessage会在此时让应用程序休眠。在它们之间还有一些其它的不同,我们将会在下面讨论,但它们相当次要。
GetMessage和PeekMessage逻辑
下面一步步的讲述了在Windows3.1版的GetMessage和PeekMessage公用代码。
提示:下面所示步骤按照消息类型的优先权进行排序。举个例子,发送的消息总在键盘和鼠标消息之前被返回,而键盘和鼠标的消息又会在绘图(paint)消息之前反回,以此类推。
1. 检视在为“活动中任务”服务的程序队列中是否有消息的存在。如果是,首先在队首删除此消息并将其返回到应用程序。然后,应用程序中的GetMessage和PeekMessage会调用一些代码,用以从程序队列中接收此消息,这些代码是由该应用程序调用的动态链接库(DLL)生成的。记住,只有由PostMessage发送的消息会常驻于此队列中。
2. 与所有消息和窗体句柄过滤器进行对照,核查此消息。如果此消息不匹配指定的过滤器,就会把此消息留在程序队列中。如果队列中在此消息的后面还有其它消息,则会转向对下一个消息的处理。
3. 如果在程序队列中没有消息了,就扫描系统队列中的事件。这个过程相当复杂,并且我们将在下面的“扫描系统队列”小节中XX。一般来讲,在系统队列首部的事件是供这个应用程序所使用的,系统会将其转化为消息,并将消息返回到这个应用程序中(它不会首先被置于应用队列中)。注意,这个扫描系统队列的过程可能导致当前活动的应用程序将控制权让给其它的应用程序。
4. 如果在系统队列中没有等待处理的事件,则核查所有与当前应用程序(任务)相关的窗体以确定更新区域。当一个窗体的一部分需要被重绘时,一个更新区域就被创建在那个窗体部分之上。这个区域将与此窗体中现存的所有更新区域相结合,并储存在内部窗体结构体中。如果GetMessage或PeekMessage在这个任务中发现某些窗体有一些未处理的更新区域,将产生一个WM_PAINT消息,并为那个窗体返回到应用程序中。WM_PAINT从不驻留在任何队列中。此时,一个应用程序将为某个窗体不断的接收WM_PATIN消息,直到更新区域由BeginPaint/EndPaint,ValidateRect,或ValidateRgn所清除。
5. 如果这个任务中没有任何窗体需要被更新,GetMessage和PeekMessage就会在这一点让出控制权,除非PeekMessage调用被设置为PM_NOYIELD属性。
6. 当让步返回时,检视在当前任务中是否有计时器到期。如果是,创建一个WM_TIMER消息并返回。它不但发生在“返回一个WM_TIMER消息到窗体”的计时器上,同样也发生在“调用一个计时器处理过程”的计时器上。如要了解更多信息,请看在微软开发者网络(MSDN)光盘(包括技术文章、Windows文章、核心和驱动程序文章)中的文章“Timers and Timing in Microsoft Windows”(译者注:如果读者能够认可我的工作,我会不遗余力地翻译这篇关于计时器的文章)。
7. 如果这个应用程序没有计时器事件服务,并且一个应用程序正在被终止,代码将尝试去缩小图形设备界面(GDI)的本地内存堆。一些应用程序,比如绘图应用程序(像Paintbrush?),为GDI分配了大量的堆内存。当应用程序终止时释放这些对象时,会使GDI本地内存堆被空闲空间填满而膨胀。为了恢复这些空闲的空间, 在GetMessage/PeekMessage处理中,LocalShrink将在这一点被调用于GDI的内存堆。这个被完成一次,(每次)一个应用程序将终止。
8. 在这一时刻,代码将分叉为两条路,一是代码任意的返回一个有效的消息,另一个是完全没有这个应用程序去处理的消息、事件,而代码最终会走哪条路决定于PeekMessage和GetMessage中的哪一个被调用。
·PeekMessage. 如果PeekMessage被调用,并设置了PM_NOYIELD标记,PeekMessage在此刻返回一个空值,这个空返回值指出已经没有要处理的消息了。如果没有设置PM_NOYIELD标记,PeekMessage就在此刻让出控制权。它不会休眠,但会单一的交给其它已准备好的应用程序一个执行的机会。(请参阅下面的“让步与休眠的不同)当让步返回,PeekMessage直接将控制权返回到应用程序,并返回一个空值,它指出这个应用程序没有要处理的消息了。
?GetMessage. 在此刻,GetMessage会让应用程序休眠、等待,直到一些事件发生需要唤醒应用程序。控制权不会返回到调用GetMessage的应用程序,直到有应用程序必须去处理的消息出现。一旦这个应用程序从被置入休眠状态中醍来,GetMessage内部的循环将回到最开始(步骤1)。
WH_GETMESSAGE钩子
在GetMessage和PeekMessage将一个消息返回到调用的应用程序之前,会做一个验证是否存在一个WH_GETMESSAGE钩子的测试。如果有一个已经被安装了,那这个钩子会被调用。如果PeekMessage没有发现可用的消息并返回一个空值时,这个钩子将不会被调用。在钩子处理过程中,你不可能得知是到底是GetMessage被调用还是PeekMessage被调用。
扫描系统队列
综上所述,在系统队列中的事件仅仅是硬件事件的记录。那些代码扫描系统队列的主要任务是,从这些事件中创建消息,并确定哪一个窗体将接收这个消息。
代码第一次在系统队列首部找到事件时,并不会马上将其删除。因为鼠标和键盘事件只是队列中的两种事件,而代码会分枝(译者注:类似于C语言中的switch语句)并单独处理每一种类型的事件。