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訊息。假如有其他訊息,那麼非零值會當成傳回值(有訊息的傳回值通常是正值,但是有些程式設計的說明文件不一定會說明的很詳細)

16位元Windows系統為非搶先單執行緒模式,應用程式沒有傳送訊息佇列,向窗口傳送一個訊息總是按同步方式執行,也即傳送程式要在接受訊息的窗口完全處理完訊息之後才能繼續執行。

這通常是一個所期望的特性。但是,如果接收訊息的窗口花很長的時間來處理訊息或者出現掛起,則傳送程式就不能再執行。這意味著系統是不強壯的。

如果應用程式訊息佇列(只用於存放投寄的訊息)為空,由於沒有虛擬輸入訊息佇列,SendMessage或PeekMessage函式存取系統事件佇列查取可用的滑鼠或鍵盤輸入訊息。

如果系統佇列中沒有需要處理的事件,SendMessage或PeekMessage函式掃描所有窗口以處理需要修改重繪的區域如果沒有需要重繪的區域,則交出CPU控制權

恢復CPU控制權時,檢視是否有定時器過期。至此如果沒有訊息可返回,SendMessage進入睡眠,直至被輸入事件喚醒PeekMessage如果沒有設定PM_NOYIELD標記,則會讓出CPU控制權,但不會讓執行緒休眠,重新獲得CPU後PeekMessage將控制權返回到執行緒,並返回一個空值指出這個執行緒沒有要處理的訊息了。

本文主要關注Win32系統的訊息處理機制

UI執行緒
Windows系統規定,窗口勾點(hook)這兩種User物件分別由建立窗口和安裝勾點的執行緒所擁有,一旦該執行緒結束,作業系統會自動刪除窗口或解除安裝勾點

而其他的User物件(圖示icon、光標cursor、窗口類WndClass、選單、加速鍵表等)則歸行程所有,行程結束時作業系統會自動刪除這些物件
建立窗口的執行緒必須就是處理窗口所有訊息的執行緒,即UI執行緒(User Interface Thread)建立了表單及表單上的各種控制項,系統為UI執行緒分配一個訊息佇列用於窗口訊息的派送(dispatch)。為了使窗口處置這些訊息,執行緒必須有它自己的「訊息迴圈」。

只有當一個執行緒呼叫Windows API中的GDI(Graphics Device Interface)和User函式時,作業系統才會將其看成是一個UI執行緒,並為它分配一些另外的資源,建立一套執行緒訊息佇列;

否則,作業系統把非UI執行緒視作普通工作執行緒(Workhorse),不會為它建立訊息佇列。

因此,呼叫PostThreadMessage前,這個執行緒必須是UI執行緒從而有投寄訊息的佇列,通常可在該執行緒中呼叫一次PeekMessage函式以達到這個目的
如果一個UI執行緒結束執行,作業系統會自動回收它所建立的所有表單。

表單過程

表單過程(Window Procedure)是一個函式,每個表單有一個表單過程,負責處理該表單的所有訊息。
UI控制項也是獨立的「Window」,擁有自己的「表單過程」。

訊息佇列

Windows作業系統的內核空間中有一個系統訊息佇列(system message queue)

在內核空間中還為每個UI執行緒分配各自的執行緒訊息佇列(Thread message queue)

在發生輸入事件之後,Windows作業系統的輸入裝置驅動程式將輸入事件轉換為一個「訊息」投寄到系統訊息佇列

作業系統的一個專門執行緒從系統訊息佇列取出訊息,分發到各個UI執行緒的輸入訊息佇列中。
每個UI執行緒的執行緒資訊塊TIB分配一個THREADINFO的結構,該結構包含一族成員變數,包括:

傳送訊息佇列(send-message queue)指標:

其他發起執行緒通過SendMessage、SendMessageTimeout、SendMessageCallback、SendNotifyMessage、ReplyMessage等函式產生的訊息放入該佇列,發起的執行緒阻塞(掛起)在該佇列上(對於SendMessageCallback、SendNotifyMessage則可不被阻塞)直至訊息處理完或者超時返回。
投寄訊息佇列(posted-message queue)指標:

其他執行緒通過PostMessage函式或PostThreadMessage函式投寄的訊息;

虛擬輸入訊息佇列(virtualized-input queue)指標:

鍵盤與滑鼠事件。該佇列最多只儲存一個鍵盤訊息,僅當應用程式處理完這個鍵盤訊息,
Windows才會從作業系統訊息佇列取出下一個鍵盤訊息放入執行緒的虛擬輸入訊息佇列。

這種方式至少有兩點用途:

一是如果用戶的鍵盤輸入速度快於應用程式處理鍵盤訊息的速度,並且特定按鍵會使輸入焦點從一個窗口切換到另一個窗口,隨後的按鍵就應該是另一個窗口的輸入;

二是Windows API函式TranslateMessage把按鍵訊息轉化為字元訊息,如WM_KEYDOWN轉化為WM_CHAR,然後放入執行緒的虛擬輸入訊息佇列中,成為下一個待處理的鍵盤訊息。

回覆訊息佇列(reply-message queue)指標:

呼叫SendMessage函式的執行緒在這個函式上阻塞後,實際上仍可能被系統使用該執行緒執行其他處理,因此SendMessage函式的目標執行緒把窗口函式的返回值登記到這個佇列作為SendMessage的返回值,以便SendMessage函式從阻塞狀態恢復時能取到該返回值(16位元Windows系統是單執行緒的,因此不可能存在這種需求)。

另外一種使用情形是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執行緒訊息佇列取得訊息。

Windows訊息類別

鍵盤訊息

按鍵訊息:WM_SYSKEYDOWN、WM_SYSKEYUP、WM_KEYDOWN、WM_KEYUP等訊息。wParam參數為虛擬鍵碼(virtual-key code)

按下F10(將啟用選單條)或者按下Alt後再按下別的鍵,或者沒有窗口具有鍵盤輸入焦點時的按鍵,為SYSKEY。lParam的第29位為context code,如果為1表示Alt被按下,如果為0表示WM_SYSKEYDOWN發出時沒有窗口具有鍵盤輸入焦點.

其他情形為普通按鍵。

字元訊息:按鍵訊息WM_SYSKEYDOWN、WM_KEYDOWN或WM_SYSKEYUP、WM_KEYUP被Windows API函式TranslateMessage處理後該函式線上程訊息佇列投寄(post)相應的字元訊息,wParam參數是ASCII character code。

WM_SYSCHAR:按下Alt後再按下別的鍵的WM_SYSKEYDOWN訊息被翻譯,

WM_CHAR:WM_KEYDOWN訊息被翻譯為WM_CHAR訊息。

WM_DEADCHAR:TranslateMessage函式處理「死鍵」(dead key)的WM_KEYUP訊息,向具有輸入焦點的窗口投寄(post)出WM_DEADCHAR訊息。死鍵是產生附加符號的按鍵。
例如在德語鍵盤,銳音符被按下、釋放後,再按下A,將獲得字母á

WM_SYSDEADCHAR:按下Alt時又按下了「死鍵」的WM_SYSKEYUP訊息。

滑鼠訊息
定時器訊息
控制項訊息

跨行程傳送資料的訊息:WM_SETTEXT、WM_GETTEXT、WM_COPYDATA,系統自動分配使用可在行程間共享的記憶體對映檔案來傳遞資料。

留言

熱門文章