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