轉:windows消息傳遞機制

     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關聯起來。
————————————————

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章