視窗訊息 (開始使用 Win32 和 C++)

GUI 應用程式必須回應使用者和作業系統的事件。

  • 使用者 的事件包括某人與程式互動的所有方式:按鍵筆劃、觸摸屏手勢等等。
  • 來自操作系統 的事件包括程式「外部」的任何專案,可能會影響程序的行為。 例如,使用者可能會插入新的硬體裝置,或 Windows 可能會進入低電源狀態(睡眠或休眠)。

程序執行時,幾乎任何順序都可能發生這些事件。 如何建構無法事先預測執行流程的程式?

若要解決此問題,Windows 會使用訊息傳遞模型。 操作系統會透過將訊息傳遞至應用程式視窗,與應用程式窗口通訊。 訊息只是指定特定事件的數值程序代碼。 例如,如果使用者按下滑鼠左鍵,視窗就會收到具有下列訊息碼的訊息。

#define WM_LBUTTONDOWN    0x0201

有些訊息有與其相關聯的數據。 例如, WM_LBUTTONDOWN 訊息包含滑鼠游標的 x 座標和 Y 座標。

若要將訊息傳遞至視窗,操作系統會呼叫為該視窗註冊的窗口程式。 (現在你知道視窗程序是什麼。

訊息迴圈

應用程式會在執行時接收數千則訊息。 (請考慮每個擊鍵和滑鼠按鈕按鍵按兩下都會產生訊息。此外,應用程式可以有數個視窗,每個視窗都有自己的視窗程式。 程式如何接收所有這些訊息,並將其傳遞至正確的視窗程式? 應用程式需要迴圈來擷取訊息,並將其分派至正確的視窗。

針對每個建立窗口的線程,操作系統會建立視窗訊息的佇列。 此佇列會保留該線程上建立之所有視窗的訊息。 佇列本身會隱藏在您的程式中。 您無法直接操作佇列。 不過,您可以呼叫 GetMessage 函式,從佇列提取訊息。

MSG msg;
GetMessage(&msg, NULL, 0, 0);

此函式會從佇列的前端移除第一個訊息。 如果佇列是空的,函式會封鎖直到另一個訊息排入佇列為止。 GetMessage 區塊不會讓您的程式沒有回應。 如果沒有訊息,則程式不會執行任何動作。 如果您必須執行背景處理,您可以建立其他線程,以在 GetMessage 等候另一則訊息時繼續執行。 (請參閱 避免視窗程式中的瓶頸。

GetMessage 的第一個參數是 MSG 結構的位址。 如果函式成功,它會以訊息的相關信息填入 MSG 結構。 這包括目標視窗和訊息碼。 其他三個參數可讓您篩選從佇列取得的訊息。 在幾乎所有情況下,您將將這些參數設定為零。

雖然 MSG 結構包含訊息的相關信息,但您幾乎永遠不會直接檢查此結構。 相反地,您會將它直接傳遞至另外兩個函式。

TranslateMessage(&msg); 
DispatchMessage(&msg);

TranslateMessage 函式與鍵盤輸入相關。 它會將擊鍵(按鍵向下、向上鍵)轉譯成字元。 您不需要真正知道此函式的運作方式;只要記得在 DispatchMessage 之前呼叫它。

DispatchMessage 函式會告訴作業系統呼叫訊息目標視窗的視窗程式。 換句話說,操作系統會在其窗口數據表中查閱視窗句柄、尋找與視窗相關聯的函式指標,並叫用函式。

例如,假設使用者按下滑鼠左鍵。 這會導致事件鏈結:

  1. 操作系統會將WM_LBUTTONDOWN訊息放在消息佇列上。
  2. 您的程式會呼叫 GetMessage 函式。
  3. GetMessage從佇列提取WM_LBUTTONDOWN訊息,並填入 MSG 結構。
  4. 您的程式會呼叫 TranslateMessage 和 DispatchMessage 函式。
  5. 在 DispatchMessage,作業系統會呼叫您的視窗程式。
  6. 您的視窗程式可以回應訊息或忽略它。

當視窗程序傳回時,它會返回 DispatchMessage 這會傳回下一個訊息的訊息迴圈。 只要程式正在執行,訊息就會繼續到達佇列。 因此,您必須有一個迴圈,其會持續從佇列提取訊息並分派訊息。 您可以將 循環視為執行下列動作:

// WARNING: Don't actually write your loop this way.

while (1)      
{
    GetMessage(&msg, NULL, 0,  0);
    TranslateMessage(&msg); 
    DispatchMessage(&msg);
}

當然,如書面所示,這個循環永遠不會結束。 這就是 GetMessage 函式傳回值所在的位置。 一般而言, GetMessage 會傳回非零值。 當您想要結束應用程式並中斷訊息循環時,請呼叫 PostQuitMessage 函式。

        PostQuitMessage(0);

PostQuitMessage 函式會將WM_QUIT訊息放在消息佇列上。 WM_QUIT是特殊的訊息:它會導致 GetMessage 傳回零,表示訊息循環的結尾。 以下是修訂後的訊息迴圈。

// Correct.

MSG msg = { };
while (GetMessage(&msg, NULL, 0, 0) > 0)
{
    TranslateMessage(&msg);
    DispatchMessage(&msg);
}

只要 GetMessage 傳回非零值,while 迴圈中的運算式就會評估為 true。 呼叫 PostQuitMessage 之後,表達式會變成 false,而程式會中斷迴圈。 (此行為有一個有趣的結果,就是您的視窗程序永遠不會收到 WM_QUIT訊息。因此,您不需要在視窗程式中有此訊息的case語句。

下一個明顯的問題是何時呼叫 PostQuitMessage 我們將在關閉視窗主題中回到這個問題,但首先我們必須撰寫視窗程式。

已張貼的訊息與已傳送的訊息

上一節討論訊息進入佇列。 有時候,操作系統會直接呼叫視窗程式,略過佇列。

此區別的術語可能會造成混淆:

  • 張貼訊息表示訊息會進入消息佇列,並透過訊息迴圈分派 (GetMessage 和 DispatchMessage)。
  • 傳送 訊息表示訊息會略過佇列,而操作系統會直接呼叫窗口程式。

目前,差異並不十分重要。 視窗程序會處理所有訊息。 不過,某些訊息會略過佇列,並直接移至您的視窗程式。 不過,如果您的應用程式在窗口之間通訊,可能會有所差異。 您可以在關於訊息和消息佇列主題中找到此問題的更徹底討論。

下一步

撰寫視窗程式