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 Basic 與 Qt 基本上是不會要求應用程式直接擁有視窗程式的訊息迴圈，但是會以鍵盤與滑鼠的按鍵動作來作為事件的處理機制。在這些架構底下，訊息迴圈的痕跡還是可以被找到的。

注意：在上述的原始碼裡，尤其在 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 系統的訊息處理機制。

背景

2.1 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 執行緒結束執行，作業系統會自動回收它所建立的所有窗體。

2.2 窗體過程

窗體過程（Window Procedure）是一個函式，每個窗體有一個窗體過程，負責處理該窗體的所有訊息。

UI 控制元件也是獨立的 “Window”，擁有自己的 “窗體過程”。

2.3 訊息佇列

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 把按鍵訊息轉化為字元訊息，如 WMKEYDOWN 轉化為 WMCHAR，然後放入執行緒的虛擬輸入訊息佇列中，成為下一個待處理的鍵盤訊息。
回覆訊息佇列（reply-message queue）指標：呼叫 SendMessage 函式的執行緒在這個函式上阻塞後，實際上仍可能被系統使用該執行緒執行其他處理，因此 SendMessage 函式的目標執行緒把視窗函式的返回值登記到這個佇列作為 SendMessage 的返回值，以便 SendMessage 函式從阻塞狀態恢復時能取到該返回值（16 位 Windows 系統是單執行緒的，因此不可能存在這種需求）。另外一種使用情形是 SendMessageCallback 函式（給所有重疊（overlapped）視窗廣播）時，總是呼叫後立即返回並繼續執行，因此接收了此訊息的執行緒把視窗函式執行結果登記到發起執行緒的回覆訊息佇列，在發起執行緒下一次呼叫 GetMessage、PeekMessage、WaitMessage 或某個 SendMessage 掛起時從回覆訊息佇列中取出該 msg 並執行登記的 ResultCallBack 函式。
nExitCode：由 PostQuitMessage 函式設定該成員，作為執行緒的退出碼。
喚醒標誌（wake flage）
區域性輸入狀態變數
QS_POSTMESSAGE 位：投寄訊息佇列是否為空；
QS_QUIT 位：由 PostQuitMessage 函式給該標誌置位。
QS_SENDMESSAGE 位：傳送訊息佇列是否為空；
QS_KEY：有按鍵訊息
QS_MOUSE：有滑鼠訊息
QSPAINT：有 WMPAINT
QSTIMER：有 WMTIMER

應用程式的每個 UI 執行緒中有一段稱之為 “訊息迴圈” 的程式碼，透過 GetMessage 系統呼叫（或是 PeekMessage 系統呼叫）訪問系統空間中的對應的 UI 執行緒的訊息佇列，並依照下述次序處理：

QS_SENDMESSAGE 置位：則對傳送訊息佇列中的每個訊息，依次呼叫各個傳送訊息的視窗函式直接處理，GetMessage 不返回；直至所有傳送訊息佇列中的訊息處理完畢。
QS_POSTMESSAGE 置位：則填充 GetMessage 函式引數的 MSG 結構為相應的投寄訊息，GetMessage 返回為真。該訊息透過 DispatchMessage 系統呼叫把訊息分發給相應視窗的訊息處理函式。
QSQUIT 置位：則填充 GetMessage 函式引數的 MSG 結構為 WMQUIT，QS_QUIT 復位，GetMessage 返回為假。
QS_INPUT 置位：則填充 GetMessage 函式引數的 MSG 結構為相應的輸入訊息，GetMessage 返回為真。該訊息透過 DispatchMessage 系統呼叫把訊息分發給相應視窗的訊息處理函式。
再一次檢查 QS_SENDMESSAGE 置位，如是則處理傳送訊息佇列中的每個訊息。
QSPAINT 置位：則填充 GetMessage 函式引數的 MSG 結構為 WMPAINT，GetMessage 返回為真。GetMessage 不從佇列中刪除 WMPAINT 訊息（即不對 QSPAINT 復位）。
QSTIMER 置位：則填充 GetMessage 函式引數的 MSG 結構為 WMTIMER，QSTIMER 復位，GetMessage 返回為真。如果 QSTIMER 復位狀態，則當前執行緒掛起（hung）。

需要注意的是，GetMessage 如果在應用程式訊息佇列未獲取訊息，則 GetMessage 呼叫不返回，該執行緒掛起，CPU 使用權交給作業系統。即 GetMessage 為阻塞呼叫。

由此可見，Windows 的事件驅動模式，並不是作業系統把訊息主動分發給應用程式；而是由應用程式的每個 UI 執行緒透過 “訊息迴圈” 程式碼從 UI 執行緒訊息佇列獲取訊息。

Windows 訊息類別

鍵盤訊息：
按鍵訊息：WMSYSKEYDOWN、WMSYSKEYUP、WMKEYDOWN、WMKEYUP 等訊息。
wParam 包含虛擬鍵碼（virtual-key code），表示按下或釋放的鍵
lParam 包含按鍵 6 個欄位資訊：
* 重複按鍵次數（Repeat Count，0～15 位）：通常設為 1。大於 1 說明按鍵速度大於程式處理能力。可以根據實際需要忽略或處理。
*OEM 掃描碼 (scan code，16～23 位)：硬體產生的程式碼。
* 擴充鍵標誌（extended key，24 位）：如果為擴充鍵（如右側的 Alt 鍵或 Ctrl 鍵）按下時為 1，否則為 0。
* 保留位（25～28 位）：保留位是系統預設保留的，一般不用。
* 上下文程式碼（context code，29 位）：如同時按下 ALT，標誌為 1；否則為 0。WMSYSKEYUP 或 WMSYSKEYDOWN 常為 1。WMKEYUP 或 WMKEYDOWN 常為 0。當所有程式都最小化時，沒有視窗具有輸入焦點，Windows 仍將傳送鍵盤訊息給活動視窗；所有的按鍵都會產生 WMSYSKEYUP 與 WMSYSKEYDOWN 訊息，此情況下如果沒按下 ALT，該欄位為 0，這樣使最小化的活動視窗不處理這些按鍵。對於一些非英文鍵盤，有些字元是 shift 等組合鍵產生的，這時內容程式碼為 1，但是其是非系統按鍵。
* 鍵先前狀態（previous key state，位 30）：鍵此前是釋放的，則為 0，還則為 1。很明顯 UP 為 1，DOWN 可以為 1 或 0，為 1 表示該鍵自動重複。
* 轉換狀態（transition state，31 位）：鍵被按下為 0，鍵被鬆開時為 1。如 UP 為 1，DOWN 為零。
SYSKEY：按下 (將啟用選單條) 或者按下後再按下別的鍵，或者沒有視窗具有鍵盤輸入焦點（WMSETFOCUS 指示視窗得到輸入焦點，WMKILLFOCUS 指示視窗失去輸入焦點）時的按鍵，為 SYSKEY。lParam 的第 29 位為 context code，如果為 1 表示被按下，如果為 0 表示 WM_SYSKEYDOWN 發出時沒有視窗具有鍵盤輸入焦點。無論使用者處理與否，必須傳送給 Windows 預設視窗過程處理此類按鍵訊息。
其他情形為普通按鍵。除 Print 鍵之外都有 “按下” 訊息；所有鍵都存在 “彈起” 訊息。
字元訊息：按鍵訊息 WMSYSKEYDOWN、WMKEYDOWN 被 Windows API 函式 TranslateMessage 處理後該函式線上程訊息佇列投寄 (post) 相應的字元訊息，wParam 引數是 ASCII 或 Unicode 的 character code；這取決於 RegisterClass 函式是 A 或 W 版；IsWindowUnicode 函式判斷視窗過程會接受哪種編碼。產生字元訊息的按鍵有：任何字元鍵、回退鍵（BACKSPACE）、回車鍵（carriage return）、ESC、SHIFT + ENTER（linefeed 換行）、TAB。因為 TranslateMessage 函式從 WMKEYDOWN 和 WMSYSKEYDOWN 訊息產生了字元訊息，所以字元訊息是夾在按鍵訊息之間傳遞給視窗訊息處理程式的。如果使用者按住一個鍵不放，會自動重複產生一系列的 WMKEYDOWN 訊息；對每條 WMKEYDOWN 訊息，都會得到一條字元訊息。如果某些 WMKEYDOWN 訊息的重複計數大於 1，那麼相應的 WMCHAR 訊息將具有同樣的重複計數。
WMSYSCHAR：按下後再按下別的鍵的 WMSYSKEYDOWN 訊息被翻譯，
WMCHAR：WMKEYDOWN 訊息被翻譯為 WM_CHAR 訊息。
WMDEADCHAR：TranslateMessage 函式處理 “死鍵”（dead key）的 WMKEYUP 訊息，向具有輸入焦點的視窗投寄（post）出 WMDEADCHAR 訊息。死鍵是產生附加符號的按鍵。例如在德語鍵盤，銳音符被按下、釋放後，再按下，將獲得字母 á 的 WMCHAR。如果在死鍵之後跟有不能帶此附件符號的字母（例如銳音符後跟「s」），那麼將接收到兩條 WMCHAR 訊息：前一個訊息的 wParam 等於附加符號本身的 ASCII 碼（與傳遞到 WMDEADCHAR 訊息的 wParam 值相同），第二個訊息的 wParam 等於字母的 ASCII 程式碼。
WMSYSDEADCHAR：按下時又按下了 “死鍵” 的 WMSYSKEYUP 訊息。
滑鼠訊息：
客戶區滑鼠訊息：WMMOUSEMOVE 及滑鼠按鍵的 DOWN、UP、DBLCLK 訊息。雙擊事件的處理只有視窗類定義接收（CSDBLCLKS）時，才起作用，這時接收到的滑鼠訊息順序為：DOWN、UP、DBLCLK、UP。滑鼠訊息傳送給被單擊的視窗或滑鼠經過的視窗，即使該視窗處於非活動或不帶輸入焦點；例外情況有 “捕獲滑鼠” 時或模式對話方塊處於活動狀態時。訊息引數 wParam (指出那個滑鼠按鈕、Shift 鍵、Ctrl 鍵被按下；lParam 的低位表示 x 座標，高位表示 y 座標的滑鼠位置。
非客戶區滑鼠訊息：為 WM_NC * 形式。
擊中測試訊息：WM_NCHITTEST。Windows 利用此訊息來產生其他所有的滑鼠訊息。
WM_MOUSEWHEEL 傳送給具有焦點的視窗（注意不一定是滑鼠下面的視窗）
定時器訊息
控制元件訊息
跨程序傳送資料的訊息：WMSETTEXT、WMGETTEXT、WM_COPYDATA，系統自動分配使用可在程序間共享的記憶體對映檔案來傳遞資料。

鍵盤輸入時需要明確插入符位置，相關 API 函式為：CreateCaret、SetCaretPos、ShowCaret、HideCaret、DestroyCaret、GetCaretPos、GetCaretBlinkTime、SetCaretBlinkTime。

3 個獲得鍵狀態的函式：GetKeyState、GetAsyncKeyState、GetKeyboardState。

對於自定義的控制元件，當單擊子視窗時，父視窗會得到焦點。但對於標準子視窗控制元件，單擊時會自動獲得焦點（子視窗過程在 WM_LBUTTONDOWN 中實現了 SetFocus (hwnd)）。如果一個子視窗擁有輸入焦點，滑鼠單擊另一個兄弟子視窗，則兄弟子視窗獲得輸入焦點。

同步與阻塞

Windows API 函式 SendMessage 是個同步呼叫，即它發出的 Windows 訊息沒被處理完之前這個函式就不返回。但這個函式不是阻塞的。分兩種情形：

如果被指定的視窗是發起呼叫 SendMessage 的執行緒建立的，則該執行緒立即執行視窗過程（window procedure）；
如果被指定的視窗不是發起呼叫 SendMessage 的執行緒建立的，作業系統切換到該視窗所屬的執行緒執行相應的視窗過程。訊息處理完之前，傳送執行緒不從呼叫 SendMessage 處返回，但傳送執行緒這期間可以處理非佇列訊息（nonqueued message）。這是 “同步非阻塞”。

非同步傳送或投寄訊息的函式，如 PostMessage、SendMessageCallback、SendNotifyMessage，訊息引數中不能使用指標，否則函式呼叫失敗。

GetMessage 偽演算法

BOOL GetMessage (MSG *lpMsg, HWND hWnd , UINT wMsgFilterMin, UINT wMsgFilterMax)

{

// 檢視 QS_SENDMESSAGE 標誌，如果有的話迴圈處理，直到沒有訊息位置

DWORD dwRetVal = 0;

ThreadInfo threadInfo;

FLAG_SENDPROCLOOP:

GetThreadInfo (GetCurrentThreadId , &threadInfo);

while (threadInfo.QSSENDMESSAGE == QSSIGNALSET) {

// 從傳送訊息佇列中獲取訊息

dwReturnVal = GetMsgFromQueue (QUEUE_SEND, lpMsg, hWnd,wMsgFilterMin, wMsgFilterMax);

// 判斷是否取到訊息，有則呼叫視窗函式，無則復為 QS_SENDMESSAGE 標誌

If (dwReturnVal == GETMESSAGE_HASMESSAGE) {

// 呼叫指定視窗的視窗函式

CallWindowProc (hWnd, &threadInfo, lpMsg);

}

else {

QSSENDMESSAGE = QSSIGNALRESET;

break;

}

}

// 在繼續處理之前再次檢查傳送訊息佇列

if (threadInfo.QSSENDMESSAGE == QSSIGNALSET) goto FLAG_SENDPROCLOOP;

// 檢查傳送訊息佇列，如果有訊息則取傳送訊息

// 判斷是否還有傳送訊息，沒有了則復位 QS_POSTMESSAGE 標誌

if (threadInfo.QSPOSTMESSAGE == QSSIGNALSET) {dwReturnVal = GetMsgFromQueue (QUEUE_POST, lpMsg, hWnd, wMsgFilterMin, wMsgFilterMax);

if (dwReturnVal == GETMESSAGE_LASTMESSAGE)

threadInfo.QSPOSTMESSAGE = QSSIGNALRESET;

return TRUE;

}

// 如果退出標誌被置位

if (threadInfo.QSQUIT == QSSIGNALSET) {

threadInfo.QSQUIT = QSSIGNALRESET;

FillMessage (lpMsg, MESSAGE_QUIT);

return FALSE;

}

// 檢查輸入訊息佇列

if (threadInfo.QSINPUT == QSSIGNALSET) {DWORD dwRetVal = GetMessageFromQueue (QUEUE_INPUT, lpMsg, hWnd, wMsgFilterMin, wMsgFilterMax);

// 檢查是否有鍵盤，滑鼠訊息

if (Test (dwRetVal, QSKEY) == QSLASTMOUSEKEYMESSAGE)

threadInfo.QSKEY = QSSIGNALRESET;

if (Test (dwRetVal, QSMOUSEBUTTON) == QSLASTMOUSEMESSAGE)

threadInfo.QSMOUSEBUTTON = QSSIGNALRESET;

return TRUE;

}

// 測試 QS_PAINT

if (threadInfo.QSPAINT == QSSIGNALSET) {

// 填充 MSG，如果沒有視窗過程確認視窗，則復位 QS_PAINT 標誌

//...

// 返回 TRUE

threadInfo.QSPAINT = QSSIGNALRESET;

return TRUE;

}

if (threadInfo.QSTIMER == QSSIGNALSET) {

// 填充 MSG，如果沒有定時器報時，則復位 QS_TIMER 標誌

//...

// 返回 TRUE

return TRUE;

}

// 等待有訊息到達

dwRetVal = MsgWaitForMultipleObjectsEx (...);

if (...)

goto FLAG_SENDPROCLOOP;

// 等待失敗

return FALSE;

}