1. 程式人生 > >Windows訊息機制『經典』

Windows訊息機制『經典』

Windows訊息機制【轉】  


2010-03-06 15:17:47|  分類: c/c++/c#語言相關 |字號 訂閱
原文地址:
http://blog.csdn.net/recle/archive/2008/11/08/3256614.aspx


(經修正的)原文
斜體是修正後的文字。對於我自己新增的文字,也以斜體標識出。


Windows的應用程式一般包含視窗(Window),它主要為使用者提供一種視覺化的互動方式,視窗是總是在某個執行緒(Thread)內建立的。 Windows系統通過訊息機制來管理互動,訊息(Message)被髮送,儲存,處理,一個執行緒會維護自己的一套訊息佇列(Message Queue),以保持執行緒間的獨佔性。佇列的特點無非是先進先出,這種機制可以實現一種非同步的需求響應過程。


PS 常見的錯誤的理解:
1) 每個視窗有自己的訊息佇列 (我加的)




訊息的是什麼樣子的?


訊息由一個叫MSG的結構體定義,包括視窗控制代碼(HWND),訊息ID(UINT),引數(WPARAM, LPARAM)等等:


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


訊息ID是訊息的型別識別符號,由系統或應用程式定義,訊息ID為訊息劃分了型別。同時,也可以看出訊息是對應於特定的視窗(視窗控制代碼)的。(此話並不完全 對,其實我們除了可以向某個視窗投遞訊息,也可以向某個執行緒投遞訊息,這個時候,訊息是沒有對應的視窗控制代碼的,控制代碼為空,要處理此類訊息,必須在 GetMessage或PostMessage之後,判斷視窗控制代碼是否為空,如果為空,就要處理該執行緒訊息,不必再往下 DispatchMessage()派發訊息,因為派發訊息函式一看到訊息視窗控制代碼為空,直接丟棄了該訊息,不往下派發,因為沒有某個視窗過程處理該消 息,所以沒必要派發【參考文獻 4】)


訊息是如何分類的?其字首都代表什麼含義?


訊息ID只是一個整數,Windows系統預定義了很多訊息ID,以不同的字首來劃分,比如WM_*,CB_*等等。
具體見下表:


Prefix Message category
ABM Application desktop toolbar
BM Button control
CB Combo box control
CBEM Extended combo box control
CDM Common dialog box
DBT Device
DL Drag list box
DM Default push button control
DTM Date and time picker control
EM Edit control
HDM Header control
HKM Hot key control
IPM IP address control
LB List box control
LVM List view control
MCM Month calendar control
PBM Progress bar
PGM Pager control
PSM Property sheet
RB Rebar control
SB Status bar window
SBM Scroll bar control
STM Static control
TB Toolbar
TBM Trackbar
TCM Tab control
TTM Tooltip control
TVM Tree-view control
UDM Up-down control
WM General window


應用程式可以定義自己的訊息,其取值範圍必須大於WM_USER。


如何通過訊息傳遞任何引數?


Windows系統的訊息機制都包含2個長整型的引數:WPARAM, LPARAM,可以存放指標,也就是說可以指向任何內容了。
傳遞的內容因訊息各異,訊息處理函式會根據訊息的型別進行特別的處理,它知道傳遞的引數是什麼含義。


訊息線上程內、執行緒間傳遞時,由於在同一個地址空間中,指標的值是有效的。但是跨程序的情況就不能直接使用指標了,所以Windows系統提供了 WM_SETTEXT, WM_GETTEXT, WM_COPYDATA等訊息,用來特殊處理,指標的內容會被放到一個臨時的記憶體對映檔案(Memory-mapped File)裡面,通過它實現執行緒間的共享資料。




訊息佇列和執行緒的關係是什麼?訊息佇列的結構是什麼樣子的?


Windows系統本身會維護一個唯一的訊息佇列,以便於傳送給各個執行緒,這是系統內部的實現方式。
而對於執行緒來說,每個執行緒可以擁有自己的訊息佇列,它和執行緒一一對應。線上程剛建立時,訊息佇列並不會被建立,而是當GDI的函式呼叫發生 時,Windows系統才認為有必要為執行緒建立訊息佇列。
訊息佇列包含在一個叫THREADINFO的結構中,有四個佇列:


Sent Message Queue
Posted Message Queue
Visualized Input Queue
Reply Message Queue


之所以維護多個佇列,是因為不同訊息的處理方式和處理順序是不同的。


執行緒和視窗是一一對應的嗎?如果想要有兩個不同的視窗對訊息作出不同反應,但是他們屬於同一個執行緒,可能嗎?


視窗由執行緒建立,一個執行緒可以建立多個視窗。視窗可由CreateWindow()函式建立,但前提是需要提供一個已註冊的視窗類(Window Class),每一個視窗類在註冊時需要指定一個視窗處理函式(Window Procedure),這個函式是一個回撥函式,就是用來處理訊息的。而由一個執行緒來建立對應於不同的視窗類的視窗是可以的。
由此可見,只要註冊多個視窗類,每個視窗都可以擁有自己的訊息處理函式,而同時,他們屬於同一個執行緒。




如何傳送訊息?


訊息的傳送終歸通過函式呼叫,比較常用的有PostMessage(),SendMessage(),另外還有一些Post*或Send*的函式。函式的 呼叫者即傳送訊息的人。
這二者有什麼不同呢?SendMessage()要求接收者立即處理訊息,等處理完畢後才返回。而PostMessage()將訊息傳送到接收者佇列中以 後,立即返回,呼叫者不知道訊息的處理情況。


他們的的原型如下:




LRESULT SendMessage(
HWND hwnd,
UINT uMsg,
WPARAM wParam,
LPARAM lParam);


LRESULT PostMessage(
HWND hwnd,
UINT uMsg,
WPARAM wParam,
LPARAM lParam);
SendMessage()要求立即處理,所以它會直接呼叫視窗的訊息處理函式(Window Procedure),完成後返回處理結果。
但這僅限於執行緒內的情況,跨執行緒時它調不到處理函式,只能把訊息傳送到接收執行緒的佇列Sent Message Queue裡。如果接收執行緒正在處理別的訊息,那麼它不會被打斷,直到它主動去獲取佇列裡的下一條訊息時,它會拿到這一條訊息,並開始處理,完成後他會通 知傳送執行緒結果(猜測是通過ReplyMessage()函式)。
在接收執行緒處理的過程中,傳送執行緒會掛起等待SendMessage()返回。但是如果這時有其他執行緒發訊息給這個傳送執行緒,它可以響應,但僅限於非佇列 型(Non-queued)訊息。


這種機制可能引起死鎖,所以有其他函式比如SendMessageTimeout(), SendMessageCallback()等函式來避免這種情況。


PostMessage()並不需要同步,所以比較簡單,它只是負責把訊息傳送到佇列裡面,然後馬上返回傳送者,之後訊息的處理則再受控制。


訊息可以不進佇列嗎?什麼訊息不進佇列?


可以。實際上MSDN把訊息分為佇列型(Queued Message)和非佇列型(Non-queued Message),這只是不同的路由方式,但最終都會由訊息處理函式來處理。
佇列型訊息包括硬體的輸入(WM_KEY*等)、WM_TIMER訊息、WM_PAINT訊息等;非佇列型的一些例子有WM_SETFOCUS, WM_ACTIVE, WM_SETCURSOR等,它們被直接傳送給處理函式。


其實,按照MSDN的說法和訊息的路由過程可以理解為,Posted Message Queue裡的訊息是真正的佇列型訊息,而通過SendMessage()傳送到訊息,即使它進入了Sent Message Queue,由於SendMessage要求的同步處理,這些訊息也應該算非佇列型訊息。也許,Windows系統會特殊處理,使訊息強行繞過佇列。




誰來發送訊息?硬體輸入是如何被響應的?


訊息可以由Windows系統傳送,也可以由應用程式本身;可以向執行緒內傳送,也可以誇執行緒。主要是看傳送函式的呼叫者。


對於硬體訊息,Windows系統啟動時會執行一個叫Raw Input Thread的執行緒,簡稱RIT。這個執行緒負責處理System Hardware Input Queue(SHIQ)裡面的訊息,這些訊息由硬體驅動傳送。RIT負責把SHIQ裡的訊息分發到執行緒的訊息佇列裡面,那RIT是如何知道該發給誰呢?如 果是滑鼠事件,那就看滑鼠指標所指的視窗屬於哪個執行緒,如果是鍵盤那就看哪個視窗當前是啟用的。一些特殊的按鍵會有所不同,比如 Alt+Tab,Ctrl+Alt+Del等,RIT能保證他們不受當前執行緒的影響而死鎖。RIT只能同時和一個執行緒關聯起來。
有可能,Windows系統還維護了除SHIQ外地其他佇列,分發給執行緒的佇列,或者直接發給視窗的處理函式。




訊息迴圈是什麼樣子?執行緒何時掛起?何時醒來?


想象一個通常的Windows應用程式啟動後,會顯示一個視窗,它在等待使用者的操作,並作出反應。
它其實是在一個不斷等待訊息的迴圈中,這個迴圈會不斷去獲取訊息並作出處理,當沒有訊息的時候執行緒會掛起進入等待狀態。這就是通常所說的訊息迴圈。


一個典型的訊息迴圈如下所示(注意這裡沒有處理GetMessage出錯的情況):




while(GetMessage(&msg, NULL, 0, 0 ) != FALSE)
{
TranslateMessage(&msg);
DispatchMessage(&msg);
}
這裡GetMessage()從佇列裡取出一條訊息,經過TranslateMessage(),主要是將虛擬按鍵訊息(WM_KEYDOWN等)翻譯成 字元訊息(WM_CHAR等)。
DispatchMessage()將呼叫訊息處理函式。這裡有一個靈活性,訊息從佇列拿出之後,也可以不分發,進行一些別的特殊操作。


下面在看看GetMessage()的細節:




BOOL GetMessage(
LPMSG lpMsg,
HWND hWnd,
UINT wMsgFilterMin,
UINT wMsgFilterMax
);
GetMessage()會從佇列中取出訊息,填到MSG結構中通過引數返回。如果此時的訊息是WM_QUIT,也就標識執行緒需要結束,則 GetMessage()返回FALSE,那麼while迴圈會終止。返回TRUE表示取到其他訊息,可以繼續迴圈並執行裡面的內容。如果返回-1表示 GetMessage()出錯。


其他幾個引數是用來過濾訊息的,可以指定接收訊息的視窗,以及確定訊息的類型範圍。


這裡還需要提到一個概念是執行緒的Wake Flag,這是一個整型值,儲存在THREADINFO裡面和4個訊息佇列平級的位置。它的每一位(bit)代表一個開關,比如QS_QUIT, QS_SENDMESSAGE等等,這些開關根據不同的情況會被開啟或關閉。GetMessage()在處理的時候會依賴這些開關。


GetMessage()的處理流程如下:


1. 處理Sent Message Queue裡的訊息,這些訊息主要是由其他執行緒的SendMessage()傳送,因為他們不能直接呼叫本執行緒的處理函式,而本執行緒呼叫 SendMessage()時會直接呼叫處理函式。一旦呼叫GetMessage(),所有的Sent Message都會被處理掉,並且GetMessage()不會返回;


2. 處理Posted Message Queue裡的訊息,這裡拿到一個訊息後,GetMessage()將它拷貝到MSG結構中並返回TRUE。注意有三個訊息WM_QUIT, WM_PAINT, WM_TIMER會被特殊處理,他們總是放在佇列的最後面,直到沒有其他訊息的時候才被處理,連續的WM_PAINT訊息甚至會被合併成一個以提高效率。 從後面討論的這三個訊息的傳送方式可以看出,使用Send或Post訊息到佇列裡情況不多。


3. 處理QS_QUIT開關,這個開關由PostQuitMessage()函式設定,表示執行緒需要結束。這裡為什麼不用Send或Post一個 WM_QUIT訊息呢?據稱:一個原因是處理記憶體緊缺的特殊情況,在這種情況下Send和Post很可能失敗;其次是可以保證執行緒結束之前,所有Sent 和Posted訊息都得到了處理,這是因為要保證程式執行的正確性,或者資料丟失?不得而知。
如果QS_QUIT開啟,GetMessage()會填充一個WM_QUIT訊息並返回FALSE。


4. 處理Virtualized Input Queue裡的訊息,主要包括硬體輸入和系統內部訊息,並返回TRUE;


5. 再次處理Sent Message Queue,來自MSDN卻沒有解釋。難道在檢查2、3、4步驟的時候可能出現新的Sent Message?或者是要保證推後處理後面兩個訊息;


6. 處理QS_PAINT開關,這個開關只和執行緒擁有的視窗的有效性(Validated)有關,不受WM_PAINT的影響,當視窗無效需要重畫的時候這個 開關就會開啟。當QS_PAINT開啟的時候,GetMessage()會返回一個WM_PAINT訊息。處理QS_PAINT放在後面,因為重繪一般比 較慢,這樣有助於提高效率;


7. 處理QS_TIMER開關,和QS_PAINT類似,返回WM_TIMER訊息,之所以它放在QS_PAINT之後是因為其優先順序更低,如果Timer消 息要求重繪但優先順序又比Paint高,那麼Paint就沒有機會運行了。


如果GetMessage()中任何訊息可處理,GetMessage()不會返回,而是將執行緒掛起,也就不會佔用CPU時間了。
類似的WaitMessage()函式也是這個作用。


還有一個PeekMessage(),其原型為:




BOOL PeekMessage(
LPMSG lpMsg,
HWND hWnd,
UINT wMsgFilterMin,
UINT wMsgFilterMax,
UINT wRemoveMsg
);
它的處理方式和GetMessage()一樣,只是多了一個引數wRemoveMsg,可以指定是否移除佇列裡的訊息。最大的不同應該是,當沒有訊息可處 理時,PeekMessage()不是掛起等待訊息的到來,而是立即返回FALSE。


WM_DESTROY, WM_QUIT, WM_CLOSE訊息有什麼不同?


而其他兩個訊息是關於視窗的,WM_CLOSE會首先發送,一般情況程式接到該訊息後可以有機會詢問使用者是否確認關閉視窗,如果使用者確認後才呼叫 DestroyWindow()銷燬視窗,此時會發送WM_DESTROY訊息,這時視窗已經不顯示了,在處理WM_DESTROY訊息是可以傳送 PostQuitMessage()來設定QS_QUIT開關,WM_QUIT訊息會由GetMessage()函式返回,不過此時執行緒的訊息迴圈可能也 即將結束。


視窗內的訊息的路由是怎樣的?視窗和其控制元件的關係是什麼?


一個視窗(Window)可以有一個Parent屬性,對一個Parent視窗來說,屬於它的視窗被稱為子視窗(Child Window)。控制元件(Control)或對話方塊(Dialog)也是視窗,他們一般屬於某個父視窗。
所有的視窗都有自己的控制代碼(HWND),訊息被髮送時,這個控制代碼就已經被指定了。所以當子視窗收到一個訊息時,其父視窗不會也收到這個訊息,除非子視窗手 動的轉發。
關於更詳細的視窗和控制元件,會在另一篇中討論。


PS:
關係的訊息的一個很詳細的描述文章在最新新出的《windows程式設計啟示錄》裡面有。
作者模擬了 SendMessage(),PostMessage()PeekMessage(),GetMessage()的實現,相當於給出了原始碼,一目瞭然!




誰來處理訊息?訊息處理函式能傳送訊息麼?


由訊息處理函式(Window Procedure)來處理。訊息處理函式是一個回撥函式,其地址在註冊視窗類的時候註冊,只有在執行緒內才能呼叫。


其原型為:




typedef LRESULT (CALLBACK* WNDPROC)(HWND hWnd, UINT uMsg, WPARAM wParam, LPARAM lParam);
處理函式內部一般是一個switch-case結構,來針對不同的訊息型別進行處理。Windows系統還為所有視窗預定義了一個預設的處理函式 DefWindowProc(),它提供了最基本的訊息處理,一般在不需要特殊處理的時候(即在switch的default分支)會呼叫這個函式。
由同一個視窗類建立的一組視窗共享一個訊息處理函式,所以在編寫處理函式的時候要小心處理視窗例項的區域性變數。


處理函式裡可以傳送訊息,但是可以想象有可能出現迴圈。另外處理函式還常常被遞迴呼叫,所以要減少區域性變數的使用,以避免遞迴過深是棧溢位。


最後關於處理函式特化的問題將在另外的文章討論。




--------------------------------------------------
參考資料:
【1】Windows 遊戲程式設計大師技巧 (第一卷)[André LaMothe]
【2】Windows 核心程式設計 [Jeffery Richter]
【3】Win32 and COM Development: User Interface: Windows User Interface: Windowing [MSDN]


【4】windows程式設計啟示錄


轉自:http://blog.sina.com.cn/s/blog_4e0c21cc0100dqcq.html