1. 程式人生 > >[轉]windows訊息機制(MFC)

[轉]windows訊息機制(MFC)

訊息分類與訊息佇列

Windows中,訊息使用統一的結構體(MSG)來存放資訊,其中message表明訊息的具體的型別,

而wParam,lParam是其最靈活的兩個變數,為不同的訊息型別時,存放資料的含義也不一樣。

time表示產生訊息的時間,pt表示產生訊息時滑鼠的位置。

按照型別,Windows將訊息分為:

(0) 訊息ID範圍

系統定義訊息ID範圍:[0x0000, 0x03ff] 
使用者自定義的訊息ID範圍: 
WM_USER: 0x0400-0x7FFF (例:WM_USER+10) 
WM_APP(winver> 4.0):0x8000-0xBFFF (例:WM_APP+4) 
RegisterWindowMessage:0xC000-0xFFFF【用來和其他應用程式通訊,為了ID的唯一性,使用::RegisterWindowMessage來得到該範圍的訊息ID 】

(1) 視窗訊息:即與視窗的內部運作有關的訊息,如建立視窗,繪製視窗,銷燬視窗等。

     可以是一般的視窗,也可以是MainFrame,Dialog,控制元件等。

如:WM_CREATE, WM_PAINT, WM_MOUSEMOVE, WM_CTLCOLOR, WM_HSCROLL等

(2) 當用戶從選單選中一個命令專案、按下一個快捷鍵或者點選工具欄上的一個按鈕,都將傳送WM_COMMAND命令訊息。

LOWORD(wParam)表示選單項,工具欄按鈕或控制元件的ID;如果是控制元件, HIWORD(wParam)表示控制元件訊息型別。

     #define LOWORD(l) ((WORD)(l))

     #define HIWORD(l) ((WORD)(((DWORD)(l) >> 16) & 0xFFFF))

(3) 隨著控制元件的種類越來越多,越來越複雜(如列表控制元件、樹控制元件等),僅僅將wParam,lParam將視為一個32位無符號整數,已經裝不下太多資訊了。

    為了給父視窗傳送更多的資訊,微軟定義了一個新的WM_NOTIFY訊息來擴充套件WM_COMMAND訊息。

    WM_NOTIFY訊息仍然使用MSG訊息結構,只是此時wParam為控制元件ID,lParam為一個NMHDR指標,

    不同的控制元件可以按照規則對NMHDR進行擴充,因此WM_NOTIFY訊息傳送的資訊量可以相當的大。

注:Window 9x 版及以後的新控制元件通告訊息不再通過WM_COMMAND 傳送,而是通過WM_NOTIFY 傳送, 
      但是老控制元件的通告訊息, 比如CBN_SELCHANGE 還是通過WM_COMMAND 訊息傳送。

(4) windwos也允許程式設計師定義自己的訊息,使用SendMessage或PostMessage來發送訊息。

windows訊息還可以分為:

(1) 佇列訊息(Queued Messages) 
訊息會先儲存在訊息佇列中,訊息迴圈會從此佇列中取出訊息並分發到各視窗處理 
如:WM_PAINT,WM_TIMER,WM_CREATE,WM_QUIT,以及滑鼠,鍵盤訊息等。 
其中,WM_PAINT,WM_TIMER只有在佇列中沒有其他訊息的時候才會被處理, 
WM_PAINT訊息還會被合併以提高效率。其他所有訊息以先進先出(FIFO)的方式被處理。

(2) 非佇列訊息(NonQueued Messages)  
訊息會繞過系統訊息佇列和執行緒訊息佇列,直接傳送到視窗過程進行處理  
如:WM_ACTIVATE, WM_SETFOCUS, WM_SETCURSOR,WM_WINDOWPOSCHANGED

Windows系統的整個訊息系統分為3個層級:

    ① Windows核心的系統訊息佇列

    ② App的UI執行緒訊息佇列

    ③ 處理訊息的窗體物件

Windows核心維護著一個全域性的系統訊息佇列;按照執行緒的不同,系統訊息佇列中的訊息會分發到應用程式的UI執行緒的訊息佇列中;

應用程式的每一個UI執行緒都有自己的訊息迴圈,會不停地從自己的訊息佇列取出訊息,併發送給Windows窗體物件;

每一個窗體物件都使用窗體過程函式(WindowProc)來處理接收到的各種訊息。

複製程式碼

 1 LRESULT CALLBACK WindowProc(HWND hWnd, UINT message, WPARAM wParam, LPARAM lParam)
 2 {
 3     PAINTSTRUCT ps;
 4     HDC hdc;
 5 
 6     switch (message)
 7     {
 8     case WM_COMMAND:
 9         break;
10     case WM_PAINT:
11         hdc = BeginPaint(hWnd, &ps);
12         // TODO: 在此新增任意繪圖程式碼...
13         EndPaint(hWnd, &ps);
14         break;
15     case WM_DESTROY:
16         PostQuitMessage(0);
17         break;
18     default:
19         return DefWindowProc(hWnd, message, wParam, lParam);
20     }
21     return 0;
22 }

複製程式碼

需要的話,在WindowProc中,可以用::GetMessageTime獲取當前訊息產生的時間, 
用::GetMessagePos獲取當前訊息產生時滑鼠游標所在的位置。

(1) 各個視窗訊息由各個窗體(或控制元件)自身的WindowProc(虛擬函式)接收並處理。

(2) WM_COMMAND命令訊息統一由當前活動主視窗的WindowProc接收,經過繞行後,可被其他的CCmdTarget物件處理。

(3) WM_COMMAND控制元件通知統一由子視窗(控制元件)的父視窗的WindowProc接收並處理,也可以進行繞行被其他的CCmdTarget物件處理。

     (例如:CFormView具備接受WM_COMMAND控制元件通知的條件,又具備把WM_COMMAND訊息派發給關聯文件物件處理的能力,

         所以給CFormView的WM_COMMAND控制元件通知是可以讓文件物件處理的。)

     另外,WM_COMMAND控制通知會先呼叫ReflectLastMsg反射通知子視窗(控制元件),如果子視窗(控制元件)處理了該訊息並返回TRUE,則訊息會停止分發;

     否則,會繼續呼叫OnCmdMsg進行命令傳送(如同WM_COMMAND命令訊息一樣)。

注:WM_COMMAND命令訊息與WM_COMMAND控制元件通知的相似之處: 
WM_COMMAND命令訊息和WM_COMMAND控制通知都是由WindowProc給OnCommand處理, 
OnCommand通過wParam和lParam引數區分是命令訊息或通知訊息,然後送給OnCmdMsg處理。 
事實上,BN_CLICKED控制元件通知訊息的處理和WM_COMMAND命令訊息的處理完全一樣。 
因為該訊息的通知程式碼是0,ON_BN_CLICKED(id,memberfunction)和ON_COMMAND(id,memberfunction)是等同的。

(4)WM_NOTIFY訊息只是對WM_COMMAND控制元件通知進行了擴充套件,與WM_COMMAND控制元件通知具有相同的特點。

SendMessage與PostMessage

PostMessage 把訊息投遞到訊息佇列後,立即返回; 
SendMessage把訊息直接送到視窗過程處理,處理完才返回。

GetMessage與PeekMessage

GetMessage 有訊息且該訊息不為WM_QUIT,返回TRUE。 
            有訊息且該訊息為WM_QUIT,返回FALSE。 
                  沒有訊息時,掛起該UI執行緒,控制權交還給系統。 
PeekMessage 有訊息返回TRUE,如果沒有訊息返回FALSE。 
                   是否從訊息佇列中刪除此訊息(PM_REMOVE),由函式引數來指定。

要想在沒有訊息時做一些工作,就必須使用PeekMessage來抓取訊息,以便在沒有訊息時,能在OnIdle中執行空閒操作(如下):

複製程式碼

 1 while (TRUE) 
 2 {
 3     if (PeekMessage(&msg, NULL, 0, 0, PM_REMOVE) 
 4     {
 5         if (msg.message == WM_QUIT)
 6             break;
 7         TranslateMessage(&msg);
 8         DispatchMessage(&msg);
 9     }
10     else 
11     {
12         OnIdle();
13     }
14 }

複製程式碼

例如:MFC使用OnIdle函式來清理一些臨時物件及未使用的動態連結庫。

只有在OnIdle返回之後程式才能繼續處理使用者的輸入,因此不應在OnIdle進行較長的任務。

MFC訊息處理

在CWnd中,MFC使用OnWndMsg來分別處理各類訊息:

如果是WM_COMMAND訊息,交給OnCommand處理;然後返回。

如果是WM_NOTIFY訊息,交給OnNotify處理;然後返回。

如果是WM_ACTIVATE訊息,先交給_AfxHandleActivate處理,再繼續下面的處理。

如果是WM_SETCURSOR訊息,先交給_AfxHandleSetCursor處理,然後返回。

如果是其他的視窗訊息(包括WM_ACTIVATE訊息),則

  首先在訊息緩衝池(一個hash表,用於加快訊息處理函式的查詢)進行訊息匹配, 
    若匹配成功,則呼叫相應的訊息處理函式; 
    若不成功,則在訊息目標的訊息對映陣列中進行查詢匹配,看它是否能處理當前訊息。 
  如果訊息目標處理了該訊息,則會匹配到訊息處理函式,呼叫它進行處理;

否則,該訊息沒有被應用程式處理,OnWndMsg返回FALSE。

MFC訊息對映

訊息對映實際是MFC內建的一個訊息分派機制。

把MFC中的巨集進行展開(如下),可以得到訊息對映表整個全貌。

注:GetMessageMap為虛擬函式。 
     {0, 0, 0, 0, AfxSig_end, (AFX_PMSG)0}:物件訊息對映表的結束標識

視窗訊息只能由CWnd物件來處理,採用向基類直線上朔的方式,來查詢對應的訊息響應函式進行處理。

一旦找到訊息響應函式(若有返回值且為TRUE),就停止上朔。因此,我們經常會看到這樣的程式碼:

增加一個訊息處理函式來寫我們的邏輯時,MFC ClassWizard會在該函式之前或之後顯示呼叫其基類對應的函式,保證基類中邏輯被執行。

命令訊息可由CCmdTarget物件接收並處理(OnCmdMsg為虛擬函式),除了向基類直線上朔方式外,還有命令繞行機制(要防止形成圈,死迴圈)。

在某種程度上,控制通知訊息由視窗物件處理是一種習慣和約定。然而,控制元件通知訊息也是可以有CCmdTarget物件接收並處理,並進行命令繞行的。

下圖為MFC經典單文件檢視框架的命令訊息繞行路線:

函式呼叫過程如下(如果沒有任何物件處理該條WM_COMMAND訊息,最後會被::DefWindowProc處理)。

非模態對話方塊的訊息處理

1 static CAboutDlg aboutDlg;
2 aboutDlg.Create(IDD_ABOUTBOX, this);
3 aboutDlg.ShowWindow(SW_SHOW);

應用程式只有一個訊息迴圈。

對於視窗訊息,非模態對話方塊(及其子控制元件)與父視窗(及其子控制元件)都是用自身的WindowProc函式接收並處理,互不干擾。

對於命令訊息,由當前活動主視窗的WindowProc接收(例如:當前活動主視窗為非模態對話方塊,則命令訊息會被非模態對話方塊接收)。

可以在當前活動主視窗的OnCmdMsg中做命令繞行,使得其他的CCmdTarget物件也可以處理命令訊息。

對於控制元件通知,由其父視窗的WindowProc接收並處理,一般不進行命令繞行被其他的CCmdTarget物件處理。

模態對話方塊的訊息處理

1 CAboutDlg aboutDlg;
2 aboutDlg.DoModal();

(1) 模態對話方塊彈出來後,首先會讓父視窗失效,使其不能接受使用者的輸入(鍵盤滑鼠訊息)。

1 EnableWindow(hwndParent, FALSE) ;

(2) 父視窗訊息迴圈被阻塞(會卡在DoModal處,等待返回),由模態對話方塊的訊息迴圈來接管(因此整個程式不會卡住)。

    接管後,模態對話方塊的訊息迴圈仍然會將屬於父視窗及其子控制元件的視窗訊息(不包括鍵盤滑鼠相關的視窗訊息)傳送給它們各自的WindowProc視窗函式,進行響應處理。

(3) 模態對話方塊銷燬時(點選IDOK或IDCANCEL),父視窗訊息迴圈重新啟用,繼續DoModal後的邏輯。

    啟用後,父視窗有可以重新接受使用者的輸入(鍵盤滑鼠訊息)。

1 EnableWindow(hwndParent, TRUE) ;

從上面的過程中,我們可以得到如下結論:

對於視窗訊息,模態對話方塊主視窗(及其子控制元件)與父視窗(及其子控制元件)都是用自身的WindowProc函式接收並處理,互不干擾。

只是父視窗(及其子控制元件)無法接受到鍵盤滑鼠訊息相關的視窗訊息。

對於命令訊息,由模態對話方塊主視窗的WindowProc接收。可以在模態對話方塊主視窗的OnCmdMsg中做命令繞行,使得其他的CCmdTarget物件也可以處理命令訊息。

對於控制元件通知,由其父視窗的WindowProc接收並處理,一般不進行命令繞行被其他的CCmdTarget物件處理。

參考

《深入淺出MFC》- 侯捷