Windows訊息機制概述

千秋大業一壺好茶發表於2012-10-17

Windows訊息機制概述

訊息是指什麼?
     訊息系統對於一個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;

    訊息可以由系統或者應用程式產生。系統在發生輸入事件時產生訊息。舉個例子, 當使用者敲鍵, 移動滑鼠或者單擊控制元件。系統也產生訊息以響應由應用程式帶來的變化, 比如應用程式改變系統字型改變窗體大小。應用程式可以產生訊息使窗體執行任務,或者與其他應用程式中的視窗通訊。

訊息中有什麼?
   
我們給出了上面的註釋,是不是會對訊息結構有了一個比較清楚的認識?如果還沒有,那麼我們再試著給出下面的解釋:
     hwnd 32位的視窗控制程式碼。視窗可以是任何型別的螢幕物件,因為Win32能夠維護大多數可視物件的控制程式碼(視窗、對話方塊、按鈕、編輯框等)。
     message用於區別其他訊息的常量值,這些常量可以是Windows單元中預定義的常量,也可以是自定義的常量。訊息識別符號以常量命名的方式指出訊息的含義。當視窗過程接收到訊息之後,他就會使用訊息識別符號來決定如何處理訊息。例如、WM_PAINT告訴視窗過程窗體客戶區被改變了需要重繪。符號常量指定系統訊息屬於的類別,其字首指明瞭處理解釋訊息的窗體的型別。
     wParam 通常是一個與訊息有關的常量值,也可能是視窗或控制元件的控制程式碼。
     lParam 通常是一個指向記憶體中資料的指標。由於WParam、lParam和Pointer都是32位的,因此,它們之間可以相互轉換。

訊息識別符號的值
     系統保留訊息識別符號的值在0x0000在0x03ff(WM_USER-1)範圍。這些值被系統定義訊息使用。應用程式不能使用這些值給自己的訊息。應用程式訊息從WM_USER(0X0400)到0X7FFF,或0XC000到0XFFFF;WM_USER到 0X7FFF範圍的訊息由應用程式自己使用;0XC000到0XFFFF範圍的訊息用來和其他應用程式通訊,我們順便說一下具有標誌性的訊息值:
     WM_NULL---0x0000    空訊息。
     0x0001----0x0087    主要是視窗訊息。
     0x00A0----0x00A9    非客戶區訊息 
     0x0100----0x0108    鍵盤訊息
     0x0111----0x0126    選單訊息
     0x0132----0x0138    顏色控制訊息
     0x0200----0x020A    滑鼠訊息
     0x0211----0x0213    選單迴圈訊息
     0x0220----0x0230    多文件訊息
     0x03E0----0x03E8    DDE訊息
     0x0400              WM_USER
     0x8000              WM_APP
     0x0400----0x7FFF    應用程式自定義私有訊息

訊息有哪幾種?
   
其實,windows中的訊息雖然很多,但是種類並不繁雜,大體上有3種:視窗訊息、命令訊息和控制元件通知訊息。
     視窗訊息大概是系統中最為常見的訊息,它是指由作業系統和控制其他視窗的視窗所使用的訊息。例如CreateWindow、DestroyWindow和MoveWindow等都會激發視窗訊息,還有我們在上面談到的單擊滑鼠所產生的訊息也是一種視窗訊息。
     命令訊息,這是一種特殊的視窗訊息,他用來處理從一個視窗傳送到另一個視窗的使用者請求,例如按下一個按鈕,他就會向主視窗傳送一個命令訊息。
     控制元件通知訊息,是指這樣一種訊息,一個視窗內的子控制元件發生了一些事情,需要通知父視窗。通知訊息只適用於標準的視窗控制元件如按鈕、列表框、組合框、編輯框,以及Windows公共控制元件如樹狀檢視、列表檢視等。例如,單擊或雙擊一個控制元件、在控制元件中選擇部分文字、操作控制元件的滾動條都會產生通知訊息。她類似於命令訊息,當使用者與控制元件視窗互動時,那麼控制元件通知訊息就會從控制元件視窗傳送到它的主視窗。但是這種訊息的存在並不是為了處理使用者命令,而是為了讓主視窗能夠改變控制元件,例如載入、顯示資料。例如按下一個按鈕,他向父視窗傳送的訊息也可以看作是一個控制元件通知訊息;單擊滑鼠所產生的訊息可以由主視窗直接處理,然後交給控制元件視窗處理。
    其中視窗訊息及控制元件通知訊息主要由視窗類即直接或間接由CWND類派生類處理。相對視窗訊息及控制元件通知訊息而言,命令訊息的處理物件範圍就廣得多,它不僅可以由視窗類處理,還可以由文件類,文件模板類及應用類所處理。
    由於控制元件通知訊息很重要的,人們用的也比較多,但是具體的含義往往令初學者暈頭轉向,所以我決定把常見的幾個列出來供大家參考:
按扭控制元件
BN_CLICKED        使用者單擊了按鈕
 BN_DISABLE 按鈕被禁止
 BN_DOUBLECLICKED  使用者雙擊了按鈕
 BN_HILITE  用/戶加亮了按鈕
 BN_PAINT  按鈕應當重畫
 BN_UNHILITE 加亮應當去掉

組合框控制元件
 CBN_CLOSEUP 組合框的列表框被關閉
 CBN_DBLCLK 使用者雙擊了一個字串
 CBN_DROPDOWN 組合框的列表框被拉出
 CBN_EDITCHANGE 使用者修改了編輯框中的文字
 CBN_EDITUPDATE 編輯框內的文字即將更新
 CBN_ERRSPACE 組合框記憶體不足
 CBN_KILLFOCUS 組合框失去輸入焦點
 CBN_SELCHANGE 在組合框中選擇了一項
 CBN_SELENDCANCEL 使用者的選擇應當被取消
 CBN_SELENDOK 使用者的選擇是合法的
 CBN_SETFOCUS 組合框獲得輸入焦點

編輯框控制元件
 EN_CHANGE 編輯框中的文字己更新
 EN_ERRSPACE 編輯框記憶體不足
 EN_HSCROLL 使用者點選了水平滾動條
 EN_KILLFOCUS 編輯框正在失去輸入焦點
 EN_MAXTEXT 插入的內容被截斷
 EN_SETFOCUS 編輯框獲得輸入焦點
 EN_UPDATE 編輯框中的文字將要更新
 EN_VSCROLL 使用者點選了垂直滾動條訊息含義

列表框控制元件
 LBN_DBLCLK 使用者雙擊了一項
 LBN_ERRSPACE 列表框記憶體不夠
 LBN_KILLFOCUS 列表框正在失去輸入焦點
 LBN_SELCANCEL 選擇被取消
 LBN_SELCHANGE 選擇了另一項
 LBN_SETFOCUS 列表框獲得輸入焦點

佇列訊息和非佇列訊息
   
從訊息的傳送途徑來看,訊息可以分成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訊息。一些函式也傳送非佇列訊息,例如下面我們要談到的函式。
     
訊息的傳送
     瞭解了上面的這些基礎理論之後,我們就可以進行一下簡單的訊息傳送與接收。
     把一個訊息傳送到視窗有3種方式:傳送、寄送和廣播。
     傳送訊息的函式有SendMessage、SendMessageCallback、SendNotifyMessage、 SendMessageTimeout;寄送訊息的函式主要有PostMessage、PostThreadMessage、 PostQuitMessage;廣播訊息的函式我知道的只有BroadcastSystemMessage、 BroadcastSystemMessageEx。
     SendMessage的原型如下:LRESULT SendMessage(HWND hWnd,UINT Msg,WPARAM wParam,LPARAM lParam),這個函式主要是向一個或多個視窗傳送一條訊息,一直等到訊息被處理之後才會返回。不過需要注意的是,如果接收訊息的視窗是同一個應用程式的一部分,那麼這個視窗的視窗函式就被作為一個子程式馬上被呼叫;如果接收訊息的視窗是被另外的執行緒所建立的,那麼視窗系統就切換到相應的執行緒並且呼叫相應的視窗函式,這條訊息不會被放進目標應用程式佇列中。函式的返回值是由接收訊息的視窗的視窗函式返回,返回的值取決於被髮送的訊息。
     PostMessage的原型如下:BOOL PostMessage(HWND hWnd,UINT Msg,WPARAM wParam,LPARAM lParam),該函式把一條訊息放置到建立hWnd視窗的執行緒的訊息佇列中,該函式不等訊息被處理就馬上將控制返回。需要注意的是,如果hWnd引數為 HWND_BROADCAST,那麼,訊息將被寄送給系統中的所有的重疊視窗和彈出視窗,但是子視窗不會收到該訊息;如果hWnd引數為NULL,則該函式類似於將dwThreadID引數設定成當前執行緒的標誌來呼叫PostThreadMEssage函式。
  從上面的這2個具有代表性的函式,我們可以看出訊息的傳送方式和寄送方式的區別所在:被髮送的訊息是否會被立即處理,函式是否立即返回。被髮送的訊息會被立即處理,處理完畢後函式才會返回;被寄送的訊息不會被立即處理,他被放到一個先進先出的佇列中,一直等到應用程式空線的時候才會被處理,不過函式放置訊息後立即返回。

  實際上,傳送訊息到一個視窗處理過程和直接呼叫視窗處理過程之間並沒有太大的區別,他們直接的唯一區別就在於你可以要求作業系統截獲所有被髮送的訊息,但是不能夠截獲對視窗處理過程的直接呼叫。
  以寄送方式傳送的訊息通常是與使用者輸入事件相對應的,因為這些事件不是十分緊迫,可以進行緩慢的緩衝處理,例如滑鼠、鍵盤訊息會被寄送,而按鈕等訊息則會被髮送。
  廣播訊息用得比較少,BroadcastSystemMessage函式原型如下:
      long BroadcastSystemMessage(DWORD dwFlags,LPDWORD lpdwRecipients,UINT uiMessage,WPARAM wParam,LPARAM lParam);該函式可以向指定的接收者傳送一條訊息,這些接收者可以是應用程式、可安裝的驅動程式、網路驅動程式、系統級別的裝置驅動訊息和他們的任意組合。需要注意的是,如果dwFlags引數是BSF_QUERY並且至少一個接收者返回了BROADCAST_QUERY_DENY,則返回值為0,如果沒有指定BSF_QUERY,則函式將訊息傳送給所有接收者,並且忽略其返回值。

訊息的接收
 
訊息的接收主要有3個函式:GetMessage、PeekMessage、WaitMessage。
  GetMessage原型如下:BOOL GetMessage(LPMSG lpMsg,HWND hWnd,UINT wMsgFilterMin,UINT wMsgFilterMax);該函式用來獲取與hWnd引數所指定的視窗相關的且wMsgFilterMin和wMsgFilterMax引數所給出的訊息值範圍內的訊息。需要注意的是,如果hWnd為NULL,則GetMessage獲取屬於呼叫該函式應用程式的任一視窗的訊息,如果 wMsgFilterMin和wMsgFilterMax都是0,則GetMessage就返回所有可得到的訊息。函式獲取之後將刪除訊息佇列中的除 WM_PAINT訊息之外的其他訊息,至於WM_PAINT則只有在其處理之後才被刪除。
   PeekMessage原型如下:BOOL PeekMessage(LPMSG lpMsg,HWND hWnd,UINT wMsgFilterMin,UINT wMsgFilterMax,UINT wRemoveMsg);該函式用於檢視應用程式的訊息佇列,如果其中有訊息就將其放入lpMsg所指的結構中,不過,與GetMessage不同的是,PeekMessage函式不會等到有訊息放入佇列時才返回。同樣,如果hWnd為NULL,則PeekMessage獲取屬於呼叫該函式應用程式的任一視窗的訊息,如果hWnd=-1,那麼函式只返回把hWnd引數為NULL的PostAppMessage函式送去的訊息。如果 wMsgFilterMin和wMsgFilterMax都是0,則PeekMessage就返回所有可得到的訊息。函式獲取之後將視最後一個引數來決定是否刪除訊息佇列中的除 WM_PAINT訊息之外的其他訊息,至於WM_PAINT則只有在其處理之後才被刪除。
   WaitMessage原型如下:BOOL WaitMessage();當一個應用程式無事可做時,該函式就將控制權交給另外的應用程式,同時將該應用程式掛起,直到一個新的訊息被放入應用程式的佇列之中才返回。

訊息的處理
  接下來我們談一下訊息的處理,首先我們來看一下VC中的訊息泵:

while(GetMessage(&msg, NULL, 0, 0))
{
       if(!TranslateAccelerator(msg.hWnd, hAccelTable, &msg))
      
            TranslateMessage(&msg);
            DispatchMessage(&msg);
       }

}

 

   首先,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訊息中呼叫。
 下面我們舉一個常見的小例子來說明這個訊息泵的運用:

if (::PeekMessage(&msg, m_hWnd, WM_KEYFIRST,WM_KEYLAST, PM_REMOVE))
{
          if (msg.message == WM_KEYDOWN && msg.wParam == VK_ESCAPE)...
}


  這裡我們接受所有的鍵盤訊息,所以就用WM_KEYFIRST 和 WM_KEYLAST作為引數。最後一個引數可以是PM_NOREMOVE 或者 PM_REMOVE,表示訊息資訊是否應該從訊息佇列中刪除。                 
   所以這段小程式碼就是判斷是否按下了Esc鍵,如果是就進行處理。

視窗過程
 視窗過程是一個用於處理所有傳送到這個視窗的訊息的函式。任何一個視窗類都有一個視窗過程。同一個類的視窗使用同樣的視窗過程來響應訊息。系統傳送訊息給視窗過程將訊息資料作為引數傳遞給他,訊息到來之後,按照訊息型別排序進行處理,其中的引數則用來區分不同的訊息,視窗過程使用引數產生合適行為。
 一個視窗過程不經常忽略訊息,如果他不處理,它會將訊息傳回到執行預設的處理。視窗過程通過呼叫DefWindowProc來做這個處理。視窗過程必須 return一個值作為它的訊息處理結果。大多數視窗只處理小部分訊息和將其他的通過DefWindowProc傳遞給系統做預設的處理。視窗過程被所有屬於同一個類的視窗共享,能為不同的視窗處理訊息。下面我們來看一下具體的例項:

LRESULT CALLBACK WndProc(HWND hWnd, UINT message, WPARAM wParam, LPARAM lParam)
{
 int wmId, wmEvent;
 PAINTSTRUCT ps;
 HDC hdc;
 TCHAR szHello[MAX_LOADSTRING];
 LoadString(hInst, IDS_HELLO, szHello, MAX_LOADSTRING);

 switch (message) 
 {
  case WM_COMMAND:
         wmId    = LOWORD(wParam); 
         wmEvent = HIWORD(wParam); 
         // Parse the menu selections:
         switch (wmId)
         {
          case IDM_ABOUT:
             DialogBox(hInst, (LPCTSTR)IDD_ABOUTBOX, hWnd, (DLGPROC)About);
             break;
          case IDM_EXIT:
             DestroyWindow(hWnd);
             break;
          default:
             return DefWindowProc(hWnd, message, wParam, lParam);
         }

   break;

  case WM_PAINT:
         hdc = BeginPaint(hWnd, &ps);
         // TODO: Add any drawing code here
         RECT rt;
         GetClientRect(hWnd, &rt);
         DrawText(hdc, szHello, strlen(szHello), &rt, DT_CENTER);
         EndPaint(hWnd, &ps);
         break;

  case WM_DESTROY:
         PostQuitMessage(0);
         break;
  default:
         return DefWindowProc(hWnd, message, wParam, lParam);
  }

  return 0;
}

 

訊息分流器
  通常的視窗過程是通過一個switch語句來實現的,這個事情很煩,有沒有更簡便的方法呢?有,那就是訊息分流器,利用訊息分流器,我們可以把switch語句分成更小的函式,每一個訊息都對應一個小函式,這樣做的好處就是對訊息更容易管理。
  之所以被稱為訊息分流器,就是因為它可以對任何訊息進行分流。下面我們做一個函式就很清楚了:

void MsgCracker(HWND hWnd,int id,HWND hWndCtl,UINT codeNotify)
{
      switch(id)
      {
     case ID_A:
                  if(codeNotify==EN_CHANGE)
                  break;
     case ID_B:
                  if(codeNotify==BN_CLICKED)
                  break;
             .
       }

}


然後我們修改一下視窗過程:

LRESULT CALLBACK WndProc(HWND hWnd, UINT message, WPARAM wParam, LPARAM lParam)
{
       switch(message)
      {
             HANDLE_MSG(hWnd,WM_COMMAND,MsgCracker);
             HANDLE_MSG(hWnd,WM_DESTROY,MsgCracker);
           default:
                    return DefWindowProc(hWnd, message, wParam, lParam);
   }

  return 0;
}


在WindowsX.h中定義瞭如下的HANDLE_MSG巨集:

   #define HANDLE_MSG(hwnd,msg,fn) \
             switch(msg): return HANDLE_##msg((hwnd),(wParam),(lParam),(fn));


實際上,HANDLE_WM_XXXX都是巨集,例如:HANDLE_MSG(hWnd,WM_COMMAND,MsgCracker);將被轉換成如下定義:

   #define HANDLE_WM_COMMAND(hwnd,wParam,lParam,fn)\ 
             ((fn)((hwnd),(int)(LOWORD(wParam)),(HWND)(lParam),(UINT)HIWORD(wParam)),0L);


好了,事情到了這一步,應該一切都明朗了。
不過,我們發現在windowsx.h裡面還有一個巨集:FORWARD_WM_XXXX,我們還是那WM_COMMAND為例,進行分析:

   #define FORWARD_WM_COMMAND(hwnd, id, hwndCtl, codeNotify, fn) \
     (void)(fn)((hwnd), WM_COMMAND, MAKEWPARAM((UINT)(id),(UINT)(codeNotify)), (LPARAM)(HWND)(hwndCtl))


所以實際上,FORWARD_WM_XXXX將訊息引數進行了重新構造,生成了wParam && lParam,然後呼叫了我們定義的函式。

前面,我們分析了訊息的基本理論和基本的函式及用法,接下來,我們將進一步討論訊息傳遞在MFC中的實現。

MFC訊息的處理實現方式
  初看MFC中的各種訊息,以及在頭腦中根深蒂固的C++的影響,我們可能很自然的就會想到利用C++的三大特性之一:虛擬機器制來實現訊息的傳遞,但是經過分析,我們看到事情並不是想我們想象的那樣,在MFC中訊息是通過一種所謂的訊息對映機制來處理的。
  為什麼呢?在潘愛民老師翻譯的《Visual C++技術內幕》(第4版)中給出了詳細的原因說明,我再簡要的說一遍。在CWnd類中大約有110個訊息,還有其它的MFC的類呢,算起來訊息太多了,在C++中對程式中用到的每一個派生類都要有一個vtable,每一個虛擬函式在vtable中都要佔用一個4位元組大小的入口地址,這樣一來,對於每個特定型別的視窗或控制元件,應用程式都需要一個440KB大小的表來支援虛擬訊息控制元件函式。
  如果說上面的視窗或控制元件可以勉強實現的話,那麼對於選單命令訊息及按鈕命令訊息呢?因為不同的應用程式有不同的選單和按鈕,我們怎麼處理呢?在MFC 庫的這種訊息對映系統就避免了使用大的vtable,並且能夠在處理常規Windows訊息的同時處理各種各樣的應用程式的命令訊息。
  說白了,MFC中的訊息機制其實質是一張巨大的訊息及其處理函式的一一對應表,然後加上分析處理這張表的應用框架內部的一些程式程式碼.這樣就可以避免在SDK程式設計中用到的繁瑣的CASE語句。

MFC的訊息對映的基類CCmdTarget
  如果你想讓你的控制元件能夠進行訊息對映,就必須從CCmdTarget類中派生。CCmdTarget類是MFC處理命令訊息的基礎、核心。MFC為該類設計了許多成員函式和一些成員資料,基本上是為了解決訊息對映問題的,所有響應訊息或事件的類都從它派生,例如:應用程式類、框架類、文件類、檢視類和各種各樣的控制元件類等等,還有很多。
不過這個類裡面有2個函式對訊息對映非常重要,一個是靜態成員函式DispatchCmdMsg,另一個是虛擬函式OnCmdMsg。
DispatchCmdMsg專門供MFC內部使用,用來分發Windows訊息。OnCmdMsg用來傳遞和傳送訊息、更新使用者介面物件的狀態。
CCmdTarget對OnCmdMsg的預設實現:在當前命令目標(this所指)的類和基類的訊息對映陣列裡搜尋指定命令訊息的訊息處理函式。
  這裡使用虛擬函式GetMessageMap得到命令目標類的訊息對映入口陣列_messageEntries,然後在陣列裡匹配命令訊息ID相同、控制通知程式碼也相同的訊息對映條目。其中GetMessageMap是虛擬函式,所以可以確認當前命令目標的確切類。
如果找到了一個匹配的訊息對映條目,則使用DispachCmdMsg呼叫這個處理函式;
如果沒有找到,則使用_GetBaseMessageMap得到基類的訊息對映陣列,查詢,直到找到或搜尋了所有的基類(到CCmdTarget)為止;
如果最後沒有找到,則返回FASLE。
  每個從CCmdTarget派生的命令目標類都可以覆蓋OnCmdMsg,利用它來確定是否可以處理某條命令,如果不能,就通過呼叫下一命令目標的 OnCmdMsg,把該命令送給下一個命令目標處理。通常,派生類覆蓋OnCmdMsg時,要呼叫基類的被覆蓋的OnCmdMsg。
  在MFC框架中,一些MFC命令目標類覆蓋了OnCmdMsg,如框架視窗類覆蓋了該函式,實現了MFC的標準命令訊息傳送路徑。必要的話,應用程式也可以覆蓋OnCmdMsg,改變一個或多個類中的傳送規定,實現與標準框架傳送規定不同的傳送路徑。例如,在以下情況可以作這樣的處理:在要打斷髮送順序的類中把命令傳給一個非MFC預設物件;在新的非預設物件中或在可能要傳出命令的命令目標中。

訊息對映的內容
    通過ClassWizard為我們生成的程式碼,我們可以看到,訊息對映基本上分為2大部分:
    在標頭檔案(.h)中有一個巨集DECLARE_MESSAGE_MAP(),他被放在了類的末尾,是一個public屬性的;與之對應的是在實現部分(.cpp)增加了一章訊息對映表,內容如下:
    BEGIN_MESSAGE_MAP(當前類, 當前類的基類)
       //{{AFX_MSG_MAP(CMainFrame)
         訊息的入口項
       //}}AFX_MSG_MAP
   END_MESSAGE_MAP()
   但是僅是這兩項還遠不足以完成一條訊息,要是一個訊息工作,必須有以下3個部分去協作:
1.在類的定義中加入相應的函式宣告;
2.在類的訊息對映表中加入相應的訊息對映入口項;
3.在類的實現中加入相應的函式體;

訊息的新增
   有了上面的這些只是作為基礎,我們接下來就做我們最熟悉、最常用的工作:新增訊息。MFC訊息的新增主要有2種方法:自動/手動,我們就以這2種方法為例,說一下如何新增訊息。
   1、利用Class Wizard實現自動新增
      在選單中選擇View-->Class Wizard,也可以用單擊滑鼠右鍵,選擇Class Wizard,同樣可以啟用Class Wizard。選擇Message Map標籤,從Class name組合框中選取我們想要新增訊息的類。在Object IDs列表框中,選取類的名稱。此時, Messages列表框顯示該類的大多數(若不是全部的話)可過載成員函式和視窗訊息。類過載顯示在列表的上部,以實際虛構成員函式的大小寫字母來表示。其他為視窗訊息,以大寫字母出現,描述了實際視窗所能響應的訊息ID。選中我們向新增的訊息,單擊Add Function按鈕,Class Wizard自動將該訊息新增進來。
      有時候,我們想要新增的訊息本應該出現在Message列表中,可是就是找不到,怎麼辦?不要著急,我們可以利用Class Wizard上Class Info標籤以擴充套件訊息列表。在該頁中,找到Message Filter組合框,通過它可以改變首頁中Messages列表框中的選項。這裡,我們選擇Window,從而顯示所有的視窗訊息,一把情況下,你想要新增的訊息就可以在Message列表框中出現了,如果還沒有,那就接著往下看:)

   2、手動地新增訊息處理函式
    如果在Messages列表框中仍然看不到我們想要的訊息,那麼該訊息可能是被系統忽略掉或者是你自己建立的,在這種情況下,就必須自己手工新增。根據我們前面所說的訊息工作的3個部件,我們一一進行處理:
      1) 在類的. h檔案中新增處理函式的宣告,緊接在//}}AFX_MSG行之後加入宣告,注意:一定要以afx_msg開頭。
     通常,新增處理函式宣告的最好的地方是原始碼中Class Wizard維護的表下面,但是在它標記其領域的{{}}括弧外面。這些括弧中的任何東西都將會被Class Wizard銷燬。
      2) 接著,在使用者類的.cpp檔案中找到//}}AFX_MSG_MAP行,緊接在它之後加入訊息入口項。同樣,也是放在{ {} }的外面
      3) 最後,在該檔案中新增訊息處理函式的實體。

【轉自】http://www.cppblog.com/suiaiguo/archive/2009/07/18/90412.html

相關文章