MFC訊息處理學習總結
阿新 • • 發佈:2019-01-24
Windows訊息機制概述
http://www.cppblog.com/suiaiguo/archive/2009/07/18/90412.html訊息是指什麼?
訊息系統對於一個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) 最後,在該檔案中新增訊息處理函式的實體。
========
MFC的訊息機制的實現原理和訊息處理的過程
下面幾節將分析MFC的訊息機制的實現原理和訊息處理的過程。為此,首先要分析ClassWizard實現訊息對映的內幕,然後討論MFC的視窗過程,分析MFC視窗過程是如何實現訊息處理的。
訊息對映的定義和實現
MFC處理的三類訊息
根據處理函式和處理過程的不同,MFC主要處理三類訊息:
Windows訊息,字首以“WM_”打頭,WM_COMMAND例外。Windows訊息直接送給MFC視窗過程處理,視窗過程呼叫對應的訊息處理函式。一般,由視窗物件來處理這類訊息,也就是說,這類訊息處理函式一般是MFC視窗類的成員函式。
控制通知訊息,是控制子視窗送給父視窗的WM_COMMAND通知訊息。視窗過程呼叫對應的訊息處理函式。一般,由視窗物件來處理這類訊息,也就是說,這類訊息處理函式一般是MFC視窗類的成員函式。
需要指出的是,Win32使用新的WM_NOFITY來處理複雜的通知訊息。WM_COMMAND型別的通知訊息僅僅能傳遞一個控制視窗控制代碼(lparam)、控制窗ID和通知程式碼(wparam)。WM_NOTIFY能傳遞任意複雜的資訊。
命令訊息,這是來自選單、工具條按鈕、加速鍵等使用者介面物件的WM_COMMAND通知訊息,屬於應用程式自己定義的訊息。通過訊息對映機制,MFC框架把命令按一定的路徑分發給多種型別的物件(具備訊息處理能力)處理,如文件、視窗、應用程式、文件模板等物件。能處理訊息對映的類必須從CCmdTarget類派生。
在討論了訊息的分類之後,應該是討論各類訊息如何處理的時候了。但是,要知道怎麼處理訊息,首先要知道如何對映訊息。
MFC訊息對映的實現方法
MFC使用ClassWizard幫助實現訊息對映,它在原始碼中新增一些訊息對映的內容,並宣告和實現訊息處理函式。現在來分析這些被新增的內容。
在類的定義(標頭檔案)裡,它增加了訊息處理函式宣告,並新增一行宣告訊息對映的巨集DECLARE_MESSAGE_MAP。
在類的實現(實現檔案)裡,實現訊息處理函式,並使用IMPLEMENT_MESSAGE_MAP巨集實現訊息對映。一般情況下,這些宣告和實現是由MFC的ClassWizard自動來維護的。看一個例子:
在AppWizard產生的應用程式類的原始碼中,應用程式類的定義(標頭檔案)包含了類似如下的程式碼:
//{{AFX_MSG(CTttApp)
afx_msg void OnAppAbout();
//}}AFX_MSG
DECLARE_MESSAGE_MAP()
應用程式類的實現檔案中包含了類似如下的程式碼:
BEGIN_MESSAGE_MAP(CTApp, CWinApp)
//{{AFX_MSG_MAP(CTttApp)
ON_COMMAND(ID_APP_ABOUT, OnAppAbout)
//}}AFX_MSG_MAP
END_MESSAGE_MAP()
標頭檔案裡是訊息對映和訊息處理函式的宣告,實現檔案裡是訊息對映的實現和訊息處理函式的實現。它表示讓應用程式物件處理命令訊息ID_APP_ABOUT,訊息處理函式是OnAppAbout。
為什麼這樣做之後就完成了一個訊息對映?這些宣告和實現到底作了些什麼呢?接著,將討論這些問題。
在宣告與實現的內部
DECLARE_MESSAGE_MAP巨集:
首先,看DECLARE_MESSAGE_MAP巨集的內容:
#ifdef _AFXDLL
#define DECLARE_MESSAGE_MAP() \
private: \
static const AFX_MSGMAP_ENTRY _messageEntries[]; \
protected: \
static AFX_DATA const AFX_MSGMAP messageMap; \
static const AFX_MSGMAP* PASCAL _GetBaseMessageMap(); \
virtual const AFX_MSGMAP* GetMessageMap() const; \
#else
#define DECLARE_MESSAGE_MAP() \
private: \
static const AFX_MSGMAP_ENTRY _messageEntries[]; \
protected: \
static AFX_DATA const AFX_MSGMAP messageMap; \
virtual const AFX_MSGMAP* GetMessageMap() const; \
#endif
DECLARE_MESSAGE_MAP定義了兩個版本,分別用於靜態或者動態連結到MFC DLL的情形。
BEGIN_MESSAE_MAP巨集
然後,看BEGIN_MESSAE_MAP巨集的內容:
#ifdef _AFXDLL
#define BEGIN_MESSAGE_MAP(theClass, baseClass) \
const AFX_MSGMAP* PASCAL theClass::_GetBaseMessageMap() \
{ return &baseClass::messageMap; } \
const AFX_MSGMAP* theClass::GetMessageMap() const \
{ return &theClass::messageMap; } \
AFX_DATADEF const AFX_MSGMAP theClass::messageMap = \
{ &theClass::_GetBaseMessageMap,&theClass::_messageEntries[0] }; \
const AFX_MSGMAP_ENTRY theClass::_messageEntries[] = \
{ \
#else
#define BEGIN_MESSAGE_MAP(theClass, baseClass) \
const AFX_MSGMAP* theClass::GetMessageMap() const \
{ return &theClass::messageMap; } \
AFX_DATADEF const AFX_MSGMAP theClass::messageMap = \
{ &baseClass::messageMap,&theClass::_messageEntries[0] }; \
const AFX_MSGMAP_ENTRY theClass::_messageEntries[] = \
{ \
#endif
#define END_MESSAGE_MAP() \
{0, 0, 0, 0, AfxSig_end, (AFX_PMSG)0 } \
}; \
對應地,BEGIN_MESSAGE_MAP定義了兩個版本,分別用於靜態或者動態連結到MFC DLL的情形。END_MESSAGE_MAP相對簡單,就只有一種定義。
ON_COMMAND巨集
最後,看ON_COMMAND巨集的內容:
#define ON_COMMAND(id, memberFxn) \
{\
WM_COMMAND,\
CN_COMMAND,\
(WORD)id,\
(WORD)id,\
AfxSig_vv,\
(AFX_PMSG)memberFxn\
};
訊息對映宣告的解釋
在清楚了有關巨集的定義之後,現在來分析它們的作用和功能。
訊息對映宣告的實質是給所在類新增幾個靜態成員變數和靜態或虛擬函式,當然它們是與訊息對映相關的變數和函式。
成員變數
有兩個成員變數被新增,第一個是_messageEntries,第二個是messageMap。
第一個成員變數的宣告:
AFX_MSGMAP_ENTRY_messageEntries[]
這是一個AFX_MSGMAP_ENTRY型別的陣列變數,是一個靜態成員變數,用來容納類的訊息對映條目。一個訊息對映條目可以用AFX_MSGMAP_ENTRY結構來描述。
AFX_MSGMAP_ENTRY結構的定義如下:
struct AFX_MSGMAP_ENTRY
{
//Windows訊息ID
UINT nMessage;
//控制訊息的通知碼
UINT nCode;
//Windows Control的ID
UINT nID;
//如果是一定範圍的訊息被對映,則nLastID指定其範圍
UINT nLastID;
UINT nSig;//訊息的動作標識
//響應訊息時應執行的函式(routine to call (orspecial value))
AFX_PMSG pfn;
};
從上述結構可以看出,每條對映有兩部分的內容:第一部分是關於訊息ID的,包括前四個域;第二部分是關於訊息對應的執行函式,包括後兩個域。
在上述結構的六個域中,pfn是一個指向CCmdTarger成員函式的指標。函式指標的型別定義如下:
typedef void (AFX_MSG_CALL CCmdTarget::*AFX_PMSG)(void);
當使用一條或者多條訊息對映條目初始化訊息對映陣列時,各種不同型別的訊息函式都被轉換成這樣的型別:不接收引數,也不返回引數的型別。因為所有可以有訊息對映的類都是從CCmdTarge派生的,所以可以實現這樣的轉換。
nSig是一個標識變數,用來標識不同原型的訊息處理函式,每一個不同原型的訊息處理函式對應一個不同的nSig。在訊息分發時,MFC內部根據nSig把訊息派發給對應的成員函式處理,實際上,就是根據nSig的值把pfn還原成相應型別的訊息處理函式並執行它。
第二個成員變數的宣告
AFX_MSGMAP messageMap;
這是一個AFX_MSGMAP型別的靜態成員變數,從其型別名稱和變數名稱可以猜出,它是一個包含了訊息對映資訊的變數。的確,它把訊息對映的資訊(訊息對映陣列)和相關函式打包在一起,也就是說,得到了一個訊息處理類的該變數,就得到了它全部的訊息對映資料和功能。AFX_MSGMAP結構的定義如下:
struct AFX_MSGMAP
{
//得到基類的訊息對映入口地址的資料或者函式
#ifdef _AFXDLL
//pfnGetBaseMap指向_GetBaseMessageMap函式
const AFX_MSGMAP* (PASCAL* pfnGetBaseMap)();
#else
//pBaseMap儲存基類訊息對映入口_messageEntries的地址
const AFX_MSGMAP* pBaseMap;
#endif
//lpEntries儲存訊息對映入口_messageEntries的地址
const AFX_MSGMAP_ENTRY* lpEntries;
};
從上面的定義可以看出,通過messageMap可以得到類的訊息對映陣列_messageEntries和函式_GetBaseMessageMap的地址(不使用MFC DLL時,是基類訊息對映陣列的地址)。
成員函式
_GetBaseMessageMap()
用來得到基類訊息對映的函式。
GetMessageMap()
用來得到自身訊息對映的函式。
訊息對映實現的解釋
訊息對映實現的實質是初始化宣告中定義的靜態成員函式_messageEntries和messageMap,實現所宣告的靜態或虛擬函式GetMessageMap、_GetBaseMessageMap。
這樣,在進入WinMain函式之前,每個可以響應訊息的MFC類都生成了一個訊息對映表,程式執行時通過查詢該表判斷是否需要響應某條訊息。
對訊息對映入口表(訊息對映陣列)的初始化
如前所述,訊息對映陣列的元素是訊息對映條目,條目的格式符合結構AFX_MESSAGE_ENTRY的描述。所以,要初始化訊息對映陣列,就必須使用符合該格式的資料來填充:如果指定當前類處理某個訊息,則把和該訊息有關的資訊(四個)和訊息處理函式的地址及原型組合成為一個訊息對映條目,加入到訊息對映陣列中。
顯然,這是一個繁瑣的工作。為了簡化操作,MFC根據訊息的不同和訊息處理方式的不同,把訊息對映劃分成若干類別,每一類的訊息對映至少有一個共性:訊息處理函式的原型相同。對每一類訊息對映,MFC定義了一個巨集來簡化初始化訊息陣列的工作。例如,前文提到的ON_COMMAND巨集用來對映命令訊息,只要指定命令ID和訊息處理函式即可,因為對這類命令訊息對映條目,其他四個屬性都是固定的。ON_COMMAND巨集的初始化內容如下:
{WM_COMMAND,
CN_COMMAND,
(WORD)ID_APP_ABOUT,
(WORD)ID_APP_ABOUT,
AfxSig_vv,
(AFX_PMSG)OnAppAbout
}
這個訊息對映條目的含義是:訊息ID是ID_APP_ABOUT,OnAppAbout被轉換成AFX_PMSG指標型別,AfxSig_vv是MFC預定義的列舉變數,用來標識OnAppAbout的函式型別為引數空(Void)、返回空(Void)。
在訊息對映陣列的最後,是巨集END_MESSAGE_MAP的內容,它標識訊息處理類的訊息對映條目的終止。
對messageMap的初始化
如前所述,messageMap的型別是AFX_MESSMAP。
經過初始化,域lpEntries儲存了訊息對映陣列_messageEntries的地址;如果動態連結到MFC DLL,則pfnGetBaseMap儲存了_GetBaseMessageMap成員函式的地址;否則pBaseMap儲存了基類的訊息對映陣列的地址。
對函式的實現
_GetBaseMessageMap()
它返回基類的成員變數messagMap(當使用MFC DLL時),使用該函式得到基類訊息對映入口表。
GetMessageMap():
它返回成員變數messageMap,使用該函式得到自身訊息對映入口表。
順便說一下,訊息對映類的基類CCmdTarget也實現了上述和訊息對映相關的函式,不過,它的訊息對映陣列是空的。
既然訊息對映巨集方便了訊息對映的實現,那麼有必要詳細的討論訊息對映巨集。下一節,介紹訊息對映巨集的分類、用法和用途。
訊息對映巨集的種類
為了簡化程式設計師的工作,MFC定義了一系列的訊息對映巨集和像AfxSig_vv這樣的列舉變數,以及標準訊息處理函式,並且具體地實現這些函式。這裡主要討論訊息對映巨集,常用的分為以下幾類。
用於Windows訊息的巨集,字首為“ON_WM_”。
這樣的巨集不帶引數,因為它對應的訊息和訊息處理函式的函式名稱、函式原型是確定的。MFC提供了這類訊息處理函式的定義和預設實現。每個這樣的巨集處理不同的Windows訊息。
例如:巨集ON_WM_CREATE()把訊息WM_CREATE對映到OnCreate函式,訊息對映條目的第一個成員nMessage指定為要處理的Windows訊息的ID,第二個成員nCode指定為0。
用於命令訊息的巨集ON_COMMAND
這類巨集帶有引數,需要通過引數指定命令ID和訊息處理函式。這些訊息都對映到WM_COMMAND上,也就是將訊息對映條目的第一個成員nMessage指定為WM_COMMAND,第二個成員nCode指定為CN_COMMAND(即0)。訊息處理函式的原型是void (void),不帶引數,不返回值。
除了單條命令訊息的對映,還有把一定範圍的命令訊息對映到一個訊息處理函式的對映巨集ON_COMMAND_RANGE。這類巨集帶有引數,需要指定命令ID的範圍和訊息處理函式。這些訊息都對映到WM_COMMAND上,也就是將訊息對映條目的第一個成員nMessage指定為WM_COMMAND,第二個成員nCode指定為CN_COMMAND(即0),第三個成員nID和第四個成員nLastID指定了對映訊息的起止範圍。訊息處理函式的原型是void (UINT),有一個UINT型別的引數,表示要處理的命令訊息ID,不返回值。
(3)用於控制通知訊息的巨集
這類巨集可能帶有三個引數,如ON_CONTROL,就需要指定控制視窗ID,通知碼和訊息處理函式;也可能帶有兩個引數,如具體處理特定通知訊息的巨集ON_BN_CLICKED、ON_LBN_DBLCLK、ON_CBN_EDITCHANGE等,需要指定控制視窗ID和訊息處理函式。
控制通知訊息也被對映到WM_COMMAND上,也就是將訊息對映條目的第一個成員的nMessage指定為WM_COMMAND,但是第二個成員nCode是特定的通知碼,第三個成員nID是控制子視窗的ID,第四個成員nLastID等於第三個成員的值。訊息處理函式的原型是void (void),沒有引數,不返回值。
還有一類巨集處理通知訊息ON_NOTIFY,它類似於ON_CONTROL,但是控制通知訊息被對映到WM_NOTIFY。訊息對映條目的第一個成員的nMessage被指定為WM_NOTIFY,第二個成員nCode是特定的通知碼,第三個成員nID是控制子視窗的ID,第四個成員nLastID等於第三個成員的值。訊息處理函式的原型是void (NMHDR*, LRESULT*),引數1是NMHDR指標,引數2是LRESULT指標,用於返回結果,但函式不返回值。
對應地,還有把一定範圍的控制子視窗的某個通知訊息對映到一個訊息處理函式的對映巨集,這類巨集包括ON__CONTROL_RANGE和ON_NOTIFY_RANGE。這類巨集帶有引數,需要指定控制子視窗ID的範圍和通知訊息,以及訊息處理函式。
對於ON__CONTROL_RANGE,是將訊息對映條目的第一個成員的nMessage指定為WM_COMMAND,但是第二個成員nCode是特定的通知碼,第三個成員nID和第四個成員nLastID等於指定了控制視窗ID的範圍。訊息處理函式的原型是void (UINT),引數表示要處理的通知訊息是哪個ID的控制子視窗傳送的,函式不返回值。
對於ON__NOTIFY_RANGE,訊息對映條目的第一個成員的nMessage被指定為WM_NOTIFY,第二個成員nCode是特定的通知碼,第三個成員nID和第四個成員nLastID指定了控制視窗ID的範圍。訊息處理函式的原型是void (UINT, NMHDR*, LRESULT*),引數1表示要處理的通知訊息是哪個ID的控制子視窗傳送的,引數2是NMHDR指標,引數3是LRESULT指標,用於返回結果,但函式不返回值。
(4)用於使用者介面介面狀態更新的ON_UPDATE_COMMAND_UI巨集
這類巨集被對映到訊息WM_COMMND上,帶有兩個引數,需要指定使用者介面物件ID和訊息處理函式。訊息對映條目的第一個成員nMessage被指定為WM_COMMAND,第二個成員nCode被指定為-1,第三個成員nID和第四個成員nLastID都指定為使用者介面物件ID。訊息處理函式的原型是 void (CCmdUI*),引數指向一個CCmdUI物件,不返回值。
對應地,有更新一定ID範圍的使用者介面物件的巨集ON_UPDATE_COMMAND_UI_RANGE,此巨集帶有三個引數,用於指定使用者介面物件ID的範圍和訊息處理函式。訊息對映條目的第一個成員nMessage被指定為WM_COMMAND,第二個成員nCode被指定為-1,第三個成員nID和第四個成員nLastID用於指定使用者介面物件ID的範圍。訊息處理函式的原型是 void (CCmdUI*),引數指向一個CCmdUI物件,函式不返回值。之所以不用當前使用者介面物件ID作為引數,是因為CCmdUI物件包含了有關資訊。
(5)用於其他訊息的巨集
例如用於使用者定義訊息的ON_MESSAGE。這類巨集帶有引數,需要指定訊息ID和訊息處理函式。訊息對映條目的第一個成員nMessage被指定為訊息ID,第二個成員nCode被指定為0,第三個成員nID和第四個成員也是0。訊息處理的原型是LRESULT (WPARAM, LPARAM),引數1和引數2是訊息引數wParam和lParam,返回LRESULT型別的值。
(6)擴充套件訊息對映巨集
很多普通訊息對映巨集都有對應的擴充套件訊息對映巨集,例如:ON_COMMAND對應的ON_COMMAND_EX,ON_ONTIFY對應的ON_ONTIFY_EX,等等。擴充套件巨集除了具有普通巨集的功能,還有特別的用途。關於擴充套件巨集的具體討論和分析,見4.4.3.2節。
作為一個總結,下表列出了這些常用的訊息對映巨集。
表4-1 常用的訊息對映巨集
訊息對映巨集
用途
ON_COMMAND
把command message對映到相應的函式
ON_CONTROL
把control notification message對映到相應的函式。MFC根據不同的控制訊息,在此基礎上定義了更具體的巨集,這樣使用者在使用時就不需要指定通知程式碼ID,如ON_BN_CLICKED。
ON_MESSAGE
把user-defined message.對映到相應的函式
ON_REGISTERED_MESSAGE
把registered user-defined message對映到相應的函式,實際上nMessage等於0x0C000,nSig等於巨集的訊息引數。nSig的真實值為Afxsig_lwl。
ON_UPDATE_COMMAND_UI
把user interface user update command message對映到相應的函式上。
ON_COMMAND_RANGE
把一定範圍內的command IDs 對映到相應的函式上
ON_UPDATE_COMMAND_UI_RANGE
把一定範圍內的user interface user update command message對映到相應的函式上
ON_CONTROL_RANGE
把一定範圍內的control notification message對映到相應的函式上
在表4-1中,巨集ON_REGISTERED_MESSAGE的定義如下:
#define ON_REGISTERED_MESSAGE(nMessageVariable, memberFxn) \
{ 0xC000, 0, 0, 0,\
(UINT)(UINT*)(&nMessageVariable), \
/*implied 'AfxSig_lwl'*/ \
(AFX_PMSG)(AFX_PMSGW)(LRESULT\
(AFX_MSG_CALL CWnd::*)\
(WPARAM, LPARAM))&memberFxn }
從上面的定義可以看出,實際上,該訊息被對映到WM_COMMAND(0XC000),指定的registered訊息ID存放在nSig域內,nSig的值在這樣的對映條目下隱含地定為AfxSig_lwl。由於ID和正常的nSig域存放的值範圍不同,所以MFC可以判斷出是否是registered訊息對映條目。如果是,則使用AfxSig_lwl把訊息處理函式轉換成引數1為Word、引數2為long、返回值為long的型別。
在介紹完了訊息對映的內幕之後,應該討論訊息處理過程了。由於CCmdTarge的特殊性和重要性,在4.3節先對其作一個大略的介紹。
CcmdTarget類
除了CObject類外,還有一個非常重要的類CCmdTarget。所有響應訊息或事件的類都從它派生。例如,CWinapp,CWnd,CDocument,CView,CDocTemplate,CFrameWnd,等等。
CCmdTarget類是MFC處理命令訊息的基礎、核心。MFC為該類設計了許多成員函式和一些成員資料,基本上是為了解決訊息對映問題的,而且,很大一部分是針對OLE設計的。在OLE應用中,CCmdTarget是MFC處理模組狀態的重要環節,它起到了傳遞模組狀態的作用:其建構函式獲取當前模組狀態,並儲存在成員變數m_pModuleState裡頭。關於模組狀態,在後面章節講述。
CCmdTarget有兩個與訊息對映有密切關係的成員函式:DispatchCmdMsg和OnCmdMsg。
靜態成員函式DispatchCmdMsg
CCmdTarget的靜態成員函式DispatchCmdMsg,用來分發Windows訊息。此函式是MFC內部使用的,其原型如下:
static BOOL DispatchCmdMsg(
CCmdTarget* pTarget,
UINT nID,
int nCode,
AFX_PMSG pfn,
void* pExtra,
UINT nSig,
AFX_CMDHANDLERINFO* pHandlerInfo)
關於此函式將在4.4.3.2章節命令訊息的處理中作更詳細的描述。
虛擬函式OnCmdMsg
CCmdTarget的虛擬函式OnCmdMsg,用來傳遞和傳送訊息、更新使用者介面物件的狀態,其原型如下:
OnCmdMsg(
UINT nID,
int nCode,
void* pExtra,
AFX_CMDHANDLERINFO* pHandlerInfo)
框架的命令訊息傳遞機制主要是通過該函式來實現的。其引數描述參見4.3.3.2章節DispacthCMdMessage的引數描述。
在本書中,命令目標指希望或者可能處理訊息的物件;命令目標類指命令目標的類。
CCmdTarget對OnCmdMsg的預設實現:在當前命令目標(this所指)的類和基類的訊息對映數組裡搜尋指定命令訊息的訊息處理函式(標準Windows訊息不會送到這裡處理)。
這裡使用虛擬函式GetMessageMap得到命令目標類的訊息對映入口陣列_messageEntries,然後在數組裡匹配指定的訊息對映條目。匹配標準:命令訊息ID相同,控制通知程式碼相同。因為GetMessageMap是虛擬函式,所以可以確認當前命令目標的確切類。
如果找到了一個匹配的訊息對映條目,則使用DispachCmdMsg呼叫這個處理函式;
如果沒有找到,則使用_GetBaseMessageMap得到基類的訊息對映陣列,查詢,直到找到或搜尋了所有的基類(到CCmdTarget)為止;
如果最後沒有找到,則返回FASLE。
每個從CCmdTarget派生的命令目標類都可以覆蓋OnCmdMsg,利用它來確定是否可以處理某條命令,如果不能,就通過呼叫下一命令目標的OnCmdMsg,把該命令送給下一個命令目標處理。通常,派生類覆蓋OnCmdMsg時,要呼叫基類的被覆蓋的OnCmdMsg。
在MFC框架中,一些MFC命令目標類覆蓋了OnCmdMsg,如框架視窗類覆蓋了該函式,實現了MFC的標準命令訊息傳送路徑。具體實現見後續章節。
必要的話,應用程式也可以覆蓋OnCmdMsg,改變一個或多個類中的傳送規定,實現與標準框架傳送規定不同的傳送路徑。例如,在以下情況可以作這樣的處理:在要打斷髮送順序的類中把命令傳給一個非MFC預設物件;在新的非預設物件中或在可能要傳出命令的命令目標中。
本節對CCmdTarget的兩個成員函式作一些討論,是為了對MFC的訊息處理有一個大致印象。後面4.4.3.2節和4.4.3.3節將作進一步的討論。
MFC視窗過程
前文曾經提到,所有的訊息都送給視窗過程處理,MFC的所有視窗都使用同一視窗過程,訊息或者直接由視窗過程呼叫相應的訊息處理函式處理,或者按MFC命令訊息派發路徑送給指定的命令目標處理。
那麼,MFC的視窗過程是什麼?怎麼處理標準Windows訊息?怎麼實現命令訊息的派發?這些都將是下文要回答的問題。
MFC視窗過程的指定
從前面的討論可知,每一個“視窗類”都有自己的視窗過程。正常情況下使用該“視窗類”建立的視窗都使用它的視窗過程。
MFC的視窗物件在建立HWND視窗時,也使用了已經註冊的“視窗類”,這些“視窗類”或者使用應用程式提供的視窗過程,或者使用Windows提供的視窗過程(例如Windows控制視窗、對話方塊等)。那麼,為什麼說MFC建立的所有HWND視窗使用同一個視窗過程呢?
在MFC中,的確所有的視窗都使用同一個視窗過程:AfxWndProc或AfxWndProcBase(如果定義了_AFXDLL)。它們的原型如下:
LRESULT CALLBACK
AfxWndProc(HWND hWnd, UINT nMsg, WPARAM wParam, LPARAMlParam)
LRESULT CALLBACK
AfxWndProcBase(HWND hWnd, UINT nMsg, WPARAM wParam,LPARAM lParam)
這兩個函式的原型都如4.1.1節描述的視窗過程一樣。
如果動態連結到MFC DLL(定義了_AFXDLL),則AfxWndProcBase被用作視窗過程,否則AfxWndProc被用作視窗過程。AfxWndProcBase首先使用巨集AFX_MANAGE_STATE設定正確的模組狀態,然後呼叫AfxWndProc。
下面,假設不使用MFC DLL,討論MFC如何使用AfxWndProc取代各個視窗的原視窗過程。
視窗過程的取代發生在視窗建立的過程時,使用了子類化(Subclass)的方法。所以,從視窗的建立過程來考察取代過程。從前面可以知道,視窗建立最終是通過呼叫CWnd::CreateEx函式完成的,分析該函式的流程,如圖4-1所示。
圖4-1中的CREATESTRUCT結構型別的變數cs包含了傳遞給視窗過程的初始化引數。CREATESTRUCT結構描述了建立視窗所需要的資訊,定義如下:
typedef struct tagCREATESTRUCT {
LPVOID lpCreateParams; //用來建立視窗的資料
HANDLE hInstance; //建立視窗的例項
HMENU hMenu; //視窗選單
HWND hwndParent; //父視窗
int cy; //高度
int cx; //寬度
int y; //原點Y座標
int x;//原點X座標
LONG style; //視窗風格
LPCSTR lpszName; //視窗名
LPCSTR lpszClass; //視窗類
DWORD dwExStyle; //視窗擴充套件風格
} CREATESTRUCT;
cs表示的建立引數可以在建立視窗之前被程式設計師修改,程式設計師可以覆蓋當前視窗類的虛擬成員函式PreCreateWindow,通過該函式來修改cs的style域,改變視窗風格。這裡cs的主要作用是儲存建立視窗的各種資訊,::CreateWindowEx函式使用cs的各個域作為引數來建立視窗,關於該函式見2.2.2節。
在建立視窗之前,建立了一個WH_CBT型別的鉤子(Hook)。這樣,建立視窗時所有的訊息都會被鉤子過程函式_AfxCbtFilterHook截獲。
AfxCbtFilterHook函式首先檢查是不是希望處理的Hook──HCBT_CREATEWND。如果是,則先把MFC視窗物件(該物件必須已經建立了)和剛剛建立的Windows視窗物件捆綁在一起,建立它們之間的對映(見後面模組-執行緒狀態);然後,呼叫::SetWindowLong設定視窗過程為AfxWndProc,並儲存原視窗過程在視窗類成員變數m_pfnSuper中,這樣形成一個視窗過程鏈。需要的時候,原視窗過程地址可以通過視窗類成員函式GetSuperWndProcAddr得到。
這樣,AfxWndProc就成為CWnd或其派生類的視窗過程。不論佇列訊息,還是非佇列訊息,都送到AfxWndProc視窗過程來處理(如果使用MFC DLL,則AfxWndProcBase被呼叫,然後是AfxWndProc)。經過訊息分發之後沒有被處理的訊息,將送給原視窗過程處理。
最後,有一點可能需要解釋:為什麼不直接指定視窗過程為AfxWndProc,而要這麼大費周折呢?這是因為原視窗過程(“視窗類”指定的視窗過程)常常是必要的,是不可缺少的。
接下來,討論AfxWndProc視窗過程如何使用訊息對映資料實現訊息對映。Windows訊息和命令訊息的處理不一樣,前者沒有訊息分發的過程。
對Windows訊息的接收和處理
Windows訊息送給AfxWndProc視窗過程之後,AfxWndProc得到HWND視窗對應的MFC視窗物件,然後,搜尋該MFC視窗物件和其基類的訊息對映陣列,判定它們是否處理當前訊息,如果是則呼叫對應的訊息處理函式,否則,進行預設處理。
下面,以一個應用程式的視視窗建立時,對WM_CREATE訊息的處理為例,詳細地討論Windows訊息的分發過程。
用第一章的例子,類CTview要處理WM_CREATE訊息,使用ClassWizard加入訊息處理函式CTview::OnCreate。下面,看這個函式怎麼被呼叫:
視視窗最終呼叫::CreateEx函式來建立。由Windows系統傳送WM_CREATE訊息給視的視窗過程AfxWndProc,引數1是建立的視視窗的控制代碼,引數2是訊息ID(WM_CREATE),引數3、4是訊息引數。圖4-2描述了其餘的處理過程。圖中函式的類屬限制並非原始碼中所具有的,而是根據處理過程得出的判斷。例如,“CWnd::WindowProc”表示CWnd類的虛擬函式WindowProc被呼叫,並不一定當前物件是CWnd類的例項,事實上,它是CWnd派生類CTview類的例項;而“CTview::OnCreate”表示CTview的訊息處理函式OnCreate被呼叫。下面描述每一步的詳細處理。
從視窗過程到訊息對映
首先,分析AfxWndProc視窗過程函式。
AfxWndProc的原型如下:
LRESULT AfxWndProc(HWND hWnd,
UINT nMsg, WPARAM wParam, LPARAM lParam)
如果收到的訊息nMsg不是WM_QUERYAFXWNDPROC(該訊息被MFC內部用來確認視窗過程是否使用AfxWndProc),則從hWnd得到對應的MFC Windows物件(該物件必須已存在,是永久性<Permanent>物件)指標pWnd。pWnd所指的MFC視窗物件將負責完成訊息的處理。這裡,pWnd所指示的物件是MFC視視窗物件,即CTview物件。
然後,把pWnd和AfxWndProc接受的四個引數傳遞給函式AfxCallWndProc執行。
AfxCallWndProc原型如下:
LRESULT AFXAPI AfxCallWndProc(CWnd* pWnd, HWND hWnd,
UINT nMsg, WPARAM wParam = 0, LPARAM lParam = 0)
MFC使用AfxCallWndProc函式把訊息送給CWnd類或其派生類的物件。該函式主要是把訊息和訊息引數(nMsg、wParam、lParam)傳遞給MFC視窗物件的成員函式WindowProc(pWnd->WindowProc)作進一步處理。如果是WM_INITDIALOG訊息,則在呼叫WindowProc前後要作一些處理。
WindowProc的函式原型如下:
LRESULT CWnd::WindowProc(UINT message,
WPARAM wParam, LPARAM lParam)
這是一個虛擬函式,程式設計師可以在CWnd的派生類中覆蓋它,改變MFC分發訊息的方式。例如,MFC的CControlBar就覆蓋了WindowProc,對某些訊息作了自己的特別處理,其他訊息處理由基類的WindowProc函式完成。
但是在當前例子中,當前物件的類CTview沒有覆蓋該函式,所以CWnd的WindowProc被呼叫。
這個函式把下一步的工作交給OnWndMsg函式來