|
Windows的應用程序一般包含窗口(Window),它主要為用戶提供一種可視化的交互方式,窗口是由線程(Thread)創(chuàng)建的。Windows系統(tǒng)通過消息機制來管理交互,消息(Message)被發(fā)送,保存,處理,一個線程會維護自己的一套消息隊列(Message Queue),以保持線程間的獨占性。隊列的特點無非是先進先出,這種機制可以實現(xiàn)一種異步的需求響應過程。
消息由一個叫MSG的結構體定義,包括窗口句柄(HWND),消息ID(UINT),參數(shù)(WPARAM, LPARAM)等等:
消息是如何分類的?其前綴都代表什么含義? 消息ID只是一個整數(shù),Windows系統(tǒng)預定義了很多消息ID,以不同的前綴來劃分,比如WM_*,CB_*等等。 Prefix Message category
如何通過消息傳遞任何參數(shù)? Windows系統(tǒng)的消息機制都包含2個長整型的參數(shù):WPARAM, LPARAM,可以存放指針,也就是說可以指向任何內容了。 消息在線程內傳遞時,由于在同一個地址空間中,指針的值是有效的。但是夸線程的情況就不能直接使用指針了,所以Windows系統(tǒng)提供了 WM_SETTEXT, WM_GETTEXT, WM_COPYDATA等消息,用來特殊處理,指針的內容會被放到一個臨時的內存映射文件(Memory-mapped File)里面,通過它實現(xiàn)線程間的共享數(shù)據(jù)。
Windows系統(tǒng)本身會維護一個唯一的消息隊列,以便于發(fā)送給各個線程,這是系統(tǒng)內部的實現(xiàn)方式。 Sent Message Queue 之所以維護多個隊列,是因為不同消息的處理方式和處理順序是不同的。 線程和窗口是一一對應的嗎?如果想要有兩個不同的窗口對消息作出不同反應,但是他們屬于同一個線程,可能嗎? 窗口由線程創(chuàng)建,一個線程可以創(chuàng)建多個窗口。窗口可由CreateWindow()函數(shù)創(chuàng)建,但前提是需要提供一個已注冊的窗口類(Window Class),每一個窗口類在注冊時需要指定一個窗口處理函數(shù)(Window Procedure),這個函數(shù)是一個回調函數(shù),就是用來處理消息的。而由一個線程來創(chuàng)建對應于不同的窗口類的窗口是可以的。
消息的發(fā)送終歸通過函數(shù)調用,比較常用的有PostMessage(),SendMessage(),另外還有一些Post*或Send*的函數(shù)。函數(shù)的調用者即發(fā)送消息的人。 他們的的原型如下: LRESULT SendMessage( 這種機制可能引起死鎖,所以有其他函數(shù)比如SendMessageTimeout(), SendMessageCallback()等函數(shù)來避免這種情況。 PostMessage()并不需要同步,所以比較簡單,它只是負責把消息發(fā)送到隊列里面,然后馬上返回發(fā)送者,之后消息的處理則再受控制。 消息可以不進隊列嗎?什么消息不進隊列? 可以。實際上MSDN把消息分為隊列型(Queued Message)和非隊列型(Non-queued Message),這只是不同的路由方式,但最終都會由消息處理函數(shù)來處理。 其實,按照MSDN的說法和消息的路由過程可以理解為,Posted Message Queue里的消息是真正的隊列型消息,而通過SendMessage()發(fā)送到消息,即使它進入了Sent Message Queue,由于SendMessage要求的同步處理,這些消息也應該算非隊列型消息。也許,Windows系統(tǒng)會特殊處理,使消息強行繞過隊列。
消息可以由Windows系統(tǒng)發(fā)送,也可以由應用程序本身;可以向線程內發(fā)送,也可以夸線程。主要是看發(fā)送函數(shù)的調用者。 對于硬件消息,Windows系統(tǒng)啟動時會運行一個叫Raw Input Thread的線程,簡稱RIT。這個線程負責處理System Hardware Input Queue(SHIQ)里面的消息,這些消息由硬件驅動發(fā)送。RIT負責把SHIQ里的消息分發(fā)到線程的消息隊列里面,那RIT是如何知道該發(fā)給誰呢?如果是鼠標事件,那就看鼠標指針所指的窗口屬于哪個線程,如果是鍵盤那就看哪個窗口當前是激活的。一些特殊的按鍵會有所不同,比如 Alt+Tab,Ctrl+Alt+Del等,RIT能保證他們不受當前線程的影響而死鎖。RIT只能同時和一個線程關聯(lián)起來。
想象一個通常的Windows應用程序啟動后,會顯示一個窗口,它在等待用戶的操作,并作出反應。 一個典型的消息循環(huán)如下所示(注意這里沒有處理GetMessage出錯的情況): while(GetMessage(&msg, NULL, 0, 0 ) != FALSE) 下面在看看GetMessage()的細節(jié): BOOL GetMessage( 其他幾個參數(shù)是用來過濾消息的,可以指定接收消息的窗口,以及確定消息的類型范圍。 這里還需要提到一個概念是線程的Wake Flag,這是一個整型值,保存在THREADINFO里面和4個消息隊列平級的位置。它的每一位(bit)代表一個開關,比如QS_QUIT, QS_SENDMESSAGE等等,這些開關根據(jù)不同的情況會被打開或關閉。GetMessage()在處理的時候會依賴這些開關。 GetMessage()的處理流程如下: 1. 處理Sent Message Queue里的消息,這些消息主要是由其他線程的SendMessage()發(fā)送,因為他們不能直接調用本線程的處理函數(shù),而本線程調用 SendMessage()時會直接調用處理函數(shù)。一旦調用GetMessage(),所有的Sent Message都會被處理掉,并且GetMessage()不會返回; 2. 處理Posted Message Queue里的消息,這里拿到一個消息后,GetMessage()將它拷貝到MSG結構中并返回TRUE。注意有三個消息WM_QUIT, WM_PAINT, WM_TIMER會被特殊處理,他們總是放在隊列的最后面,直到?jīng)]有其他消息的時候才被處理,連續(xù)的WM_PAINT消息甚至會被合并成一個以提高效率。從后面討論的這三個消息的發(fā)送方式可以看出,使用Send或Post消息到隊列里情況不多。 3. 處理QS_QUIT開關,這個開關由PostQuitMessage()函數(shù)設置,表示線程需要結束。這里為什么不用Send或Post一個 WM_QUIT消息呢?據(jù)稱:一個原因是處理內存緊缺的特殊情況,在這種情況下Send和Post很可能失?。黄浯问强梢员WC線程結束之前,所有Sent 和Posted消息都得到了處理,這是因為要保證程序運行的正確性,或者數(shù)據(jù)丟失?不得而知。 4. 處理Virtualized Input Queue里的消息,主要包括硬件輸入和系統(tǒng)內部消息,并返回TRUE; 5. 再次處理Sent Message Queue,來自MSDN卻沒有解釋。難道在檢查2、3、4步驟的時候可能出現(xiàn)新的Sent Message?或者是要保證推后處理后面兩個消息; 6. 處理QS_PAINT開關,這個開關只和線程擁有的窗口的有效性(Validated)有關,不受WM_PAINT的影響,當窗口無效需要重畫的時候這個開關就會打開。當QS_PAINT打開的時候,GetMessage()會返回一個WM_PAINT消息。處理QS_PAINT放在后面,因為重繪一般比較慢,這樣有助于提高效率; 7. 處理QS_TIMER開關,和QS_PAINT類似,返回WM_TIMER消息,之所以它放在QS_PAINT之后是因為其優(yōu)先級更低,如果Timer消息要求重繪但優(yōu)先級又比Paint高,那么Paint就沒有機會運行了。 如果GetMessage()中任何消息可處理,GetMessage()不會返回,而是將線程掛起,也就不會占用CPU時間了。 還有一個PeekMessage(),其原型為: BOOL PeekMessage( WM_DESTROY, WM_QUIT, WM_CLOSE消息有什么不同? 而其他兩個消息是關于窗口的,WM_CLOSE會首先發(fā)送,一般情況程序接到該消息后可以有機會詢問用戶是否確認關閉窗口,如果用戶確認后才調用 DestroyWindow()銷毀窗口,此時會發(fā)送WM_DESTROY消息,這時窗口已經(jīng)不顯示了,在處理WM_DESTROY消息是可以發(fā)送 PostQuitMessage()來設置QS_QUIT開關,WM_QUIT消息會由GetMessage()函數(shù)返回,不過此時線程的消息循環(huán)可能也即將結束。 窗口內的消息的路由是怎樣的?窗口和其控件的關系是什么? 一個窗口(Window)可以有一個Parent屬性,對一個Parent窗口來說,屬于它的窗口被稱為子窗口(Child Window)??丶–ontrol)或對話框(Dialog)也是窗口,他們一般屬于某個父窗口。
由消息處理函數(shù)(Window Procedure)來處理。消息處理函數(shù)是一個回調函數(shù),其地址在注冊窗口類的時候注冊,只有在線程內才能調用。 其原型為: typedef LRESULT (CALLBACK* WNDPROC)(HWND hWnd, UINT uMsg, WPARAM wParam, LPARAM lParam); 處理函數(shù)內部一般是一個switch-case結構,來針對不同的消息類型進行處理。Windows系統(tǒng)還為所有窗口預定義了一個默認的處理函數(shù) DefWindowProc(),它提供了最基本的消息處理,一般在不需要特殊處理的時候(即在switch的default分支)會調用這個函數(shù)。 處理函數(shù)里可以發(fā)送消息,但是可以想象有可能出現(xiàn)循環(huán)。另外處理函數(shù)還常常被遞歸調用,所以要減少局部變量的使用,以避免遞歸過深是棧溢出。 最后關于處理函數(shù)特化的問題將在另外的文章討論。 |
|
|