Windows消息循环

微軟視窗作業系統是以事件驅動做為程式設計的基礎。程式的執行緒會從作業系統取得訊息。應用程式會不斷迴圈呼叫 GetMessage 函式（或是 PeekMessage 函式）來接收這些訊息，這個迴圈稱之為「**事件迴圈**」。基本上事件迴圈的程式碼如下所示（C 語言 / C++ 程式語言）：

雖然在程式上並沒有很嚴格的規定與要求，但是一般來說，它的事件迴圈通常會呼叫 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 系統的訊息處理機制。

## 背景

### 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 函式（給所有重疊（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），表示按下或釋放的鍵
    * 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。WM\_SYSKEYUP 或 WM\_SYSKEYDOWN 常為 1。WM\_KEYUP 或 WM\_KEYDOWN 常為 0。當所有程式都最小化時，沒有窗口具有輸入焦點，Windows 仍將傳送鍵盤訊息給活動窗口；所有的按鍵都會產生 WM\_SYSKEYUP 與 WM\_SYSKEYDOWN 訊息，此情況下如果沒按下 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：按下 `F10`(將啟用選單條) 或者按下 `Alt` 後再按下別的鍵，或者沒有窗口具有鍵盤輸入焦點（WM\_SETFOCUS 指示窗口得到輸入焦點，WM\_KILLFOCUS 指示窗口失去輸入焦點）時的按鍵，為 SYSKEY。lParam 的第 29 位為 context code，如果為 1 表示 `Alt` 被按下，如果為 0 表示 WM\_SYSKEYDOWN 發出時沒有窗口具有鍵盤輸入焦點。無論使用者處理與否，必須傳送給 Windows 預設窗口過程處理此類按鍵訊息。
    * 其他情形為普通按鍵。除 Print 鍵之外都有「按下」訊息；所有鍵都存在「彈起」訊息。
  * 字元訊息：按鍵訊息 WM\_SYSKEYDOWN、WM\_KEYDOWN 被 Windows API 函式 `TranslateMessage` 處理後該函式線上程訊息佇列投寄 (post) 相應的字元訊息，wParam 參數是 ASCII 或 Unicode 的 character code；這取決於 RegisterClass 函式是 A 或 W 版；IsWindowUnicode 函式判斷窗口過程會接受哪種編碼。產生字元訊息的按鍵有：任何字元鍵、回退鍵（BACKSPACE）、確認鍵（carriage return）、ESC、SHIFT + ENTER（linefeed 換行）、TAB。因為 TranslateMessage 函式從 WM\_KEYDOWN 和 WM\_SYSKEYDOWN 訊息產生了字元訊息，所以字元訊息是夾在按鍵訊息之間傳遞給窗口訊息處理程式的。如果使用者按住一個鍵不放，會自動重複產生一系列的 WM\_KEYDOWN 訊息；對每條 WM\_KEYDOWN 訊息，都會得到一條字元訊息。如果某些 WM\_KEYDOWN 訊息的重複計數大於 1，那麼相應的 WM\_CHAR 訊息將具有同樣的重複計數。
    * WM\_SYSCHAR：按下 `Alt` 後再按下別的鍵的 WM\_SYSKEYDOWN 訊息被翻譯，
    * WM\_CHAR：WM\_KEYDOWN 訊息被翻譯為 WM\_CHAR 訊息。
    * WM\_DEADCHAR：`TranslateMessage` 函式處理「死鍵」（dead key）的 WM\_KEYUP 訊息，向具有輸入焦點的窗口投寄（post）出 WM\_DEADCHAR 訊息。死鍵是產生附加符號的按鍵。例如在德語鍵盤，銳音符被按下、釋放後，再按下 `A`，將獲得字母 á 的 WM\_CHAR。如果在死鍵之後跟有不能帶此附件符號的字母（例如銳音符後跟「s」），那麼將接收到兩條 WM\_CHAR 訊息：前一個訊息的 wParam 等於附加符號本身的 ASCII 碼（與傳遞到 WM\_DEADCHAR 訊息的 wParam 值相同），第二個訊息的 wParam 等於字母的 ASCII 代碼。
    * WM\_SYSDEADCHAR：按下 `Alt` 時又按下了「死鍵」的 WM\_SYSKEYUP 訊息。
* 滑鼠訊息：
  * 客戶區滑鼠訊息：WM\_MOUSEMOVE 及滑鼠按鍵的 DOWN、UP、DBLCLK 訊息。雙擊事件的處理只有窗口類別定義接收（CS\_DBLCLKS）時，才起作用，這時接收到的滑鼠訊息順序為：DOWN、UP、DBLCLK、UP。滑鼠訊息傳送給被單擊的窗口或滑鼠經過的窗口，即使該窗口處於非活動或不帶輸入焦點；例外情況有「擷取滑鼠」時或模式對話方塊處於活動狀態時。訊息參數 wParam (指出那個滑鼠按鈕、Shift 鍵、Ctrl 鍵被按下；lParam 的低位表示 x 坐標，高位表示 y 坐標的滑鼠位置。
  * 非客戶區滑鼠訊息：為 WM\_NC\* 形式。
    * 擊中測試訊息：WM\_NCHITTEST。Windows 利用此訊息來產生其他所有的滑鼠訊息。
  * WM\_MOUSEWHEEL 傳送給具有焦點的窗口（注意不一定是滑鼠下面的窗口）
* 定時器訊息
* 控制項訊息
* 跨行程傳送資料的訊息：WM\_SETTEXT、WM\_GETTEXT、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 偽演算法

## 參考資料

## 相關條目

* C 語言
* C++

## 外部連結

* Meandering Through the Maze of MFC Message and Command Routing
* Platform SDK: Message and Message Queue Overviews （頁面存檔備份，存於網際網路檔案館）
* Platform SDK: Windows API: Entering the Message Loop （頁面存檔備份，存於網際網路檔案館）
* GetMessage function （頁面存檔備份，存於網際網路檔案館）
* PeekMessage function （頁面存檔備份，存於網際網路檔案館）