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位代碼中混雜有大量的從以前的Windows3.x上移植過來的16位代碼。架構
此時間稍後,另外一支潛力股的關鍵進化過程結束,Windows NT 4.0隆重登場,這個分支的操做系統是全32位的,成爲了 Windows 95 系列的掘墓者,也是咱們如今所使用幾乎全部的 Windows 桌面系統(Windows2000/XP/2003/Vista/2008)的前輩。可是,這個版本因爲對系統硬件的要求甚高(在當時),因此沒有引發普通用戶的普遍關注。app
下一個里程碑就是Windows 2000了,微軟實現了Windows9x/Me分支和Windows NT分支的合併。緊接着,Windows XP 現身。從有關消息方面來考察,Windows2000 作了微小的改進,在此以前,咱們在不少狀況下須要建立一個正常的、隱藏的、完整的窗口來處理消息,而 Windows 2000 引入了一種特殊類型的窗口用於此類需求。道理上來說,應該會減小一些資源佔用。異步
此後通過五六年的時間,Windows Vista誕生。事實上,從 Windows2000 開始,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 消息,此消息一般由公用控件(CommonControls)發出,表示一些事件發生了,須要處理。反射消息,通常用於對 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 位的,其名字的意思是 wordparameter,LPARAM 是 32 位的,其名字的意思是 longparameter。
4. Windows 的消息機制
4.1. 消息隊列
說到消息機制,可能連最初級的 Windows 程序員都會對消息隊列(MessageQueue)這個名詞耳熟(不過不見得能詳)。對於這樣一個基本概念,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)、網絡驅動程序(networkdriver)、系統級設備驅動程序(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 平臺上,若是尚未了解並掌握這個函數,那必定不能稱其爲專家。
這個函數揭示瞭如下平時不太爲人所注意的細節:
一、 消息和內核對象,有千絲萬縷的聯繫
二、 消息和內核對象能夠按照類似的方式去處理
若是說,SendMessageTimeout 是 Windows 平臺下最強大的發送消息的機制,那麼,MsgWaitForMultipleObjects[Ex] 就是最強大等待機制,它是 WaitMessage 和 WaitFor… 函數族的集大成者。根據咱們上面使用 WaitMessage 和 PeekMessage 結合使用能夠取代 GetMessage 的論斷,咱們也能夠這樣說,MsgWaitForMultipleObjects[Ex]是最強大的消息循環發動機。
仔細描述此函數會超出單純的消息機制範疇,因此把深刻學習它的工做遺留給各位本身去實踐。
7. Windows 的消息辨析
7.1. SendMessage和PostMessage的區別
請考慮有面試考官問及此問題時你如何組織回答。J
7.2. SendMessage發送的消息不進入消息隊列嗎
提示:請考慮跨線程的狀況。
這個說法不徹底正確。當SendMessage發送的消息跨越線程邊界時,消息其實被加入到了目標線程的消息隊列裏。不過,在線程隊列裏,別的線程Send過來的消息會被優先處理。
7.3. PostMessage(WM_QUIT)和PostQuitMessage()的區別,可能會產生怎樣的差別化執行效果
提示:請考慮發生以上某個調用時,消息隊列裏不爲空的狀況。
7.4. 文章開頭的經典消息循環正確麼?
提示:請注意 GetMessage 的返回值。
曾經有很長一段時間,連微軟的例子也這樣寫。可是,這樣寫實際上是不對的。緣由很簡單,GetMessage不只僅是取道消息返回 TRUE,取不到(遇到WM_QUIT 消息)返回FALSE這麼單純,它還會出錯。出錯時返回 -1。這就了能使得經典循環在GetMessage發生錯誤時變成死循環。微軟的建議是,當GetMessage返回 -1 時,跳出循環,結束程序。