1. 程式人生 > >Windows 訊息機制淺析

Windows 訊息機制淺析

1. Windows 的歷史

中國人喜歡以史為鑑,而事實也確實是,如果你能知道一件事情的來龍去脈,往往可以更容易地理解事物為什麼會表現為當前這樣的現狀。所以,我的介紹性開場白通常會以一段歷史開始。不過,我不會以精確到年月日的那種方式詳細講述,而是選取幾個對我們的程式設計生涯有重要影響的關鍵點。

Windows 是真正的圖形化介面作業系統的普及者,無論任何人,爭奪什麼第一個實現的 GUI、第一個商業化的 GUI 之類的虛名,都替代不了 Windows 的歷史功績,讓最普通的使用者能夠容易地操縱 PC。

第一個聲名大噪的版本是 Windows 3.0(也有人認為應該是它的更加健康強壯的弟弟 Windows 3.1),從那個時候開始,我們就和本文中以下的幾個關鍵角色有了不盡的情緣:

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

上面程式碼中的這三個相關函式,會在後文中提到。

第二個大紅大紫的版本則非 Windows 95 莫屬。這個版本的主要變化在於,無論如何,它是一個大眾化的所謂 32 位系統了。之所以要加上“所謂的”三個字,是因為這個系統是個混血兒,在 32 位程式碼中混雜有大量的從之前的 Windows 3.x 上移植過來的 16 位程式碼。

此時間稍後,另一支潛力股的關鍵進化過程結束,Windows NT 4.0 隆重登場,這個分支的作業系統是全 32 位的,成為了  Windows 95 系列的掘墓者,也是我們現在所使用幾乎所有的 Windows 桌面系統(Windows 2000/XP/2003/Vista/2008)的前輩。但是,這個版本由於對系統硬體的要求甚高(在當時),所以沒有引起普通使用者的廣泛關注。

下一個里程碑就是 Windows 2000 了,微軟實現了 Windows9x/Me 分支和 Windows NT 分支的合併。緊接著,Windows XP 現身。從有關訊息方面來考察,Windows 2000 做了微小的改進,在此之前,我們在很多情況下需要建立一個正常的、隱藏的、完整的視窗來處理訊息,而 Windows 2000 引入了一種特殊型別的視窗用於此類需求。道理上來講,應該會減少一些資源佔用。

此後經過五六年的時間,Windows Vista 誕生。事實上,從 Windows 2000 開始,Windows 家族的程式設計模型,尤其是對原生態程式碼(native code)而言,已經基本沒有太大的變化。通常只是增加了新的 API 或者使用者控制元件,或者現有控制元件增加了新的功能或者風格。儘管 Windows Vista 中有很多的變化,但是對於我們今天要講到的主題,影響不大。最主要的一個影響,是訊息的傳送方和接收方之間有了等級限制,不像之前可以隨意互相進行訊息傳遞,這是出於安全性的考慮。

2. Windows 的巨集觀構造

從最原始的版本開始,有三個比較大的功能塊佔據了 Windows 系統的絕大部分,這三個塊,就是赫赫有名的 Kernel、GDI、User。從 Windows 95 起,另兩個在先前不太起眼的部分也迅速崛起,那就是大名鼎鼎的 Registry 和 Shell。

這幾個大塊的分工是這樣的:Kernel,望文生義,負責核心部分,這是任何一個可以稱之為作業系統的東西的基石,主要職責有:記憶體管理、任務排程、外設管理等;GDI,則是對可以進行圖形化操縱的裝置的操作介面,對外提供的主要功能是在裝置上:提供座標系統,繪製點、線、形狀,進行填充,文字繪製,管理畫筆、畫刷、字型等繪圖物件;User,則是前兩者的粘合劑,使系統能夠通過圖形化操作方式和使用者(也就是 User)進行互動,把零散的 GDI 物件有機地組織起來,抽象為視窗,用以接受使用者的輸入,進行相應的運算(廣義上的,並不是侷限於算數運算),並最終將結果呈現給使用者。當然,User 部分通常是指可以實現上述的功能的基礎構造,真正的實現部分需要大量的額外工作,這也是 Shell 部分的主要工作。而 Registry,則是提供給使用者一種與物理儲存無關的統一的資料訪問方式。

很容易就可以看出,訊息功能,這種被我們一直以視窗間通訊最為自然的方式所使用的機制,應該隸屬於 User 部分。

對於 Windows Mobile 系統來說,底層的實現上與桌面系統大相徑庭,例如,它本身並沒有 kernel32.dll、gdi32.dll、user32.dll 這幾個眾所周知的系統庫,而是有一個多合一的 coredll.dll,而且核心被實現為一個更接近於正常程序的 nk.exe 程序,而不是桌面系統下的那個抽象的執行體。儘管如此,但是在邏輯上,我們依然可以將之與桌面系統同等看待。

3. Windows 的訊息概念

在我們的通常認識上,訊息事實就是一個數值。我們檢查一下訊息相關的各個回撥函式的原型就會發現,表示訊息的那個引數的資料型別是 UINT,也就是無符號的整數型別。不過,我們通常也會發現,訊息往往還附帶有兩個其他型別的資料,一個是 WPARAM 型別的,一個是 LPARAM 型別的,如果算上訊息的目標視窗的控制代碼,那麼,一個訊息以及相關資訊才能夠說是比較完整。為什麼說是比較呢?看一下 MSG 這個結構的定義就會發現,其實還有另外兩個我們不太經常使用的資料,是與一條訊息有關係的。MSG 的完整宣告如下:

typedef struct {
    HWND hwnd;
    UINT message;
    WPARAM wParam;
    LPARAM lParam;
    DWORD time;
    POINT pt;
} MSG, *PMSG;

前四項正是我們已經提及過的,而後兩項,一個表示訊息發生時的時間,一個表示此訊息發生時的按螢幕座標表示的滑鼠游標的位置。

從這個結構也可以看出,我們經常所說的訊息,更多是指代表了一個確定的訊息的數值。

我們可能還會聽到有這樣的稱呼:命令訊息、通知訊息、反射訊息等等。首先需要宣告的一點是,這並不是對 Windows 系統中的訊息的科學分類,而是在某些特定場景下的通俗稱謂。命令訊息,一般特指 WM_COMMAND 訊息,此訊息通常由控制元件或者選單發出,表示使用者執行/發出了一個命令。通知訊息,一般特指 WM_NOTIFY 訊息,此訊息通常由公用控制元件(Common Controls)發出,表示一些事件發生了,需要處理。反射訊息,一般用於對 Windows API 的封裝類或者類庫中。這是一類訊息的總稱,它們的處理需要經過一種被稱為“反射”的機制。這一機制的具體方式下一節中會有描述。

Windows 的訊息分類不好分(如果非要劃分的話,可以分為系統定義的訊息和應用程式定義的訊息),不過有一個區段劃分。從 0x0000 到 0x03FF,為系統定義的訊息,常見的 WM_PAINT、WM_CREATE 等均在其中;從 0x0400 到 0x7FFF,專用於使用者自定義的訊息,可以使用 WM_USER + x 的形式自行定義,其中 WM_USER 的值就是 0x0400,x 取一個整數;從 0x8000 到 0xBFFF,從 Windows 95 開始,也用作使用者自定義的訊息範圍,可以使用 WM_APP + x 的形式自行定義。根據微軟的建議,WM_APP 類訊息用於程式之間的訊息通訊,而 WM_USER 類訊息則最好用於某個特定的視窗類。微軟自己遵循這一慣例,所以,公用控制元件的訊息,如 TVM_DELETEITEM,基本都是 WM_USER 類屬。從 0xC000 開始,到 0xFFFF,這個區段的訊息值保留給 RegisterWindowMessage 這個 API,此 API 可以接受一個字串,把它變換成一個唯一的訊息值。在桌面系統上,最常見的源字串,可能就是“TaskbarCreated”了,由它對應的訊息會發送到所有的頂級視窗,通知工作列剛剛被建立(可能是由於資源管理崩潰後重新啟動導致的)。

由上也可以看出,Windows 的訊息值是一個 16 位的數字,這是 16 系統時代留給我們的痕跡。另外的一個痕跡是 WPARAM 和 LPARAM 這兩個資料型別,在 16 位時代,WPARAM 是 16 位的,其名字的意思是 word parameter,LPARAM 是 32 位的,其名字的意思是 long parameter。

4. Windows 的訊息機制

4.1. 訊息佇列

說到訊息機制,可能連最初級的 Windows 程式設計師都會對訊息佇列(Message Queue)這個名詞耳熟(不過不見得能詳)。對於這樣一個基本概念,Windows 作業系統提供的針對訊息佇列的API 卻少的可憐(GetQueueStatus、GetInputState、GetMessageExtraInfo、SetMessageExtraInfo),而且,這些 API 的出鏡率也相當的低,甚至有不少經驗豐富的程式設計師也從來沒有使用過它們。在 Windows Mobile 上,這些 API 乾脆付諸闕如,不過有一個同樣極少使用的 GetMessageQueueReadyTimeStamp 函式在充門面。

這一切,都歸功於在 API 層極好的封裝性,減少了開始接觸這個平臺時需要了解的概念。但是,對於我們這樣既想知其然,又想知其所以然的群體,還是有必要對訊息佇列有充分的瞭解。

4.1.1. 系統訊息佇列

這是一個系統唯一的佇列,輸入裝置(鍵盤、滑鼠或者其他)的驅動程式會把使用者的操作輸入轉化成訊息放置於系統佇列中,然後系統會把此訊息轉到目標視窗所線上程的訊息佇列中等待處理。

4.1.2. 執行緒訊息佇列(應用程式訊息佇列)

應用程式訊息佇列這個名稱是歷史遺留,在 32 位(以及之後的 64 位)系統中,正確的名稱應該是執行緒訊息佇列。每一個 GUI 執行緒都會維護這樣一個執行緒訊息佇列。(這個佇列只有在執行緒呼叫 User 或者 GDI 函式時才會建立,預設並不建立)。然後執行緒訊息佇列中的訊息會被本執行緒的訊息迴圈(有時也被稱為訊息泵)派送到相應的視窗過程(也叫視窗回撥函式)處理。

4.2. 訊息的生命期

4.2.1. 訊息的產生

訊息產生的源頭有兩個,一個是系統,一個是應用程式。系統產生的訊息又可以大致分為兩類,一類是由輸入裝置導致的,例如 WM_MOUSEMOVE,一類是 User 部分(或者是系統內的其他部分通過 User 部分)為了實現自身的正常行為或者管理功能而主動生成的,如 WM_WINDOWPOSCHANGED。

產生的方式也有兩種,一種稱為傳送(Send),另一種稱為投遞(Post,也有譯作張貼的),對應於大家極為熟悉的兩個 API,SendMessage 和 PostMessage。系統產生的訊息,雖然我們看不到程式碼,不過我們還是可以粗略地劃撥一下,基本上所有的輸入類訊息,都是以投遞的方式抵達應用的,而其他的訊息,則大部分是採取了傳送方式。

至於應用程式,可以隨意選用適合自己的訊息產生方式。

4.2.2. 訊息的處理

在絕大部分情況下,訊息總是有一個目標視窗的,因此,訊息也絕大部分是被某個視窗所處理的。處理訊息的地方,就是這個視窗的回撥函式。

視窗的回撥函式,之所以被稱作“回撥”,就是因為這個函式一般並不是由使用者(程式設計師)主動呼叫它的,而是系統認為在恰當的時候對它進行呼叫。那麼,這個“恰當的時候”是什麼時候呢?根據訊息產生的方式,“恰當的時候”也有兩個時機。

第一個時機是,DispatchMessage 函式被呼叫時,另一個時機是 SendMessage 函式被呼叫時。

我們正常情況下以系統處理對一個頂級視窗的關閉按鈕的滑鼠左鍵點選事件為例來說明。

這個點選事件完成的標誌性訊息是 WM_NCLBUTTONUP,表示在一個視窗的非客戶區的滑鼠左鍵釋放動作,另外,這個滑鼠訊息的其他資料中會表明,發生這個動作的位置是在關閉按鈕上(HTCLOSE)。這是一個滑鼠輸入事件,從前文可以知道,它會被系統投遞到訊息佇列中。

於是,在訊息迴圈中 GetMessage 的某次執行結束後,這個訊息被取到了 MSG 結構裡。從文章開頭的訊息迴圈程式碼可知,這個訊息接下來會被 TranslateMessage 函式做必要的(事實上是“可能的”)翻譯,然後交給 DispatchMessage 來全權處理。

DispatchMessage 拿到了 MSG 結構,開始自己的一套辦事流程。

首先,檢查訊息指定的目標視窗控制代碼。看系統內(實際上是本執行緒內)是不是確實存在這樣一個視窗,如果沒有,那說明這個訊息已經不會有需要對它負責的人選了,那麼這個訊息就會被丟棄。

如果有,它就會直接呼叫目標視窗的回撥函式。終於看到,我們寫的回撥函數出場了,這就是“恰當的時機”之一。當然,為了敘述清晰,此處省略了系統做的一些其他處理。

這樣,對於系統來說,一條投遞訊息就處理完成,轉而繼續 GetMessage。

不過對於我們上面的例子,事情還沒有完。

我們都清楚,對於 WM_NCLBUTTONUP 這樣一條訊息,通常我們是無暇去做額外處理的(正事還忙不過來呢……)。所以,我們一般都會把它扔到那個著名的垃圾堆裡,沒錯,DefWindowProc。儘管如此,我們還是可以看出,DefWindowProc 其實已經成了我們的回撥函式的一個組成部分,唯一的差別在,這個函式不是我們自己寫的而已。

DefWindowProc 對這個訊息的處理也是相當輕鬆,它基本上沒有做什麼實質性的事情,而是生成了另外一個訊息,WM_SYSCOMMAND,同時在 wParam 裡指定為 SC_CLOSE。這一次,訊息沒有被投遞到訊息佇列裡,而是直接 Send 出來的。

於是,SendMessage 的艱難歷程開始。

第一步,SendMessage 的方向和 DispatchMessage 幾乎一模一樣,檢查控制代碼。

第二步,事情就來了,它需要檢查目標視窗和自己在不在一個執行緒內。如果在,那就比較好辦,按照 DispatchMessage 趟出來的老路走:呼叫目標視窗的回撥函式。這,就是“恰當的時機”之二。

可是要是不在一個執行緒內,那就麻煩了。道理很簡單,別的執行緒有自己的執行軌跡,沒有辦法去讓它立即就來處理這個訊息。

現在,SendMessage 該怎麼處理手裡的這個燙手山芋呢?(作者注:寫到此處時,很有寫上“欲知後事如何,且聽下回分解”的衝動)

微軟的架構師做了個非常聰明的選擇:不干涉其他執行緒的內政。我不會生拉硬拽讓你來處理我的訊息,我會把訊息投遞給你(這個投遞是內部操作,從外面看,這條訊息應該一直被認為是傳送過去的),然後 —— 我等著。

這下,球踢到了目標執行緒那邊。目標執行緒一點也不含糊,既然訊息來到了我的佇列裡,那我的 GetMessage 會按照既定的流程走,不過,和上文 WM_NCLBUTTONUP 的經歷有所不同。鑑於這條訊息是外來客,而且是 Send 方式,於是它以優先於執行緒內部的其他訊息進行處理(畢竟友邦在等著啊),處理完畢之後,把結果返回給訊息的源執行緒。可以參見下文中對 GetMessage 函式的敘述。

在我們的現在討論的這個例子裡,由於 SendMessage(WM_SYSCOMMAND) 是屬於本執行緒內的,所以就會遞迴呼叫回視窗的回撥函式裡。此後的處理,還是另外的幾個訊息被衍生出來,如 WM_CLOSE 和 WM_DESTROY。這個例子僅僅出於概念性的展示,而不是完全精確可靠的,而且,在 Windows Mobile 上,乾脆就沒有非客戶區的概念。

這就是系統內所有訊息的處理方式。

不過稍等,PostThreadMessage 投遞到訊息佇列裡的訊息怎麼辦?答案是:你自己看著辦。最好的處理位置,就是在訊息迴圈中的 TranslateMessage 呼叫之前。

另外一個需要稍做註解的問題是訊息的返回值問題,這個問題有些微妙。對於大多數的訊息,返回值都沒有什麼意義。對於另外的一些訊息,返回值意義重大。我相信有很多人對 WM_ERASEBKGND 訊息的返回值會有印象,該訊息的返回值直接影響到系統是不是要進行預設的繪製視窗背景操作。所以,處理完一條訊息究竟應該返回什麼,查一下文件會更穩妥一些。

這才算是功德圓滿了。

4.2.3. 訊息的優先順序

上一節中其實已經暗示了這一點,來自於其他執行緒的傳送的訊息優先順序會高一點點。

不過還需要注意,還有那麼幾個優先順序比正常的訊息低一點點的。它們是:WM_PAINT、WM_TIMER、WM_QUIT。只有在佇列中沒有其他訊息的時候,這幾個訊息才會被處理,多個 WM_PAINT 訊息還會被合併以提高效率(內幕揭示:WM_PAINT 其實也是一個標誌位,所以看上去是被“合併了”)。

其他所有訊息則以先進先出(FIFO)的方式被處理。

4.2.4. 沒有處理的訊息呢?

有人會問出這個問題的。事實上,這差不多就是一個偽命題,基本不存在沒有處理的訊息。從 4.2.2 節的敘述也可以看出,訊息總會流到某一個處理分支裡去。

那麼,我本人傾向於提問者在問這樣一個問題:如果視窗回撥函式沒有處理某個訊息,那這個訊息最終怎麼樣了?其實這還是取決於回撥函式實現者的意志。如果你只是簡單地返回,那事實上也是進行了處理,只不過,處理的方式是“什麼都沒做”而已;如果你把訊息傳遞給 DefWindowProc,那麼它會處理自己感興趣的若干訊息,對於別的訊息,它也一概不管,直接返回。

4.3. 訊息死鎖

假設有執行緒 A 和 B, 現在有以下步驟:

1) 執行緒 A SendMessage 給執行緒 B,A 等待訊息線上程 B 中處理後返回;

2) 執行緒 B 收到了執行緒 A 發來的訊息,並進行處理,在處理過程中,B 也向執行緒 A SendMessage,然後等待從 A 返回。

此時執行緒 A 正等待從執行緒 B 返回,無法處理 B 發來的訊息,從而導致了執行緒 A 和 B 相互等待,形成死鎖。

以此類推,多個執行緒也可以形成環形死鎖。

可以使用 SendNotifyMessage 或 SendMessageTimeout 來避免出現此類死鎖。

(作者注:對兩個執行緒互相 SendMessage 曾經專門寫程式進行過驗證,結果卻沒有死鎖,不知道是不是新一些的 Windows 系統作了特殊的處理。請大家自行驗證。)

4.4. 模態(Modal)

這個詞彙曾給我帶來極大的困惑,我曾經做過不少的努力,想弄清楚為什麼當初系統的構建者使用“模態”這個詞彙來表達這樣一種情景,但是最後失敗了。我不得不接受這個詞,並運用它。直到數天前,我找到了一個對模態的簡要介紹,如果有興趣,各位可以自己去看:http://www.usabilityfirst.com/glossary/main.cgi?function=display_term&term_id=320。(我曾做過的另外一個努力是想知道為什麼 Handle 會被翻譯為“控制代碼”,或者,是誰首先這樣翻譯的,迄今無解)。Windows 中的模態有好幾個場景,比較典型的有:

顯示了一個對話方塊
顯示出一個選單
操作滾動條
移動視窗
改變視窗大小

把我的體會歸納起來,那就是:如果進入了一個模態場景,那麼,除了這個模態本身的明確目標,其餘操作被一概禁止。概念上可以理解為,模態,是一種獨佔模式、一種強制模式,一種霸道模式。

在 Windows 裡,模態的實現其實很簡單,只不過就是包含了自己的訊息迴圈而已,說穿了毫無懸念可言,但是如果不明白這個內幕的話,就會覺得很神祕。那麼,根據此結論,我們就可以做一些有趣(或者有意義)的事情了,看一下以下程式碼,預測一下 TestModal 的執行結果:

void CALLBACK RequestQuit(HWNDhwnd, UINT uMsg, UINT idEvent, DWORD dwTime);
void TestModal()
{
    UINT uTimerId =SetTimer(NULL, 66, 1000, RequestQuit);
    MessageBox(NULL, NULL, NULL, MB_OK);
    KillTimer(NULL, uTimerId);
}

void CALLBACK RequestQuit(HWND hwnd, UINT uMsg, UINT idEvent, DWORD dwTime)
{
    PostMessage(NULL, WM_QUIT, 0, 0);
}

答案見本大節末尾。

需要提醒的是,模態是使用者介面裡相當重要而普遍的一個概念,不僅存在於 Windows 環境下,也存在於其他的使用者介面系統中,例如 Symbian。

4.5. 與訊息處理有關的鉤子(Hook)

很多人都或多或少地聽說過或者接觸過鉤子。鉤子在處理事務的正常流程之外,額外給予了我們一種監聽或者控制的方式(注意:在 Windows Mobile 系統下,鉤子並不被正式支援)。

TODO: 細化,不過由於這個內容針對桌面系統更多,所以暫時可以略過

4.6. 所謂的反射(Reflection)

上文也已經提到,反射通常會在對 Windows API 的封裝類或者類庫中出現,這是由於Windows SDK 的 API 是以 C 的風格暴露給使用者的,與 C++ 語言的主要用類程式設計的風格有一些需要齧合的地方。

舉例來說,一個Button,在 SDK 中是一個已經定型的控制元件,基本上實現了自包容,要擴充套件它的功能的話(例如,繪製不同的外觀),系統把介面(廣義上的介面,即一種互動上的契約)制定為發給 Button 的屬主(通常就是父視窗)的兩條訊息(WM_MEASUREITEM 和 WM_DRAWITEM)。其道理在於,使用 Button 控制元件的父視窗,往往是使用者自己實現的,處理起來更方便,而不需要對 Button 自身做什麼手腳。

但是,這種互動方式在 C++ 的世界裡是相當忌諱的。C++ 的自包容單位是物件,那麼一個 Button 物件的封裝類,假定是 CButton,不能自己處理自己的繪製問題,這是不太符合法則的(儘管不是不可以)。

為了消除這一不和諧音,就有人提出了反射機制。其核心就在於,對於本該子控制元件自己處理的事件所對應的訊息(如前面的 WM_DRAWITEM),父視窗即使收到,也不進行直接處理,而是把這個訊息重新發回給子控制元件本身。

這樣帶來一個問題,當 Button 收到一個 WM_DRAWITEM 訊息時,弄不清楚究竟是自己的子視窗發來的(雖說往 Button 上建立子視窗不常見,但不是不可以),還是父視窗把原本是自己的訊息反射回來了。所以,最後微軟給出一個解決辦法,就是反射訊息的時候,把訊息的值上加一個固定的附加值,這個值就是 OCM__BASE。儘管最初只是微軟自己在這樣做,這個值也完全可以各取各的,但是後來別的類/類庫的編制者幾乎都無一例外地和微軟保持了一致。

當控制元件收到訊息之後,先把這個附加值減掉,就可以知道是哪一條訊息被反射回來了,然後再作相應的處理。

4.4 節小測試的答案:一個訊息框顯示大概 1 秒鐘的時間,然後自動消失。有的人根據這一表現,寫出了自己的超時候自動關閉的訊息框。如果各位有興趣,可以自己嘗試也實現一下。(提示:需要考慮一下使用者先於定時器觸發就手動關閉了訊息框的情況)

5. Windows 的訊息本質

一個特殊的事件同步機制,使用多種常規執行緒間同步機制實現。

6. Windows 的訊息操縱

注意:以下討論中用淺綠色標註的函式,表示在 WindowsMobile 平臺上是沒有的。

SendMessage

PostMessage

在使用訊息的過程中,這兩個函式的使用率是最高的。初學者有時會搞不清楚這兩個傳送訊息的函式的使用場景,容易誤用。所以放在這裡一起說。其實上面已經對 SendMessage 做了很多的介紹,所以在這兒的重點會放在 PostMessage 上。相較 SendMessage 而言,PostMessage 的工作要輕鬆許多,只要找到知道那個的視窗控制代碼所在的執行緒,把訊息放到該執行緒的訊息佇列裡就可以了,完全不理會這條訊息最終的命運,是不是被正確處理了。

這一點,從 PostMessage 和 SendMessage 的返回值的不同也有體現。PostMessage 函式的返回值是 BOOL 型別,體現的是投遞操作是否成功。投遞操作是有可能失敗的,儘管我們不願意同時也確實很少看到。例如,目標執行緒的訊息佇列已經滿(在 16 位時代出現概率較高),或者更糟糕,目標執行緒根本就沒有訊息佇列。

當然,PostMessage 也要檢查視窗控制代碼的合法性,不過和 SendMessage 不同的一點是,它允許視窗控制代碼是 NULL。在此情況下,對它的呼叫就等價於呼叫 PostThreadMessage 向自身所線上程投遞一條訊息。

從上面的描述可以很容易地看出,PostMessage 和 SendMessage 的本質區別在於前者發出的訊息是非同步處理的,而後者發出的訊息是同步處理的。理解這一點非常重要。

從上面的這個結果推演,還可以得到另外一個有時會很有用的推論。在本執行緒之內,如果你在處理某個視窗訊息的時候,希望在處理之後開展另一項以此訊息為前提的工作,那麼可以向本視窗 Post 一條訊息,來作為該後續工作的觸發機制。


GetMessage

檢查執行緒的訊息佇列,如果有訊息就取出該訊息到一個傳入的 MSG 結構中並返回,沒有訊息,就等待。等待時執行緒處於休眠狀態,CPU 被分配給系統內的其他執行緒使用。

需要注意的是,由其它執行緒 Send 過來的訊息,會在這裡就地處理(即呼叫相應的視窗回撥函式),而不會返回給呼叫者。

DispatchMessage

這個訊息的來龍去脈在上文中已經有較為詳細的敘述,故此略去。

TranslateMessage(TranslateAccelerator

這個函式在本質上與訊息機制的關係不大,絕大多數的訊息迴圈中都出現它的身影是因為絕大多數的程式設計師都不知道這個函式真正是幹什麼的,僅僅是出於慣例或者初學時教科書上給出的範例。這個函式的作用主要和輸入有關,它會把 WM_KEYDOWN 和 WM_KEYUP 這樣的訊息恰當地、適時地翻譯出新的訊息來,如 WM_CHAR。如果你確信某個執行緒根本不會有使用者輸入方面的需求,基本上可以安全地將之從迴圈中移除。

可以和它相提並論的就是列出的 TranslateAccelerator 函式,這個函式會把使用者輸入根據指定的加速鍵(Accelerator)表翻譯為適當的命令訊息。

PeekMessage

窺探執行緒的訊息佇列。無論佇列中有沒有訊息,這個函式都立即返回。它的引數列表與 GetMessage 基本一致,只是多了一個標誌引數。這個標誌引數指定了如果佇列中如果有訊息的話,PeekMessage 的行為。如果該標誌中含有 PM_REMOVE,則 PeekMessage 會把新訊息返回到 MSG 結構中,正如 GetMessage 的行為那樣。如果標誌中指定了 PM_NOREMOVE,則不會取出任何訊息。

WaitMessage

這個函式的作用是等待一條訊息的到來。等待期間執行緒處於休眠狀態,一旦有新訊息到來,則立即返回。

瞭解了 PeekMessage 和 WaitMessage 之後,理論上,我們可以寫出自己的 GetMessage 了。

SendNotifyMessage

這個函式很有意思,它的行為屬於看人下菜碟型。如果目標執行緒就是自身所處執行緒,那麼它就是 SendMessage;而一旦發現目標執行緒是其他執行緒,那它就類似於 PostMessage,不等待目標視窗處理完成。不過,僅僅是類似,因為它發出的訊息仍然會被目標執行緒認為是 Send 過來的。

SendMessageTimeout

這個函式可以說是 SendMessage 函式家族(相對 PostMessage 而言)之中最強大的函式。它在標準的 SendMessage 函式的功能前提下,加入了許多額外的控制選項以及一個超時設定。例如,它可以指定,如果發現目標視窗已經失去響應的話,那麼就立即返回;也可以指定如果目標視窗的響應時間超過了指定的超時時限的話也返回,而不是無限等待下去。而且我們知道,SendMessage 是會固執地等待下去的。(內幕揭示:SendMessage 其實就是對 SendMessageTimeout 的一個淺封裝)

SendMessageCallback

與 SendMessageTimeout 不同,這個函式在另外一個方向上對標準的 SendMessage 進行了擴充套件。它的行為與SendNotifyMessage 類似,只不過允許在對方處理完訊息之後,指定一個本執行緒內的後續處理函式。仔細觀察可以發現,SendNotifyMessage 其實是本函式的一個特例。

對這個函式的使用場景較少,實際上,作者幾乎從來沒有見到必須使用它的情況。網上有一些對此函式的討論和測試程式碼,但很少有實用價值。(恐怕這也是 Windows Mobile 沒有實現此函式的原因之一。)

PostQuitMessage

這個函式的名字具有迷惑性。事實上,它本身並不會投遞任何訊息,而是偷偷在系統內部置了一個標誌,當呼叫 GetMessage 時會檢測此標誌位。若此標誌位被置位,而且佇列中已經沒有別的符合條件的投遞訊息,則 GetMessage 返回 FALSE,用以終止訊息迴圈。

不過,有人會有這樣的疑惑。我們知道,PostMessage 當視窗控制代碼為 NULL 的時候,就相當於 PostThreadMessage(GetCurrentThreadId(), …),那麼,為什麼不用 PostMessage(NULL, WM_QUIT, 0, 0),而要引入這麼一個單獨的 API 呢?有的人給出的原因是,這個 API 出現在 Windows 的 16 位時代,當時還沒有執行緒的概念。這個答案仔細推敲的話,其實似是而非,因為完全可以把程序的執行看作是一個執行緒。真正的原因,可能從前文能得到一些思考線索,尤其注意“佇列中已經沒有別的符合條件的投遞訊息”這個敘述。

PostThreadMessage

跨執行緒投遞訊息。我們知道,訊息佇列是屬於執行緒的,所以,可以不指定目標視窗而只指定目標執行緒就投遞訊息。投遞到目標執行緒的訊息通常會被 GetMessage 取出,但是,由於沒有指定目標視窗,所以不會被派發到任何一個視窗回撥函式中。

請注意上文中的通常二字。這是因為在一般的情況下,我們是按照 GetMessage(&msg, NULL, 0, 0) 這樣的形式對 GetMessage 進行呼叫的,但是,第二個引數是一個視窗控制代碼,如果指定了一個合法的視窗控制代碼,那麼 GetMessage 就只會取出與該視窗有關的投遞訊息。如果這樣的呼叫放線上程的主訊息迴圈中,就可能會造成訊息積壓(這和你在本執行緒中究竟建立了多少個視窗有關)。所幸的是,迄今我還沒有見到過有誰這樣使用 GetMessage。

BroadcastSystemMessage[Ex]

我們一般所接觸到的訊息都是傳送給視窗的,其實, 訊息的接收者可以是多種多樣的,它可以是應用程式(application)、可安裝驅動程式(installable driver)、網路驅動程式(network driver)、系統級裝置驅動程式(system-leveldevice driver)等,用 BroadcastSystemMessage 這個 API 可以對以上系統元件傳送訊息。

InSendMessage[Ex]

這個函式用於在處理某條訊息時,檢查訊息是不是來自於其他執行緒的傳送操作。它的使用場景也極其有限,除非你確實計劃限制某些訊息的來源和產生方式。

ReplyMessage

這個函式在 MSDN 中的解釋非常簡單,只有寥寥數語,幾乎到了模糊不清的地步。從示例程式碼段來推測,其作用大概是:訊息的接收執行緒(目標執行緒)在處理過程中可以通過呼叫此函式使得訊息的傳送執行緒(源執行緒)結束等待狀態繼續執行。

根據微軟的文件,其官方建議是:在處理每個有可能來自於其他執行緒的訊息的時候,如果某一步驟的處理會呼叫到導致執行緒移交控制的函式(原文如此:any function that causes the thread to yield control),都應該先呼叫 InSendMessage 類屬的函式進行判斷,如果返回 TRUE,則要立即使用 ReplyMessage 答覆訊息的源執行緒。

“會導致執行緒移交控制的函式”,MSDN 給出的例子是 DialogBox,這使得我做出自己的推測,這樣的函式,至少包括會導致進入某種模態場景的函式。

至於“有可能來自於其他執行緒的訊息”,在 Windows 世界裡的現實狀況是,幾乎任何一個訊息都會來自於其他執行緒。

我多年以來的觀察可以斷定,現實中有無數沒有進行以上流程判斷的程式碼都在執行,而且也幾乎沒有暴露出什麼嚴重的不良後果。這使得我有理由猜測,微軟也許已經把對此情況的處理隱含到了系統內部。更何況,Windows Mobile 中根本就沒有 ReplyMessage 這個 API。

GetMessagePos

GetMessageTime

這兩個函式用於訪問當前處理的訊息的另外兩個資訊,對應於 MSG 結構裡的相應域。它們存在的原因是因為視窗回撥/訊息處理函式一般都不會傳遞這兩個資料。

MsgWaitForMultipleObjects[Ex]

這是一個在講到訊息相關的內容時,十有八九會被人遺忘的 API。它屬於傳統的 ITC、IPC 和 Windows 特有的訊息機制的交叉地帶。不過,在 Windows 平臺上,如果還沒有了解並掌握這個函式,那一定不能稱其為專家。

這個函式揭示了以下平時不太為人所注意的細節:

1、訊息和核心物件,有千絲萬縷的聯絡;

2、訊息和核心物件可以按照相似的方式去處理。

如果說,SendMessageTimeout 是 Windows 平臺下最強大的傳送訊息的機制,那麼,MsgWaitForMultipleObjects[Ex] 就是最強大等待機制,它是 WaitMessage 和 WaitFor… 函式族的集大成者。根據我們上面使用 WaitMessage 和 PeekMessage 結合使用可以取代 GetMessage 的論斷,我們也可以這樣說,MsgWaitForMultipleObjects[Ex] 是最強大的訊息迴圈發動機。

仔細描述此函式會超出單純的訊息機制範疇,所以把深入學習它的工作遺留給各位自己去實踐。

7. Windows 的訊息辨析

7.1. SendMessage 和 PostMessage 的區別

請考慮有面試考官問及此問題時你如何組織回答。

7.2. SendMessage 傳送的訊息不進入訊息佇列嗎

提示:請考慮跨執行緒的情況。

這個說法不完全正確。當 SendMessage 傳送的訊息跨越執行緒邊界時,訊息其實被加入到了目標執行緒的訊息佇列裡。不過,線上程佇列裡,別的執行緒 Send 過來的訊息會被優先處理。

7.3. PostMessage(WM_QUIT) 和 PostQuitMessage() 的區別,可能會產生怎樣的差異化執行效果

提示:請考慮發生以上某個呼叫時,訊息佇列裡不為空的情況。

7.4. 文章開頭的經典訊息迴圈正確麼?

提示:請注意 GetMessage 的返回值。

曾經有很長一段時間,連微軟的例子也這樣寫。但是,這樣寫其實是不對的。原因很簡單,GetMessage 不僅僅是取到訊息返回 TRUE,取不到(遇到 WM_QUIT 訊息)返回 FALSE 這麼單純,它還會出錯。出錯時返回 -1。這就了能使得經典迴圈在 GetMessage 發生錯誤時變成死迴圈。微軟的建議是,當 GetMessage 返回 -1 時,跳出迴圈,結束程式。

注:本文乃是數年前的培訓講義,文中有某處不完整,迄今未補,讀者自察之。