標籤:
Windows是一個訊息(Message)驅動系統。Windows的訊息提供了應用程式之間、應用程式與Windows系統之間進行通訊的手段。應用程式想要實現的功能由訊息來觸發,並且靠對訊息的響應和處理來完成。必須注意的是,訊息並非是搶佔性的,無論事件的緩急,總是按照到達的先後派對,依次處理(一些系統訊息除外),這樣可能使一些即時外來事件得不到及時處理。
Windows的應用程式一般包含視窗(Window),它主要為使用者提供一種可視化的互動方式,視窗是總是在某個線程(Thread)內建立的。Windows系統通過訊息機制來管理互動,訊息(Message)被發送,儲存,處理,一個線程會維護自己的一套訊息佇列(Message Queue),以保持線程間的獨佔性。隊列的特點無非是先進先出,這種機制可以實現一種非同步需求響應過程。
目錄:
1、訊息
2 、訊息類型
3 、訊息佇列(Message Queues)
4 、隊列訊息(Queued Messages)和非隊列訊息(Non-Queued Messages)
5 、PostMessage(PostThreadMessage), SendMessage
6 、GetMessage, PeekMessage
7 、TranslateMessage, TranslateAccelerator
8、(訊息死結( Message Deadlocks)
9、BroadcastSystemMessage
10、訊息的處理
11、MFC的訊息映射
12、訊息反射機制
1、訊息
訊息系統對於一個win32程式來說十分重要,它是一個程式啟動並執行動力源泉。一個訊息,是系統定義的一個32位的值,他唯一的定義了一個事件,向Windows發出一個通知,告訴應用程式某個事情發生了。例如,單擊滑鼠、改變視窗尺寸、按下鍵盤上的一個鍵
都會使Windows發送一個訊息給應用程式。
訊息本身是作為一個記錄傳遞給應用程式的,這個記錄中包含了訊息的類型以及其他資訊。例如,對於單擊滑鼠所產生的訊息來
說,這個記錄中包含了單擊滑鼠時的座標。這個記錄類型叫做MSG,MSG含有來自windows應用程式訊息佇列的訊息資訊,它在
Windows中聲明如下:
typedef struct tagMsg
{
HWND hwnd; // 接受該訊息的視窗控制代碼
UINT message; // 訊息常量標識符,也就是我們通常所說的訊息編號
WPARAM wParam; // 32位訊息的特定附加資訊,確切含義依賴於訊息值
LPARAM lParam; // 32位訊息的特定附加資訊,確切含義依賴於訊息值
DWORD time; // 訊息建立時的時間
POINT pt; // 訊息建立時的滑鼠/游標在螢幕座標系中的位置
}MSG;
訊息可以由系統或者應用程式產生。系統在發生輸入事件時產生訊息。舉個例子, 當使用者敲鍵, 移動滑鼠或者單擊控制項。系統也產生訊息以響應由應用程式帶來的變化, 比如應用程式改變系統字型,改變表單大小。應用程式可以產生訊息使表單執行任務,或者與其他應用程式中的視窗通訊。
2、訊息類型
1) 系統定義訊息(System-Defined Messages)
在SDK中事先定義好的訊息,非使用者定義的,其範圍在[0x0000, 0x03ff]之間, 可以分為以下三類:
1> 視窗訊息(Windows Message)
與視窗的內部運作有關,如建立視窗,繪製視窗,銷毀視窗等。可以是一般的視窗,也可以是Dialog,控制項等。
如:WM_CREATE, WM_PAINT, WM_MOUSEMOVE, WM_CTLCOLOR, WM_HSCROLL...
2> 命令訊息(Command Message)
與處理使用者請求有關, 如單擊功能表項目或工具列或控制項時, 就會產生命令訊息。
WM_COMMAND, LOWORD(wParam)表示功能表項目,工具列按鈕或控制項的ID。如果是控制項, HIWORD(wParam)表示控制項訊息類型
3> 控制項通知(Notify Message)
控制項通知訊息, 這是最靈活的訊息格式, 其Message, wParam, lParam分別為:WM_NOTIFY, 控制項ID,指向NMHDR的指標。NMHDR包含控制項通知的內容, 可以任意擴充。
2) 程式定義訊息(Application-Defined Messages)
使用者自訂的訊息, 對於其範圍有如下規定:
WM_USER: 0x0400-0x7FFF (ex. WM_USER+10)
WM_APP(winver> 4.0): 0x8000-0xBFFF (ex.WM_APP+4)
RegisterWindowMessage: 0xC000-0xFFFF
3、訊息佇列(Message Queues)
Windows中有兩種類型的訊息佇列
1) 系統訊息佇列(System Message Queue)
這是一個系統唯一的Queue,裝置驅動(mouse, keyboard)會把操作輸入轉化成訊息存在系統隊列中,然後系統會把此訊息放到目標視窗所在的線程的訊息佇列(thread-specific message queue)中等待處理
2) 線程訊息佇列(Thread-specific Message Queue)
每一個GUI線程都會維護這樣一個線程訊息佇列。(這個隊列只有線上程調用GDI函數時才會建立,預設不建立)。然後線程訊息佇列中的訊息會被送到相應的視窗過程(WndProc)處理.
注意: 線程訊息佇列中WM_PAINT,WM_TIMER只有在Queue中沒有其他訊息的時候才會被處理,WM_PAINT訊息還會被合并以提高效率。其他所有訊息以先進先出(FIFO)的方式被處理。
4、隊列訊息(Queued Messages)和非隊列訊息(Non-Queued Messages)
1)隊列訊息(Queued Messages)
訊息會先儲存在訊息佇列中,訊息迴圈會從此隊列中取訊息並分發到各視窗處理 、如滑鼠,鍵盤訊息。
2) 非隊列訊息(NonQueued Messages)
訊息會繞過系統訊息佇列和線程訊息佇列直接發送到視窗過程被處理 如: WM_ACTIVATE, WM_SETFOCUS, WM_SETCURSOR, WM_WINDOWPOSCHANGED
注意: postMessage發送的訊息是隊列訊息,它會把訊息Post到訊息佇列中; SendMessage發送的訊息是非隊列訊息, 被直接送到視窗過程處理.
隊列訊息和非隊列訊息的區別
從訊息的發送途徑來看,訊息可以分成2種:隊列訊息和非隊列訊息。訊息佇列由可以分成系統訊息佇列和線程訊息佇列。系統訊息佇列由Windows維護,線程訊息佇列則由每個GUI線程自己進行維護,為避免給non-GUI現成建立訊息佇列,所有線程產生時並沒有訊息佇列,僅當線程第一次調用GDI函數時系統才給線程建立一個訊息佇列。隊列訊息送到系統訊息佇列,然後到線程訊息佇列;非隊列訊息直接送給目的視窗過程。
對於隊列訊息,最常見的是滑鼠和鍵盤觸發的訊息,例如WM_MOUSERMOVE,WM_CHAR等訊息,還有一些其它的訊息,例如:WM_PAINT、 WM_TIMER和WM_QUIT。當滑鼠、鍵盤事件被觸發後,相應的滑鼠或鍵盤驅動程式就會把這些事件轉換成相應的訊息,然後輸送到系統訊息佇列,由 Windows系統去進行處理。Windows系統則在適當的時機,從系統訊息佇列中取出一個訊息,根據前面我們所說的MSG訊息結構確定訊息是要被送往那個視窗,然後把取出的訊息送往建立視窗的線程的相應隊列,下面的事情就該由線程訊息佇列操心了,Windows開始忙自己的事情去了。線程看到自己的訊息佇列中有訊息,就從隊列中取出來,通過作業系統發送到合適的視窗過程去處理。
一般來講,系統總是將訊息Post在訊息佇列的末尾。這樣保證視窗以先進先出的順序接受訊息。然而,WM_PAINT是一個例外,同一個視窗的多個 WM_PAINT被合并成一個 WM_PAINT 訊息, 合并所有的無效地區到一個無效地區。合并WM_PAIN的目的是為了減少重新整理視窗的次數。
非隊列訊息將會繞過系統隊列和訊息佇列,直接將訊息發送到視窗過程,。系統發送非隊列訊息通知視窗,系統發送訊息通知視窗。例如,當使用者啟用一個視窗系統發送WM_ACTIVATE, WM_SETFOCUS, and WM_SETCURSOR。這些訊息通知視窗它被啟用了。非隊列訊息也可以由當應用程式調用系統函數產生。例如,當程式調用SetWindowPos系統發送WM_WINDOWPOSCHANGED訊息。一些函數也發送非隊列訊息,例如下面我們要談到的函數。
5 、PostMessage(PostThreadMessage), SendMessage
PostMessage:把訊息放到指定視窗所在的線程訊息佇列中後立即返回。 PostThreadMessage:把訊息放到指定線程的訊息佇列中後立即返回。
SendMessage:直接把訊息送到視窗過程處理, 處理完了才返回。
PostMessage(非同步)和SendMessage(同步)的區別
a、 PostMessage 是非同步,SendMessage 是同步的。
PostMessage 只把訊息放到隊列,不管訊息是不是被處理就返回,訊息可能不被處理;
SendMessage等待訊息被處理完了才返回,如果訊息不被處理,發送訊息的線程將一直處於阻塞狀態,等待訊息的返回。
b、 同一個線程內:
SendMessage 發送訊息時,由USER32.DLL模組調用目標視窗的訊息處理常式,並將結果返回,SendMessage 在同一個線程裡面發送訊息不進入線程訊息佇列;PostMessage 發送的訊息要先放到訊息佇列,然後通過訊息迴圈指派到目標視窗(DispatchMessage)。
c、不同線程:
SendMessage 發送訊息到目標視窗的訊息佇列,然後發送訊息的線程在USER32。DLL模組內監視和等待訊息的處理結果,直到目標視窗的才處理返回,SendMessage在返回之前還需要做許多工作,如響應別的線程向它發送的SendMessage().PostMessge() 到別的線程的時候最好使用PostThreadMessage 代替。PostMessage()的HWND 參數可以為NULL,相當於PostThreadMessage() + GetCrrentThreadId.
d、系統處理訊息。
系統只處理(marshal)系統訊息(0--WM_USER),發送使用者訊息(使用者自己定義)時需要使用者自己處理。
使用PostMessage,SendNotifyMessage,SendMessageCallback等非同步函數發送系統訊息時,參數不可以使用指標,因為寄件者不等待訊息的處理就返回,接收者還沒有處理,指標就有可能被釋放了,或則內容變化了。
e、在Windows 2000/XP,每個訊息佇列最多隻能存放一定數量的訊息,超過的將不會被處理就丟掉。系統預設是10000;:[HKEY_LOCAL_MACHINE\SOFTWARE\Microsoft\Windows NT\CurrentVersion\Windows] USERPostMessageLimit
6 、GetMessage, PeekMessage
PeekMessage會立即返回 可以保留訊息
GetMessage在有訊息時返回 會刪除訊息
PeekMessage和GetMessage函數的主要區別有:
a. GetMessage的主要功能是從訊息佇列中“取出”訊息,訊息被取出以後,就從訊息佇列中將其刪除;而PeekMessage的主要功能是“窺視”訊息,如果有訊息,就返回true,否則返回false。也可以使用PeekMessage從訊息佇列中取出訊息,這要用到它的一個參數(UINT wRemoveMsg),如果設定為PM_REMOVE,訊息則被取出並從訊息佇列中刪除;如果設定為PM_NOREMOVE,訊息就不會從訊息佇列中取出。
b. 如果GetMessage從訊息佇列中取不到訊息,則線程就會被作業系統掛起,等到OS重新調度該線程時,兩者的性質不同:使用GetMessage線程仍會被掛起,使用PeekMessage線程會得到CPU的控制權,運行一段時間。
c、GetMessage每次都會等待訊息,直到取到訊息才返回;而PeekMessage只是查詢訊息佇列,沒有訊息就立即返回,從傳回值判斷是否取到了訊息。
我們也可以說,PeekMessage是一個具有線程非同步行為的函數,不管訊息佇列中是否有訊息,函數都會立即返回。而GetMessage則是一個具有線程同步行為的函數,如果訊息佇列中沒有訊息的話,函數就會一直等待,直到訊息佇列中至少有一條訊息時才返回。
如果訊息佇列中沒有訊息,PeekMessage總是能返回,這就相當於在執行一個迴圈,如果訊息佇列一直為空白, 它就進入了一個死迴圈。GetMessage則不可能因為訊息佇列為空白而進入死迴圈。
聯絡:
在Windows的內部,GetMessage和PeekMessage執行著相同的代碼,Peekmessage和Getmessage都是向系統的訊息佇列中取得訊息,並將其放置在指定的結構。
區別:
PeekMessage:有訊息時返回TRUE,沒有訊息返回FALSE
GetMessage:有訊息時且訊息不為WM_QUIT時返回TRUE,如果有訊息且為WM_QUIT則返回FALSE,沒有訊息時不返回。
GetMessage:取得訊息後,刪除除WM_PAINT訊息以外的訊息。
PeekMessage:取得訊息後,根據wRemoveMsg參數判斷是否刪除訊息。PM_REMOVE則刪除,PM_NOREMOVE不刪除。
The PeekMessage function normally does not remove WM_PAINT messages from the queue. WM_PAINT messages remain in the queue until they are processed. However, if a WM_PAINT message has a null update region, PeekMessage does remove it from the queue.
不能用PeekMessage從訊息佇列中刪除WM_PAINT訊息,從隊列中刪除WM_PAINT訊息可以令視窗顯示地區的失效地區變得有效(重新整理視窗),如果隊列中包含WM_PAINT訊息程式就會一直while迴圈了。
7 、TranslateMessage, TranslateAccelerator
TranslateMessage: 把一個virtual-key訊息轉化成字元訊息(character message),並放到當前線程的訊息佇列中,訊息迴圈下一次取出處理。
TranslateAccelerator: 將快速鍵對應到相應的功能表命令。它會把WM_KEYDOWN 或 WM_SYSKEYDOWN轉化成快速鍵表中相應的WM_COMMAND 或WM_SYSCOMMAND訊息, 然後把轉化後的 WM_COMMAND或WM_SYSCOMMAND直接發送到視窗過程處理, 處理完後才會返回。
8、(訊息死結( Message Deadlocks)
假設有線程A和B, 現在有以下下步驟
1) 線程A SendMessage給線程B, A等待訊息線上程B中處理後返回
2) 線程B收到了線程A發來的訊息,並進行處理, 在處理過程中,B也向線程A SendMessgae,然後等待從A返回。 因為此時, 線程A正等待從線程B返回, 無法處理B發來的訊息, 從而導致了線程A,B相互等待, 形成死結。多個線程也可以形成環形死結。
可以使用 SendNotifyMessage或SendMessageTimeout來避免出現死結。
9、BroadcastSystemMessage
我們一般所接觸到的訊息都是發送給視窗的, 其實, 訊息的接收者可以是多種多樣的,它可以是應用程式(applications), 可安裝驅動(installable drivers), 網路裝置(network drivers), 系統級裝置驅動(system-level device drivers)等,
BroadcastSystemMessage這個API可以對以上系統組件發送訊息。
10、訊息的處理
接下來我們談一下訊息的處理,首先我們來看一下VC中的訊息泵:
while(GetMessage(&msg, NULL, 0, 0))
{
if(!TranslateAccelerator(msg.hWnd, hAccelTable, &msg))
{
TranslateMessage(&msg);
DispatchMessage(&msg);
}
}
TranslateMessage(轉換訊息):
用來把虛擬鍵訊息轉換為字元訊息。由於Windows對所有鍵盤編碼都是採用虛擬鍵的定義,這樣當按鍵按下時,並不得字元訊息,需要鍵盤對應轉換為字元的訊息。
TranslateMessage函數
用於將虛擬鍵訊息轉換為字元訊息。字元訊息被投遞到調用線程的訊息佇列中,當下一次調用GetMessage函數時被取出。當我們敲擊鍵盤上的某個字元鍵時,系統將產生WM_KEYDOWN和WM_KEYUP訊息。這兩個訊息的附加參數(wParam和lParam)包含的是虛擬按鍵碼和掃描碼等資訊,而我們在程式中往往需要得到某個字元的ASCII碼,TranslateMessage這個函數就可以將WM_KEYDOWN和WM_ KEYUP訊息的組合轉換為一條WM_CHAR訊息(該訊息的wParam附加參數包含了字元的ASCII碼),並將轉換後的新訊息投遞到調用線程的訊息佇列中。注意,TranslateMessage函數並不會修改原有的訊息,它只是產生新的訊息並投遞到訊息佇列中。
也就是說TranslateMessage會發現訊息裡是否有字元鍵的訊息,如果有字元鍵的訊息,就會產生WM_CHAR訊息,如果沒有就會產生什麼訊息。
DispatchMessage(指派訊息):
把 TranslateMessage轉換的訊息發送到視窗的訊息處理函數,此函數在視窗註冊時已經指定。
首先,GetMessage從進程的主線程的訊息佇列中擷取一個訊息並將它複製到MSG結構,如果隊列中沒有訊息,則GetMessage函數將等待一個訊息的到來以後才返回。如果你將一個視窗控制代碼作為第二個參數傳入GetMessage,那麼只有指定視窗的的訊息可以從隊列中獲得。GetMessage也可以從訊息佇列中過濾訊息只接受訊息佇列中落在範圍內的訊息。這時候就要利用GetMessage/PeekMessage指定一個訊息過濾器。這個過濾器是一個訊息標識符的範圍或者是一個表單控制代碼,或者兩者同時指定。當應用程式要尋找一個後入訊息佇列的訊息是很有用。WM_KEYFIRST 和 WM_KEYLAST 常量用於接受所有的鍵盤訊息。 WM_MOUSEFIRST 和 WM_MOUSELAST 常量用於接受所有的滑鼠訊息。
然後TranslateAccelerator判斷該訊息是不是一個按鍵訊息並且是一個加速鍵訊息,如果是,則該函數將把幾個按鍵訊息轉換成一個加速鍵訊息傳遞給視窗的回呼函數。處理了加速鍵之後,函數TranslateMessage將把兩個按鍵訊息WM_KEYDOWN和WM_KEYUP轉換成一個 WM_CHAR,不過需要注意的是,訊息WM_KEYDOWN,WM_KEYUP仍然將傳遞給視窗的回呼函數。
處理完之後,DispatchMessage函數將把此訊息發送給該訊息指定的視窗中已設定的回呼函數。如果訊息是WM_QUIT,則 GetMessage返回0,從而退出迴圈體。應用程式可以使用PostQuitMessage來結束自己的訊息迴圈。通常在主視窗的 WM_DESTROY訊息中調用。11、MFC的訊息映射
使用MFC編程時,訊息發送和處理的本質和Win32相同,但是,它對訊息處理進行了封裝,簡化了程式員編程時訊息處理的複雜性,它通過訊息映射機制來處理訊息,程式員不必去設計和實現自己的視窗過程。
說白了,MFC中的訊息映射機制實質是一張巨大的訊息及其處理函數對應表。訊息映射基本上分為兩大部分:
在標頭檔(.h)中有一個宏DECLARE_MESSAGE_MAP(),它放在類的末尾,是一個public屬性的;與之對應的是在實現部分(.cpp)增加了一個訊息映射表,內容如下:
BEGIN_MASSAGE_MAP(當前類,當前類的基類)
//{{AFX_MSG_MAP(CMainFrame)
訊息的入口項
//}}AFX_MSG_MAP
END_MESSAGE_MAP()
但是僅是這兩項還不足以完成一條訊息,要是一個訊息工作,必須還有以下3個部分去協作:
1、在類的定義中加入相應的函式宣告;
2、在類的訊息映射表中加入相應的訊息映射入口項;
3、在類的實現中加入相應的函數體;
訊息的添加
(1)、利用Class Wizard實現自動添加
在菜單中選擇View -> Class Wizard啟用Class Wizard,選擇Message Map標籤,從Class name組合框中選取我們想要添加訊息的類。在Object IDs列表框中,選取類的名稱。此時,Messages列表框顯示該類的可重載成員函數和視窗訊息。可重載成員函數顯示在列表的上部,以實際虛構成員函數的大小寫字母來表示。其他為視窗訊息,以大寫字母出現。選中我們要添加的訊息,單擊Add Funtion按鈕,Class Wizard自動將該訊息添加進來。
有時候,我們想要添加的訊息在Message列表中找不到,我們可以利用Class Wizard上Class Info標籤以擴充訊息列表。在該頁中,找到Message Filter組合框,通過它可以改變首頁中Messages列表框中的選項。
(2)、手動添加訊息
如果Messages列表框中確實沒有我們想要的訊息,就需要我們手工添加:
1)在類的.h檔案中添加處理函數的聲明,緊接著在//}}AFX_MSG行之後加入聲明,注意,一定要以afx_msg開頭。
通常,添加處理函式宣告的最好的地方是原始碼中Class Wizard維護的表的下面,在它標記其領域的{{ }}括弧外面。這些括弧中的任何東西都有可能會被Class Wizard銷毀。
2)接著,在使用者類的.cpp檔案中找到//}}AFX_MSG_MAP行,緊接在它之後加入訊息入口項。同樣,也放在{{ }}外面。
3)最後,在該檔案中添加訊息處理函數的實體。
對於能夠使用Class Wizard添加的訊息,盡量使用Class Wizard添加,以減少我們的工作量;對於不能使用Class Wizard添加的訊息和自訂訊息,需要手動添加。總體說來,MFC的訊息編程對使用者來說,相對比較簡單,在此不再使用執行個體示範。
12、訊息反射機制
什麼叫訊息反射?
父視窗將控制項發給它的通知訊息,反射回控制項進行處理(即讓控制項處理這個訊息),這種通知訊息讓控制項自己處理的機制叫做訊息反射機制。
通過前面的學習我們知道,一般情況下,控制項向父視窗發送通知訊息,由父視窗處理這些通知訊息。這樣,父視窗(通常是一個對話方塊)會對這些訊息進行處理,換句話說,控制項的這些訊息處理必須在父視窗類體內,每當我們添加子控制項的時候,就要在父視窗類中複製這些代碼。很明顯,這對代碼的維護和移植帶來了不便,而且,明顯背離C++的對象編程原則。
從4.0版開始,MFC提供了一種訊息反射機制(Message Reflection),可以把控制項通知訊息反射回控制項。具體地講,對於反射訊息,如果控制項有該訊息的處理函數,那麼就由控制項自己處理該訊息,如果控制項不處理該訊息,則架構會把該訊息繼續送給父視窗,這樣父視窗繼續處理該訊息。可見,新的訊息反射機制並不破壞原來的通知訊息處理機制。
訊息反射機製為控制項提供了處理通知訊息的機會,這是很有用的。如果按傳統的方法,由父視窗來處理這個訊息,則加重了控制項對象對父視窗的依賴程度,這顯然違背了物件導向的原則。若由控制項自己處理訊息,則使得控制項對象具有更大的獨立性,大大方便了代碼的維護和移植。
執行個體M8:簡單地示範MFC的訊息反射機制。(見附帶源碼 工程M8)
開VC++ 6.0,建立一個基於對話方塊的工程M8。
在該工程中,建立一個CMyEdit類,基類是CEdit。接著,在該類中添加三個變數,如下:
private:
CBrush m_brBkgnd;
COLORREF m_clrBkgnd;
COLORREF m_clrText;
在CMyEdit::CMyEdit()中,給這三個變數賦初值:
{
m_clrBkgnd = RGB( 255, 255, 0 );
m_clrText = RGB( 0, 0, 0 );
m_brBkgnd.CreateSolidBrush(RGB( 150, 150, 150) );
}
開啟ClassWizard,類名為CMyEdit,Messages處選中“=WM_CTLCOLOR”,您是否發現,WM_CTLCOLOR訊息前面有一個等號,它表示該訊息是反射訊息,也就是說,前面有等號的訊息是可以反射的訊息。
訊息反射函數代碼如下:
HBRUSH CMyEdit::CtlColor(CDC* pDC, UINT nCtlColor)
{
// TODO: Change any attributes of the DC here
pDC->SetTextColor( m_clrText );//設定文本顏色
pDC->SetBkColor( m_clrBkgnd );//設定背景顏色
//請注意,在我們改寫該函數的內容前,函數返回NULL,即return NULL;
//函數返回NULL將會執行父視窗的CtlColor函數,而不執行控制項的CtlColor函數
//所以,我們讓函數返回背景刷,而不返回NULL,目的就是為了實現訊息反射
return m_brBkgnd; //返回背景刷
}
在IDD_M8_DIALOG對話方塊中添加一個Edit控制項,使用ClassWizard給該Edit控制項添加一個CMyEdit類型的變數m_edit1,把Edit控制項和CMyEdit關聯起來。
林炳文Evankaka原創作品。轉載請註明出處http://blog.csdn.net/evankaka
Windows訊息傳遞機制詳解