本页使用了标题或全文手工转换

Microsoft Windows的訊息迴圈

维基百科,自由的百科全书
跳转至: 导航搜索

微軟視窗操作系统是以事件驅動做為程式設計的基礎。程式的執行緒会从作業系統获取訊息。應用程式會不斷循环呼叫GetMessage函式(或是PeekMessage函式)來接收這些訊息,這個循环稱之為“事件迴圈”。基本上事件迴圈的程式碼如下所示(C語言 / C++程式語言):

MSG msg; //用于存储一条消息
BOOL bRet;

//从UI线程消息队列中取出一条消息
while( (bRet = GetMessage( &msg, NULL, 0, 0 )) != 0)
{
   if (bRet == -1)
   {
       //错误处理代码,通常是直接退出程序
    }
    else
    {
       TranslateMessage(&msg); //转换消息格式
       DispatchMessage(&msg); //分发消息给相应的窗体
     }
}

雖然在程序上並沒有很嚴格的規定與要求,但是一般來說,它的事件迴圈通常會呼叫TranslateMessage函式與DispatchMessage函式,這兩個函式會傳遞訊息給回呼函式,以及调用相应視窗的消息处理函数。

現在的繪圖介面架構程式設計,例如Visual BasicQt基本上是不會要求應用程式直接拥有視窗程式的訊息迴圈,但是會以鍵盤與滑鼠的按鍵動作來作為事件的處理機制。在這些架構底下,訊息迴圈還的痕迹是可以被找到的。

注意:在上述的原始碼裡,尤其在while迴圈大於零的條件。即使GetMessage函式的傳回值型態是英文字大寫的BOOL,但是在Win32視窗程式裡,它是被定義成int整數型態,它有兩個值,TRUE是整數的1,FALSE是整數的0。整數 -1代表error,整數0值当GetMessage获取到WM_QUIT訊息。假如有其他訊息,那麼非零值會當成傳回值(有訊息的傳回值通常是正值,但是有些程式設計的說明文件不一定會說明的很詳細[1][2])。

历史[编辑]

在16位Windows系统中,向窗口发送一个消息总是按同步方式执行的:发送程序要在接受消息的窗口完全处理完消息之后才能继续运行。这通常是一个所期望的特性。但是,如果接收消息的窗口花很长的时间来处理消息或者出现挂起,则发送程序就不能再执行。这意味着系统是不强壮的。

背景[编辑]

UI线程[编辑]

Windows系统规定,窗口和钩子(hook)这两种User对象分别由建立窗口和安装钩子的线程所拥有,一旦该线程结束,操作系统会自动删除窗口或卸载钩子。而其他的User对象(图标icon、光标cursor、窗口类WndClass、菜单、加速键表等)则归进程所有,进程结束时操作系统会自动删除这些对象。

建立窗口的线程必须就是处理窗口所有消息的线程,即UI线程(User Interface Thread)创建了窗体及窗体上的各种控件,系统为UI线程分配一个消息队列用于窗口消息的派送(dispatch)。为了使窗口处置这些消息,线程必须有它自己的“消息循环”的代码。只有当一个线程调用Windows API中的GDI(Graphics Device Interface)和User函数时,操作系统才会将其看成是一个UI线程,并为它分配一些另外的资源,创建一套线程消息队列;否则,操作系统把非UI线程视作普通工作线程(Workhorse),不会为它创建消息队列。

如果一个UI线程结束运行,操作系统会自动回收它所创建的所有窗体。

窗体过程[编辑]

窗体过程(Window Procedure)是一个函数,每个窗体有一个窗体过程,负责处理该窗体的所有消息。

UI控件也是“Window”,拥有自己的“窗体过程”。

消息队列[编辑]

Windows操作系统的系统空间中有一个系统消息队列(system message queue),在系统空间中还为每个UI线程分配各自的线程消息队列(Thread message queue)。在发生输入事件之后,Windows操作系统将输入事件转换为一个「消息」投寄到系统消息队列;操作系统的一个专门线程从系统消息队列取出消息,分发到各个UI线程的输入消息队列中。

每个UI线程的线程信息块TIB分配一个THREADINFO的结构,该结构包含一族成员变量,包括:[3]

  • 发送消息队列(send-message queue)指针:其他发起线程通过SendMessage、SendMessageTimeout、SendMessageCallback、SendNotifyMessage、ReplyMessage等函数产生的消息放入该队列,发起的线程阻塞(挂起)在该队列上(对于SendMessageCallback、SendNotifyMessage则可不被阻塞)直至消息处理完或者超时返回。
  • 虚拟输入消息队列(virtualized-input queue)指针:键盘与鼠标事件。
  • 投递消息队列(posted-message queue)指针:其他线程通过PostMessage函数或PostThreadMessage函数投递的消息;
  • 回复消息队列(reply-message queue)指针:SendMessage函数的目标线程把窗口函数的返回值登记到这个队列作为SendMessage的返回值。另外一种使用情形是SendMessageCallback函数或SendMessage函数给所有重叠(overlapped)窗口广播时,总是调用后立即返回并继续执行,因此接收了此消息的线程把窗口函数执行结果登记到发起线程的回复消息队列,在发起线程下一次调用GetMessage、PeekMessage、WaitMessage或某个SendMessage挂起时从回复消息队列中取出该msg并执行登记的ResultCallBack函数。
  • nExitCode:由PostQuitMessage函数设置该成员。
  • 唤醒标志(wake flage)
  • 局部输入状态变量
    • QS_POSTMESSAGE位:投递消息队列是否为空;
    • QS_QUIT位:由PostQuitMessage函数给该标志置位。
    • QS_SENDMESSAGE位:发送消息队列是否为空;
    • QS_KEY:有按键消息
    • QS_MOUSE:有鼠标消息
    • QS_PAINT:有WM_PAINT
    • QS_TIMER:有WM_TIMER

应用程序的每个UI线程中有一段称之为“消息循环”的代码,通过GetMessage系统调用(或是PeekMessage系统调用)访问系统空间中的对应的UI线程的消息队列,并依照下述次序处理:

  • QS_SENDMESSAGE置位:则对发送消息队列中的每个消息,依次调用各个发送消息的窗口函数直接处理,GetMessage不返回;直至所有发送消息队列中的消息处理完毕。
  • QS_POSTMESSAGE置位:则填充GetMessage函数参数的MSG结构为相应的投递消息,GetMessage返回为真。该消息通过DispatchMessage系统调用把消息分发给相应窗口的消息处理函数。
  • QS_QUIT置位:则填充GetMessage函数参数的MSG结构为WM_QUIT,QS_QUIT复位,GetMessage返回为假。
  • QS_INPUT置位:则填充GetMessage函数参数的MSG结构为相应的输入消息,GetMessage返回为真。该消息通过DispatchMessage系统调用把消息分发给相应窗口的消息处理函数。
  • 再一次检查QS_SENDMESSAGE置位,如是则处理发送消息队列中的每个消息。
  • QS_PAINT置位:则填充GetMessage函数参数的MSG结构为WM_PAINT,GetMessage返回为真。GetMessage不从队列中删除WM_PAINT消息(即不对QS_PAINT复位)。
  • QS_TIMER置位:则填充GetMessage函数参数的MSG结构为WM_TIMER,QS_TIMER复位,GetMessage返回为真。如果QS_TIMER复位状态,则当前线程挂起(hung)。

需要注意的是,GetMessage如果在应用程序消息队列未获取消息,则GetMessage调用不返回,该线程挂起,CPU使用权交给操作系统。即GetMessage为阻塞调用。

由此可见,Windows的事件驱动模式,并不是操作系统把消息主动分发给应用程序;而是由应用程序的每个UI线程通过“消息循环”代码从UI线程消息队列获取消息。

參考資料[编辑]

  1. ^ GetMessage function
  2. ^ PeekMessage function
  3. ^ (美)Jeffrey Richter:Programming Applications for Microsoft Windows, Microsoft Press,2000,Fourth edition,“第26章 窗口消息”,《Windows核心编程》中文版,机械工业出版社2008年5月1日。 ISBN:9787111237914.

相關條目[编辑]

外部連結[编辑]