MFC消息处理学习总结
生活随笔
收集整理的這篇文章主要介紹了
MFC消息处理学习总结
小編覺得挺不錯的,現在分享給大家,幫大家做個參考.
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函數來處理。如果OnWndMsg沒有處理,則交給DefWindowProc來處理。
OnWndMsg和DefWindowProc都是CWnd類的虛擬函數。
OnWndMsg的原型如下:
BOOL CWnd::OnWndMsg( UINT message,
WPARAM wParam, LPARAM lParam,RESULT*pResult );
該函數是虛擬函數。
和WindowProc一樣,由于當前對象的類CTview沒有覆蓋該函數,所以CWnd的OnWndMsg被調用。
在CWnd中,MFC使用OnWndMsg來分別處理各類消息:
如果是WM_COMMAND消息,交給OnCommand處理;然后返回。
如果是WM_NOTIFY消息,交給OnNotify處理;然后返回。
如果是WM_ACTIVATE消息,先交給_AfxHandleActivate處理(后面5.3.3.7節會解釋它的處理),再繼續下面的處理。
如果是WM_SETCURSOR消息,先交給_AfxHandleSetCursor處理;然后返回。
如果是其他的Windows消息(包括WM_ACTIVATE),則
首先在消息緩沖池進行消息匹配,
若匹配成功,則調用相應的消息處理函數;
若不成功,則在消息目標的消息映射數組中進行查找匹配,看它是否處理當前消息。這里,消息目標即CTview對象。
如果消息目標處理了該消息,則會匹配到消息處理函數,調用它進行處理;
否則,該消息沒有被應用程序處理,OnWndMsg返回FALSE。
關于Windows消息和消息處理函數的匹配,見下一節。
缺省處理函數DefWindowProc將在討論對話框等的實現時具體分析。
Windows消息的查找和匹配
CWnd或者派生類的對象調用OnWndMsg搜索本對象或者基類的消息映射數組,尋找當前消息的消息處理函數。如果當前對象或者基類處理了當前消息,則必定在其中一個類的消息映射數組中匹配到當前消息的處理函數。
消息匹配是一個比較耗時的任務,為了提高效率,MFC設計了一個消息緩沖池,把要處理的消息和匹配到的消息映射條目(條目包含了消息處理函數的地址)以及進行消息處理的當前類等信息構成一條緩沖信息,放到緩沖池中。如果以后又有同樣的消息需要同一個類處理,則直接從緩沖池查找到對應的消息映射條目就可以了。
MFC用哈希查找來查詢消息映射緩沖池。消息緩沖池相當于一個哈希表,它是應用程序的一個全局變量,可以放512條最新用到的消息映射條目的緩沖信息,每一條緩沖信息是哈希表的一個入口。
采用AFX_MSG_CACHE結構描述每條緩沖信息,其定義如下:
struct AFX_MSG_CACHE
{
UINT nMsg;
const AFX_MSGMAP_ENTRY* lpEntry;
const AFX_MSGMAP* pMessageMap;
};
nMsg存放消息ID,每個哈希表入口有不同的nMsg。
lpEnty存放和消息ID匹配的消息映射條目的地址,它可能是this所指對象的類的映射條目,也可能是這個類的某個基類的映射條目,也可能是空。
pMessageMap存放消息處理函數匹配成功時進行消息處理的當前類(this所指對象的類)的靜態成員變量messageMap的地址,它唯一的標識了一個類(每個類的messageMap變量都不一樣)。
this所指對象是一個CWnd或其派生類的實例,是正在處理消息的MFC窗口對象。
哈希查找:使用消息ID的值作為關鍵值進行哈希查找,如果成功,即可從lpEntry獲得消息映射條目的地址,從而得到消息處理函數及其原型。
如何判斷是否成功匹配呢?有兩條標準:
第一,當前要處理的消息message在哈希表(緩沖池)中有入口;第二,當前窗口對象(this所指對象)的類的靜態變量messageMap的地址應該等于本條緩沖信息的pMessagMap。MFC通過虛擬函數GetMessagMap得到messageMap的地址。
如果在消息緩沖池中沒有找到匹配,則搜索當前對象的消息映射數組,看是否有合適的消息處理函數。
如果匹配到一個消息處理函數,則把匹配結果加入到消息緩沖池中,即填寫該條消息對應的哈希表入口:
nMsg=message;
pMessageMap=this->GetMessageMap;
lpEntry=查找結果
然后,調用匹配到的消息處理函數。否則(沒有找到),使用_GetBaseMessageMap得到基類的消息映射數組,查找和匹配;直到匹配成功或搜尋了所有的基類(到CCmdTarget)為止。
如果最后沒有找到,則也把該條消息的匹配結果加入到緩沖池中。和匹配成功不同的是:指定lpEntry為空。這樣OnWndMsg返回,把控制權返還給AfxCallWndProc函數,AfxCallWndProc將繼續調用DefWndProc進行缺省處理。
消息映射數組的搜索在CCmdTarget::OnCmdMsg函數中也用到了,而且算法相同。為了提高速度,MFC把和消息映射數組條目逐一比較、匹配的函數AfxFindMessageEntry用匯編書寫。
const AFX_MSGMAP_ENTRY* AFXAPI
AfxFindMessageEntry(const AFX_MSGMAP_ENTRY* lpEntry,
UINT nMsg, UINT nCode, UINT nID)
第一個參數是要搜索的映射數組的入口;第二個參數是Windows消息標識;第三個參數是控制通知消息標識;第四個參數是命令消息標識。
對Windows消息來說,nMsg是每條消息不同的,nID和nCode為0。
對命令消息來說,nMsg固定為WM_COMMAND,nID是每條消息不同,nCode都是CN_COMMAND(定義為0)。
對控制通知消息來說,nMsg固定為WM_COMMAND或者WM_NOTIFY,nID和nCode是每條消息不同。
對于Register消息,nMsg指定為0XC000,nID和nCode為0。在使用函數AfxFindMessageEntry得到匹配結果之后,還必須判斷nSig是否等于message,只有相等才調用對應的消息處理函數。
Windows消息處理函數的調用
對一個Windows消息,匹配到了一個消息映射條目之后,將調用映射條目所指示的消息處理函數。
調用處理函數的過程就是轉換映射條目的pfn指針為適當的函數類型并執行它:MFC定義了一個成員函數指針mmf,首先把消息處理函數的地址賦值給該函數指針,然后根據消息映射條目的nSig值轉換指針的類型。但是,要給函數指針mmf賦值,必須使該指針可以指向所有的消息處理函數,為此則該指針的類型是所有類型的消息處理函數指針的聯合體。
對上述過程,MFC的實現大略如下:
union MessageMapFunctions mmf;
mmf.pfn = lpEntry->pfn;
swithc (value_of_nsig){
…
case AfxSig_is: //OnCreate就是該類型
lResult = (this->*mmf.pfn_is)((LPTSTR)lParam);
break;
…
default:
ASSERT(FALSE); break;
}
…
LDispatchRegistered: // 處理registered windows messages
ASSERT(message >= 0xC000);
mmf.pfn = lpEntry->pfn;
lResult = (this->*mmf.pfn_lwl)(wParam, lParam);
…
如果消息處理函數有返回值,則返回該結果,否則,返回TRUE。
對于圖4-1所示的例子,nSig等于AfxSig_is,所以將執行語句
(this->*mmf.pfn_is)((LPTSTR)lParam)
也就是對CTview::OnCreate的調用。
順便指出,對于Registered窗口消息,消息處理函數都是同一原型,所以都被轉換成lwl型(關于Registered窗口消息的映射,見4.4.2節)。
綜上所述,標準Windwos消息和應用程序消息中的Registered消息,由窗口過程直接調用相應的處理函數處理:
如果某個類型的窗口(C++類)處理了某條消息(覆蓋了CWnd或直接基類的處理函數),則對應的HWND窗口(Winodws window)收到該消息時就調用該覆蓋函數來處理;如果該類窗口沒有處理該消息,則調用實現該處理函數最直接的基類(在C++的類層次上接近該類)來處理,上述例子中如果CTview不處理WM_CREATE消息,則調用上一層的CWnd::OnCreate處理;
如果基類都不處理該消息,則調用DefWndProc來處理。
消息映射機制完成虛擬函數功能的原理
綜合對Windows消息的處理來看,MFC使用消息映射機制完成了C++虛擬函數的功能。這主要基于以下幾點:
所有處理消息的類從CCmdTarget派生。
使用靜態成員變量_messageEntries數組存放消息映射條目,使用靜態成員變量messageMap來唯一地區別和得到類的消息映射。
通過GetMessage虛擬函數來獲取當前對象的類的messageMap變量,進而得到消息映射入口。
按照先底層,后基層的順序在類的消息映射數組中搜索消息處理函數。基于這樣的機制,一般在覆蓋基類的消息處理函數時,應該調用基類的同名函數。
以上論斷適合于MFC其他消息處理機制,如對命令消息的處理等。不同的是其他消息處理有一個命令派發/分發的過程。
下一節,討論命令消息的接受和處理。
對命令消息的接收和處理
MFC標準命令消息的發送
在SDI或者MDI應用程序中,命令消息由用戶界面對象(如菜單、工具條等)產生,然后送給主邊框窗口。主邊框窗口使用標準MFC窗口過程處理命令消息。窗口過程把命令傳遞給MFC主邊框窗口對象,開始命令消息的分發。MFC邊框窗口類CFrameWnd提供了消息分發的能力。
下面,還是通過一個例子來說明命令消息的處理過程。
使用AppWizard產生一個單文檔應用程序t。從help菜單選擇“About”,就會彈出一個ABOUT對話框。下面,討論從命令消息的發出到對話框彈出的過程。
首先,選擇“ About”菜單項的動作導致一個Windows命令消息ID_APP_ABOUT的產生。Windows系統發送該命令消息到邊框窗口,導致它的窗口過程AfxWndProc被調用,參數1是邊框窗口的句柄,參數2是消息ID(即WM_COMMAND),參數3、4是消息參數,參數3的值是ID_APP_ABOUT。接著的系列調用如圖4-3所示。
下面分別講述每一層所調用的函數。
前4步同對Windows消息的處理。這里接受消息的HWND窗口是主邊框窗口,因此,AfxWndProc根據HWND句柄得到的MFC窗口對象是MFC邊框窗口對象。
在4.2.2節談到,如果CWnd::OnWndMsg判斷要處理的消息是命令消息(WM_COMMAND),就調用OnCommand進一步處理。由于OnCommand是虛擬函數,當前MFC窗口對象是邊框窗口對象,它的類從CFrameWnd類導出,沒有覆蓋CWnd的虛擬函數OnCommand,而CFrameWnd覆蓋了CWnd的OnCommand,所以,CFrameWnd的OnCommand被調用。換句話說,CFrameWnd的OnCommand被調用是動態約束的結果。接著介紹的本例子的有關調用,也是通過動態約束而實際發生的函數調用。
接著的有關調用,將不進行為什么調用某個類的虛擬或者消息處理函數的分析。
(1)CFrameWnd的OnCommand函數
BOOL CFrameWnd::OnCommand(WPARAM wParam, LPARAM lParam)
參數wParam的低階word存放了菜單命令nID或控制子窗口ID;如果消息來自控制窗口,高階word存放了控制通知消息;如果消息來自加速鍵,高階word值為1;如果消息來自菜單,高階word值為0。
如果是通知消息,參數lParam存放了控制窗口的句柄hWndCtrl,其他情況下lParam是0。
在這個例子里,低階word是ID_APP_ABOUT,高階word是1;lParam是0。
MFC對CFrameWnd的缺省實現主要是獲得一個機會來檢查程序是否運行在HELP狀態,需要執行上下文幫助,如果不需要,則調用基類的CWnd::OnCommand實現正常的命令消息發送。
(2)CWnd的OnCommand函數
BOOL CWnd::OnCommand(WPARAM wParam, LPARAM lParam)
它按一定的順序處理命令或者通知消息,如果發送成功,返回TRUE,否則,FALSE。處理順序如下:
如果是命令消息,則調用OnCmdMsg(nID, CN_UPDATE_COMMAND_UI, &state, NULL)測試nID命令是否已經被禁止,如果這樣,返回FALSE;否則,調用OnCmdMsg進行命令發送。關于CN_UPDATE_COMMAND_UI通知消息,見后面用戶界面狀態的更新處理。
如果是控制通知消息,則先用ReflectLastMsg反射通知消息到子窗口。如果子窗口處理了該消息,則返回TRUE;否則,調用OnCmdMsg進行命令發送。關于通知消息的反射見后面4.4.4.3節。OnCommand給OnCmdMsg傳遞四個參數:nID,即命令消息ID;nCode,如果是通知消息則為通知代碼,如果是命令消息則為NC_COMMAND(即0);其余兩個參數為空。
(3)CFrameWnd的OnCmdMsg函數
BOOL CFrameWnd::OnCmdMsg(UINT nID, int nCode, void* pExtra,
AFX_CMDHANDLERINFO* pHandlerInfo)
參數1是命令ID;如果是通知消息(WM_COMMAND或者WM_NOTIFY),則參數2表示通知代碼,如果是命令消息,參數2是0;如果是WM_NOTIFY,參數3包含了一些額外的信息;參數4在正常消息處理中應該是空。
在這個例子里,參數1是命令ID,參數2為0,參數3空。
OnCmdMsg是虛擬函數,CFrameWnd覆蓋了該函數,當前對象(this所指)是MFC單文檔的邊框窗口對象。故CFrameWnd的OnCmdMsg被調用。CFrameWnd::OnCmdMsg在MFC消息發送中占有非常重要的地位,MFC對該函數的缺省實現確定了MFC的標準命令發送路徑:
送給活動(Active)視處理,調用活動視的OnCmdMsg。由于當前對象是MFC視對象,所以,OnCmdMsg將搜索CTview及其基類的消息映射數組,試圖得到相應的處理函數。
如果視對象自己不處理,則視得到和它關聯的文檔,調用關聯文檔的OnCmdMsg。由于當前對象是MFC視對象,所以,OnCmdMsg將搜索CTdoc及其基類的消息映射數組,試圖得到相應的處理函數。
如果文檔對象不處理,則它得到管理文檔的文檔模板對象,調用文檔模板的OnCmdMsg。由于當前對象是MFC文檔模板對象,所以,OnCmdMsg將搜索文檔模板類及其基類的消息映射數組,試圖得到相應的處理函數。
如果文檔模板不處理,則把沒有處理的信息逐級返回:文檔模板告訴文檔對象,文檔對象告訴視對象,視對象告訴邊框窗口對象。最后,邊框窗口得知,視、文檔、文檔模板都沒有處理消息。
CFrameWnd的OnCmdMsg繼續調用CWnd::OnCmdMsg(斜體表示有類屬限制)來處理消息。由于CWnd沒有覆蓋OnCmdMsg,故實際上調用了函數CCmdTarget::OnCmdMsg。由于當前對象是MFC邊框窗口對象,所以OnCmdMsg函數將搜索CMainFrame類及其所有基類的消息映射數組,試圖得到相應的處理函數。CWnd沒有實現OnCmdMsg卻指定要執行其OnCmdMsg函數,可能是為了以后MFC給CWnd實現了OnCmdMsg之后其他代碼不用改變。
這一步是邊框窗口自己嘗試處理消息。
如果邊框窗口對象不處理,則送給應用程序對象處理。調用CTApp的OnCmdMsg,由于實際上CTApp及其基類CWinApp沒有覆蓋OnCmdMsg,故實際上調用了函數CCmdTarget::OnCmdMsg。由于當前對象是MFC應用程序對象,所以OnCmdMsg函數將搜索CTApp類及其所有基類的的消息映射入口數組,試圖得到相應的處理函數
如果應用程序對象不處理,則返回FALSE,表明沒有命令目標處理當前的命令消息。這樣,函數逐級別返回,OnCmdMsg告訴OnCommand消息沒有被處理,OnCommand告訴OnWndMsg消息沒有被處理,OnWndMsg告訴WindowProc消息沒有被處理,于是WindowProc調用DefWindowProc進行缺省處理。
本例子在第六步中,應用程序對ID_APP_ABOUT消息作了處理。它找到處理函數CTApp::OnAbout,使用DispatchCmdMsg派發消息給該函數處理。
如果是MDI邊框窗口,標準發送路徑還有一個環節,該環節和第二、三、四步所涉及的OnCmdMsg函數,將在下兩節再次具體分析。
命令消息的派發和消息的多次處理
命令消息的派發
如前3.1所述,CCmdTarget的靜態成員函數DispatchCmdMsg用來派發命令消息給指定的命令目標的消息處理函數。
static BOOL DispatchCmdMsg(CCmdTarget* pTarget,
UINT nID, int nCode,
AFX_PMSG pfn, void* pExtra, UINT nSig,
AFX_CMDHANDLERINFO* pHandlerInfo)
前面在講CCmdTarget時,提到了該函數。這里講述它的實現:
第一個參數指向處理消息的對象;第二個參數是命令ID;第三個是通知消息等;第四個是消息處理函數地址;第五個參數用于存放一些有用的信息,根據nCode的值表示不同的意義,例如當消息是WM_NOFITY,指向一個NMHDR結構(關于WM_NOTIFY,參見4.4.4.2節通知消息的處理);第六個參數標識消息處理函數原型;第七個參數是一個指針,指向AFX_CMDHANDLERINFO結構。前六個參數(除了第五個外)都是向函數傳遞信息,第五個和第七個參數是雙向的,既向函數傳遞信息,也可以向調用者返回信息。
關于AFX_CMDHANDLERINFO結構:
struct AFX_CMDHANDLERINFO
{
CCmdTarget* pTarget;
void (AFX_MSG_CALL CCmdTarget::*pmf)(void);
};
第一個成員是一個指向命令目標對象的指針,第二個成員是一個指向CCmdTarget成員函數的指針。
該函數的實現流程可以如下描述:
首先,它檢查參數pHandlerInfo是否空,如果不空,則用pTarget和pfn填寫其指向的結構,返回TRUE;通常消息處理時傳遞來的pHandlerInfo空,而在使用OnCmdMsg來測試某個對象是否處理某條命令時,傳遞一個非空的pHandlerInfo指針。若返回TRUE,則表示可以處理那條消息。
如果pHandlerInfo空,則進行消息處理函數的調用。它根據參數nSig的值,把參數pfn的類型轉換為要調用的消息處理函數的類型。這種指針轉換技術和前面講述的Windows消息的處理是一樣的。
消息的多次處理
如果消息處理函數不返回值,則DispatchCmdMsg返回TRUE;否則,DispatchCmdMsg返回消息處理函數的返回值。這個返回值沿著消息發送相反的路徑逐級向上傳遞,使得各個環節的OnCmdMsg和OnCommand得到返回的處理結果:TRUE或者FALSE,即成功或者失敗。
這樣就產生了一個問題,如果消息處理函數有意返回一個FALSE,那么不就傳遞了一個錯誤的信息?例如,OnCmdMsg函數得到FALSE返回值,就認為消息沒有被處理,它將繼續發送消息到下一環節。的確是這樣的,但是這不是MFC的漏洞,而是有意這么設計的,用來處理一些特別的消息映射宏,實現同一個消息的多次處理。
通常的命令或者通知消息是沒有返回值的(見4.4.2節的消息映射宏),僅僅一些特殊的消息處理函數具有返回值,這類消息的消息處理函數是使用擴展消息映射宏映射的,例如:
ON_COMMAND對應的ON_COMMAND_EX
擴展映射宏和對應的普通映射宏的參數個數相同,含義一樣。但是擴展映射宏的消息處理函數的原型和對應的普通映射宏相比,有兩個不同之處:一是多了一個UINT類型的參數,另外就是有返回值(返回BOOL類型)。回顧4.4.2章節,范圍映射宏ON_COMMAND_RANGE的消息處理函數也有一個這樣的參數,該參數在兩處的含義是一樣的,例如:命令消息擴展映射宏ON_COMMAND_EX定義的消息處理函數解釋該參數是當前要處理的命令消息ID。有返回值的意義在于:如果擴展映射宏的消息處理函數返回FALSE,則導致當前消息被發送給消息路徑上的下一個消息目標處理。
綜合來看,ON_COMMAND_EX宏有兩個功能:
一是可以把多個命令消息指定給一個消息處理函數處理。這類似于ON_COMMAND_RANGE宏的作用。不過,這里的多條消息的命令ID或者控制子窗口ID可以不連續,每條消息都需要一個ON_COMMAND_EX宏。
二是可以讓幾個消息目標處理同一個命令或者通知或者反射消息。如果消息發送路徑上較前的命令目標不處理消息或者處理消息后返回FALSE,則下一個命令目標將繼續處理該消息。
對于通知消息、反射消息,它們也有擴展映射宏,而且上述論斷也適合于它們。例如:
ON_NOTIFY對應的ON_NOTIFY_EX
ON_CONTROL對應的ON_CONTROL_EX
ON_CONTROL_REFLECT對應的ON_CONTROL_REFLECT_EX
等等。
范圍消息映射宏也有對應的擴展映射宏,例如:
ON_NOTIFY_RANGE對應的ON_NOTIFY_EX_RANGE
ON_COMMAND_RANGE對應的ON_COMMAND_EX_RANGE
使用這些宏的目的在于利用擴展宏的第二個功能:實現消息的多次處理。
關于擴展消息映射宏的例子,參見13.2..4.4節和13.2.4.6節。
一些消息處理類的OnCmdMsg的實現
從以上論述知道,OnCmdMsg虛擬函數在MFC命令消息的發送中扮演了重要的角色,CFrameWnd的OnCmdMsg實現了MFC的標準命令消息發送路徑。
那么,就產生一個問題:如果命令消息不送給邊框窗口對象,那么就不會有按標準命令發送路徑發送消息的過程?答案是肯定的。例如一個菜單被一個對話框窗口所擁有,那么,菜單命令將送給MFC對話框窗口對象處理,而不是MFC邊框窗口處理,當然不會和CFrameWnd的處理流程相同。
但是,有一點需要指出,一般標準的SDI和MDI應用程序,只有主邊框窗口擁有菜單和工具條等用戶接口對象,只有在用戶與用戶接口對象進行交互時,才產生命令,產生的命令必然是送給SDI或者MDI程序的主邊框窗口對象處理。
下面,討論幾個MFC類覆蓋OnCmdMsg虛擬函數時的實現。這些類的OnCmdMsg或者可能是標準MFC命令消息路徑的一個環節,或者可能是一個獨立的處理過程(對于其中的MFC窗口類)。
從分析CView的OnCmdMsg實現開始。
CView的OnCmdMsg
CView::OnCmdMsg(UINT nID, int nCode, void* pExtra,
AFX_CMDHANDLERINFO* pHandlerInfo)
首先,調用CWnd::OnCmdMsg,結果是搜索當前視的類和基類的消息映射數組,搜索順序是從下層到上層。若某一層實現了對命令消息nID的處理,則調用它的實現函數;否則,調用m_pDocument->OnCmdMsg,把命令消息送給文檔類處理。m_pDocument是和當前視關聯的文檔對象指針。如果文檔對象類實現了OnCmdMsg,則調用它的覆蓋函數;否則,調用基類(例如CDocument)的OnCmdMsg。
接著,討論CDocument的實現。
CDocument的 OnCmdMsg
BOOL CDocument::OnCmdMsg(UINT nID, int nCode, void* pExtra,
AFX_CMDHANDLERINFO* pHandlerInfo)
首先,調用CCmdTarget::OnCmdMsg,導致當前對象(this)的類和基類的消息映射數組被搜索,看是否有對應的消息處理函數可用。如果有,就調用它;如果沒有,則調用文檔模板的OnCmdMsg函數(m_pTemplate->OnCmdMsg)把消息送給文檔模板處理。
MFC文檔模板沒有覆蓋OnCmdMsg,導致基類CCmdTarget的OnCmdMsg被調用,看是否有文檔模板類或基類實現了對消息的處理。是的話,調用對應的消息處理函數,否則,返回FALSE。從前面的分析知道,CCmdTarget類的消息映射數組是空的,所以這里返回FALSE。
CDialog的OnCmdMsg
BOOL CDialog::OnCmdMsg(UINT nID, int nCode, void* pExtra,
AFX_CMDHANDLERINFO* pHandlerInfo)
調用CWnd::OnCmdMsg,讓對話框或其基類處理消息。
如果還沒有處理,而且是控制消息或系統命令或非命令按鈕,則返回FALSE,不作進一步處理。否則,調用父窗口的OnCmdmsg(GetParent()->OnCmdmsg)把消息送給父窗口處理。
如果仍然沒有處理,則調用當前線程的OnCmdMsg(GetThread()->OnCmdMsg)把消息送給線程對象處理。
如果最后沒有處理,返回FALSE。
CMDIFrameWnd的OnCmdMsg
對于MDI應用程序,MDI主邊框窗口首先是把命令消息發送給活動的MDI文檔邊框窗口進行處理。MDI主邊框窗口對OnCmdMsg的實現函數的原型如下:
BOOL CMDIFrameWnd::OnCmdMsg(UINT nID, int nCode, void* pExtra,
AFX_CMDHANDLERINFO* pHandlerInfo)
如果有激活的文檔邊框窗口,則調用它的OnCmdMsg(MDIGetActive()->OnCmdMsg)把消息交給它進行處理。MFC的文檔邊框窗口類并沒有覆蓋OnCmdMsg函數,所以基類CFrameWnd的函數被調用,導致文檔邊框窗口的活動視、文檔邊框窗口本身、應用程序對象依次來進行消息處理。
如果文檔邊框窗口沒有處理,調用CFrameWnd::OnCmdMsg把消息按標準路徑發送,重復第一次的步驟,不過對于MDI邊框窗口來說不存在活動視,所以省卻了讓視處理消息的必要;接著讓MDI邊框窗口本身來處理消息,如果它還沒有處理,則讓應用程序對象進行消息處理──雖然這是一個無用的重復。
除了CView、CDocument和CMDIFrameWnd類,還有幾個OLE相關的類覆蓋了OnCmdMsg函數。OLE的處理本書暫不涉及,CDialog::OnCmdMsg將在對話框章節專項討論其具體實現。
一些消息處理類的OnCommand的實現
除了虛擬函數OnCmdMsg,還有一個虛擬函數OnCommand在命令消息的發送中占有重要地位。在處理命令或者通知消息時,OnCommand被MFC窗口過程調用,然后它調用OnCmdMsg按一定路徑傳送消息。除了CWnd類和一些OLE相關類外,MFC里主要還有MDI邊框窗口實現了OnCommand。
BOOL CMDIFrameWnd::OnCommand(WPARAM wParam, LPARAM lParam)
第一,如果存在活動的文檔邊框窗口,則使用AfxCallWndProc調用它的窗口過程,把消息送給文檔邊框窗口來處理。這將導致文檔邊框窗口的OnCmdMsg作如下的處理:
活動視處理消息→與視關聯的文檔處理消息→本文檔邊框窗口處理消息→應用程序對象處理消息→文檔邊框窗口缺省處理
任何一個環節如果處理消息,則不再向下發送消息,處理終止。如果消息仍然沒有被處理,就只有交給主邊框窗口了。
第二,第一步沒有處理命令,繼續調用CFrameWnd::OnCommand,將導致CMDIFrameWnd的OnCmdMsg被調用。從前面的分析知道,將再次把消息送給MDI邊框窗口的活動文檔邊框窗口,第一步的過程除了文檔邊框窗口缺省處理外都將被重復。具體的處理過程見前文的CMDIFrameWnd::OnCmdMsg函數。
對于MDI消息,如果主邊框窗口還不處理的話,交給CMDIFrameWnd的DefWindowProc作缺省處理。
消息沒有處理,返回FALSE。
上述分析綜合了OnCommand和OnCmdMsg的處理,它們是在MFC內部MDI邊框窗口處理命令消息的完整的流程和標準的步驟。整個處理過程再次表明了邊框窗口在處理命令消息時的中心作用。從程序員的角度來看,可以認為整個標準處理路徑如下:
活動視處理消息→與視關聯的文檔處理消息→本文檔邊框窗口處理消息→應用程序對象處理消息→文檔邊框窗口缺省處理→MDI邊框窗口處理消息→MDI邊框窗口缺省處理
任何一個環節如果處理消息,不再向下發送消息,急處理終止。
對控制通知消息的接收和處理
WM_COMMAND控制通知消息的處理
WM_COMMAND控制通知消息的處理和WM_COMMAND命令消息的處理類似,但是也有不同之處。
首先,分析處理WM_COMMAND控制通知消息和命令消息的相似處。如前所述,命令消息和控制通知消息都是由窗口過程給OnCommand處理(參見CWnd::OnWndMsg的實現),OnCommand通過wParam和lParam參數區分是命令消息或通知消息,然后送給OnCmdMsg處理(參見CWnd::OnCommnd的實現)。
其次,兩者的不同之處是:
命令消息一般是送給主邊框窗口的,這時,邊框窗口的OnCmdMsg被調用;而控制通知消息送給控制子窗口的父窗口,這時,父窗口的OnCmdMsg被調用。
OnCmdMsg處理命令消息時,通過命令分發可以由多種命令目標處理,包括非窗口對象如文檔對象等;而處理控制通知消息時,不會有消息分發的過程,控制通知消息最終肯定是由窗口對象處理的。
不過,在某種程度上可以說,控制通知消息由窗口對象處理是一種習慣和約定。當使用ClassWizard進行消息映射時,它不提供把控制通知消息映射到非窗口對象的機會。但是,手工地添加消息映射,讓非窗口對象處理控制通知消息的可能是存在的。例如,對于CFormView,一方面它具備接受WM_COMMAND通知消息的條件,另一方面,具備把WM_COMMAND消息派發給關聯文檔對象處理的能力,所以給CFormView的通知消息是可以讓文檔對象處理的。
事實上,BN_CLICKED控制通知消息的處理和命令消息的處理完全一樣,因為該消息的通知代碼是0,ON_BN_CLICKED(id,memberfunction)和ON_COMMAND(id,memberfunction)是等同的。
此外,MFC的狀態更新處理機制就是建立在通知消息可以發送給各種命令目標的基礎之上的。關于MFC的狀態更新處理機制,見后面4.4.4.4節的討論。
控制通知消息可以反射給子窗口處理。OnCommand判定當前消息是WM_COMAND通知消息之后,首先它把消息反射給控制子窗口處理,如果子窗口處理了反射消息,OnCommand不會繼續調用OnCmdMsg讓父窗口對象來處理通知消息。
WM_NOTIFY消息及其處理:
(1)WM_NOTIFY消息
還有一種通知消息WM_NOTIFY,在Win32中用來傳遞信息復雜的通知消息。WM_NOTIFY消息怎么來傳遞復雜的信息呢?WM_NOTIFY的消息參數wParam包含了發送通知消息的控制窗口ID,另一個參數lParam包含了一個指針。該指針指向一個NMHDR結構,或者更大的結構,只要它的第一個結構成員是NMHDR結構。
NMHDR結構:
typedef struct tagNMHDR {
HWND hwndFrom;
UINT idFrom;
UINT code;
} NMHDR;
上述結構有三個成員,分別是發送通知消息的控制窗口的句柄、ID和通知消息代碼。
舉一個更大、更復雜的結構例子:列表控制窗發送LVN_KEYDOWN控制通知消息,則lParam包含了一個指向LV_KEYDOWN結構的指針。其結構如下:
typedef struct tagLV_KEYDOWN {
NMHDR hdr;
WORD wVKey;
UINT flags;
}LV_KEYDOWN;
它的第一個結構成員hdr就是NMHDR類型。其他成員包含了更多的信息:哪個鍵被按下,哪些輔助鍵(SHIFT、CTRL、ALT等)被按下。
(2)WM_NOTIFY消息的處理
在分析CWnd::OnWndMsg函數時,曾指出當消息是WM_NOTIFY時,它把消息傳遞給OnNotify虛擬函數處理。這是一個虛擬函數,類似于OnCommand,CWnd和派生類都可以覆蓋該函數。OnNotify的函數原型如下:
BOOL CWnd::OnNotify(WPARAM, LPARAM lParam, LRESULT* pResult)
參數1是發送通知消息的控制窗口ID,沒有被使用;參數2是一個指針;參數3指向一個long類型的數據,用來返回處理結果。
WM_NOTIFY消息的處理過程如下:
反射消息給控制子窗口處理。
如果子窗口不處理反射消息,則交給OnCmdMsg處理。給OnCmdMsg的四個參數分別如下:第一個是命令消息ID,第四個為空;第二個高階word是WM_NOTIFY,低階word是通知消息;第三個參數是指向AFX_NOTIFY結構的指針。第二、三個參數有別于OnCommand送給OnCmdMsg的參數。
AFX_NOTIFY結構:
struct AFX_NOTIFY
{
LRESULT* pResult;
NMHDR* pNMHDR;
};
pNMHDR的值來源于參數2 lParam,該結構的域pResult用來保存處理結果,域pNMHDR用來傳遞信息。
OnCmdMsg后續的處理和WM_COMMAND通知消息基本相同,只是在派發消息給消息處理函數時,DispatchMsdMsg的第五個參數pExtra指向OnCmdMsg傳遞給它的AFX_NOTIFY類型的參數,而不是空指針。這樣,處理函數就得到了復雜的通知消息信息。
消息反射
(1)消息反射的概念
前面討論控制通知消息時,曾經多次提到了消息反射。MFC提供了兩種消息反射機制,一種用于OLE控件,一種用于Windows控制窗口。這里只討論后一種消息反射。
Windows控制常常發送通知消息給它們的父窗口,通常控制消息由父窗口處理。但是在MFC里頭,父窗口在收到這些消息后,或者自己處理,或者反射這些消息給控制窗口自己處理,或者兩者都進行處理。如果程序員在父窗口類覆蓋了通知消息的處理(假定不調用基類的實現),消息將不會反射給控制子窗口。這種反射機制是MFC實現的,便于程序員創建可重用的控制窗口類。
MFC的CWnd類處理以下控制通知消息時,必要或者可能的話,把它們反射給子窗口處理:
WM_CTLCOLOR,
WM_VSCROLL,WM_HSCROLL,
WM_DRAWITEM,WM_MEASUREITEM,
WM_COMPAREITEM,WM_DELETEITEM,
WM_CHARTOITEM,WM_VKEYTOITEM,
WM_COMMAND、WM_NOTIFY。
例如,對WM_VSCROLL、WM_HSCROLL消息的處理,其消息處理函數如下:
void CWnd::OnHScroll(UINT, UINT, CScrollBar* pScrollBar)
{
//如果是一個滾動條控制,首先反射消息給它處理
if (pScrollBar != NULL &&pScrollBar->SendChildNotifyLastMsg())
return; //控制窗口成功處理了該消息
Default();
}
又如:在討論OnCommand和OnNofity函數處理通知消息時,都曾經指出,它們首先調用ReflectLastMsg把消息反射給控制窗口處理。
為了利用消息反射的功能,首先需要從適當的MFC窗口派生出一個控制窗口類,然后使用ClassWizard給它添加消息映射條目,指定它處理感興趣的反射消息。下面,討論反射消息映射宏。
上述消息的反射消息映射宏的命名遵循以下格式:“ON”前綴+消息名+“REFLECT”后綴,例如:消息WM_VSCROLL的反射消息映射宏是ON_WM_VSCROLL_REFECT。但是通知消息WM_COMMAND和WM_NOTIFY是例外,分別為ON_CONTROL_REFLECT和ON_NOFITY_REFLECT。狀態更新通知消息的反射消息映射宏是ON_UPDATE_COMMAND_UI_REFLECT。
消息處理函數的名字和去掉“WM_”前綴的消息名相同 ,例如WM_HSCROLL反射消息處理函數是Hscroll。
消息處理函數的原型這里不一一列舉了。
這些消息映射宏和消息處理函數的原型可以借助于ClassWizard自動地添加到程序中。ClassWizard添加消息處理函數時,可以處理的反射消息前面有一個等號,例如處理WM_HSCROLL的反射消息,選擇映射消息“=EN_HSC ROLL”。ClassWizard自動的添加消息映射宏和處理函數到框架文件。
(2)消息反射的處理過程
如果不考慮有OLE控件的情況,消息反射的處理流程如下圖所示:
首先,調用CWnd的成員函數SendChildNotifyLastMsg,它從線程狀態得到本線程最近一次獲取的消息(關于線程狀態,后面第9章會詳細介紹)和消息參數,并且把這些參數傳遞給函數OnChildNotify。注意,當前的CWnd對象就是MFC控制子窗口對象。
OnChlidNofify是CWnd定義的虛擬函數,不考慮OLE控制的話,它僅僅只調用ReflectChildNotify。OnChlidNofify可以被覆蓋,所以如果程序員希望處理某個控制的通知消息,除了采用消息映射的方法處理通知反射消息以外,還可以覆蓋OnChlidNotify虛擬函數,如果成功地處理了通知消息,則返回TRUE。
ReflectChildNotify是CWnd的成員函數,完成反射消息的派發。對于WM_COMMAND,它直接調用CWnd::OnCmdMsg派發反射消息WM_REFLECT_BASE+WM_COMMAND;對于WM_NOTIFY,它直接調用CWnd::OnCmdMsg派發反射消息WM_REFLECT_BASE+WM_NOFITY;對于其他消息,則直接調用CWnd::OnWndMsg(即CmdTarge::OnWndMsg)派發相應的反射消息,例如WM_REFLECT_BASE+WM_HSCROLL。
注意:ReflectChildNotify直接調用了CWnd的OnCmdMsg或OnWndMsg,這樣反射消息被直接派發給控制子窗口,省卻了消息發送的過程。
接著,控制子窗口如果處理了當前的反射消息,則返回反射消息被成員處理的信息。
(3)一個示例
如果要創建一個編輯框控制,要求它背景使用黃色,其他特性不變,則可以從CEdit派生一個類CYellowEdit,處理通知消息WM_CTLCOLOR的反射消息。CYellowEdit有三個屬性,定義如下:
CYellowEdit::CYellowEdit()
{
m_clrText = RGB( 0, 0, 0 );
m_clrBkgnd = RGB( 255, 255, 0 );
m_brBkgnd.CreateSolidBrush( m_clrBkgnd );
}
使用ClassWizard添加反射消息處理函數:
函數原型:
afx_msg void HScroll();
消息映射宏:
ON_WM_CTLCOLOR_REFLECT()
函數的框架
HBRUSH CYellowEdit::CtlColor(CDC* pDC, UINT nCtlColor)
{
// TODO:添加代碼改變設備描述表的屬性
// TODO: 如果不再調用父窗口的處理,則返回一個非空的刷子句柄
return NULL;
}
添加一些處理到函數CtlColor中,如下:
pDC->SetTextColor( m_clrText );//設置文本顏色
pDC->SetBkColor( m_clrBkgnd );//設置背景顏色
return m_brBkgnd; //返回背景刷
這樣,如果某個地方需要使用黃色背景的編輯框,則可以使用CYellowEdit控制。
對更新命令的接收和處理
用戶接口對象如菜單、工具條有多種狀態,例如:禁止,可用,選中,未選中,等等。這些狀態隨著運行條件的變化,由程序來進行更新。雖然程序員可以自己來完成更新,但是MFC框架為自動更新用戶接口對象提供了一個方便的接口,使用它對程序員來說可能是一個好的選擇。
實現方法
每一個用戶接口對象,如菜單、工具條、控制窗口的子窗口,都由唯一的ID號標識,用戶和它們交互時,產生相應ID號的命令消息。在MFC里,一個用戶接口對象還可以響應CN_UPDATE_COMMAND_UI通知消息。因此,對每個標號ID的接口對象,可以有兩個處理函數:一個消息處理函數用來處理該對象產生的命令消息ID,另一個狀態更新函數用來處理給該對象的CN_UPDATE_COMMAND_UID的通知消息。
使用ClassWizard可把狀態更新函數加入到某個消息處理類,其結果是:
在類的定義中聲明一個狀態函數;
在消息映射中使用ON_UPDATE_COMMAND_UI宏添加一個映射條目;
在類的實現文件中實現狀態更新函數的定義。
ON_UPDATE_COMMAND_UI給指定ID的用戶對象指定狀態更新函數,例如:
ON_UPDATE_COMMAND_UI(ID_EDIT_COPY, OnUpdateEditCopy)
映射標識號ID為ID_EDIT_COPY菜單的通知消息CN_UPDATE_COMMAND_UI到函數OnUpdateEditCopy。用于給EDIT(編輯菜單)的菜單項ID_EDIT_COPY(復制)添加一個狀態處理函數OnUpdateEditCopy,通過處理通知消息CN_UPDATE_COMMAND_UI實現該菜單項的狀態更新。
狀態處理函數的原型如下:
afxmsg void ClassName::OnUpdateEditPaste(CCmdUI* pCmdUI)
CCmdUI對象由MFC自動地構造。在完善函數的實現時,使用pCmdUI對象和CmdUI的成員函數實現菜單項ID_EDIT_COPY的狀態更新,讓它變灰或者變亮,也就是禁止或者允許用戶使用該菜單項。
狀態更新命令消息
要討論MFC的狀態更新處理,先得了解一條特殊的消息。MFC的消息映射機制除了處理各種Windows消息、控制通知消息、命令消息、反射消息外,還處理一種特別的“通知命令消息”,并通過它來更新菜單、工具欄(包括對話框工具欄)等命令目標的狀態。
這種“通知命令消息”是MFC內部定義的,消息ID是WM_COMMAND,通知代碼是CN_UPDATE_COMMAND_UI(0XFFFFFFFF)。
它不是一個真正意義上的通知消息,因為沒有控制窗口產生這樣的通知消息,而是MFC自己主動產生,用于送給工具條窗口或者主邊框窗口,通知它們更新用戶接口對象的狀態。
它和標準WM_COMMAND命令消息也不相同,因為它有特定的通知代碼,而命令消息通知代碼是0。
但是,從消息的處理角度,可以把它看作是一條通知消息。如果是工具條窗口接收該消息,則在發送機制上它和WM_COMMAND控制通知消息是相同的,相當于讓工具條窗口處理一條通知消息。如果是邊框窗口接收該消息,則在消息的發送機制上它和WM_COMMAND命令消息是相同的,可以讓任意命令目標處理該消息,也就是說邊框窗口可以把該條通知消息發送給任意命令目標處理。
從程序員的角度,可以把它看作一條“狀態更新命令消息”,像處理命令消息那樣處理該消息。每條命令消息都可以對應有一條“狀態更新命令消息”。ClassWizard也支持讓任意消息目標處理“狀態更新命令消息”(包括非窗口命令目標),實現用戶接口狀態的更新。
在這條消息發送時,通過OnCmdMsg的第三個參數pExtra傳遞一些信息,表示要更新的用戶接口對象。pExtra指向一個CCmdUI對象。這些信息將傳遞給狀態更新命令消息的處理函數。
下面討論用于更新用戶接口對象狀態的類CCmdUI。
類CCmdUI
CCmdUI不是從CObject派生,沒有基類。
成員變量
m_nID 用戶接口對象的ID
m_nIndex 用戶接口對象的index
m_pMenu 指向CCmdUI對象表示的菜單
m_pSubMenu 指向CCmdUI對象表示的子菜單
m_pOther 指向其他發送通知消息的窗口對象
m_pParentMenu 指向CCmdUI對象表示的子菜單
成員函數
Enable(BOOL bOn = TRUE ) 禁止用戶接口對象或者使之可用
SetCheck( int nCheck = 1) 標記用戶接口對象選中或未選中
SetRadio(BOOL bOn = TRUE)
SetText(LPCTSTR lpszText)
ContinueRouting()
還有一個MFC內部使用的成員函數:
DoUpdate(CCmdTarget* pTarget, BOOL bDisableIfNoHndler)
其中,參數1指向處理接收更新通知的命令目標,一般是邊框窗口;參數2指示如果沒有提供處理函數(例如某個菜單沒有對應的命令處理函數),是否禁止用戶對象。
DoUpdate作以下事情:
首先,發送狀態更新命令消息給參數1表示的命令目標:調用pTarget->OnCmdMsg(m_nID, CN_UPDATE_COMMAND_UI, this, NULL)發送m_nID對象的通知消息CN_UPDATE_COMMAND_UI。OnCmdMsg的參數3取值this,包含了當前要更新的用戶接口對象的信息。
然后,如果參數2為TRUE,調用pTarget->OnCmdMsg(m_nID,CN_COMMAND, this, &info)測試命令消息m_nID是否被處理。這時,OnCmdMsg的第四個參數非空,表示僅僅是測試,不是真的要派發消息。如果沒有提供命令消息m_nID的處理函數,則禁止用戶對象m_nID,否則使之可用。
從上面的討論可以知道:通過其結構,一個CCmdUI對象標識它表示了哪一個用戶接口對象,如果是菜單接口對象,pMenu表示了要更新的菜單對象;如果是工具條,pOther表示了要更新的工具條窗口對象,nID表示了工具條按鈕ID。
所以,由參數上狀態更新消息的消息處理函數就知道要更新什么接口對象的狀態。例如,第1節的函數OnUpdateEditPaste,函數參數pCmdUI表示一個菜單對象,需要更新該菜單對象的狀態。
通過其成員函數,一個CCmdUI可以更新、改變用戶接口對象的狀態。例如,CCmdUI可以管理菜單和對話框控制的狀態,調用Enable禁止或者允許菜單或者控制子窗口,等等。
所以,函數OnUpdateEditPaste可以直接調用參數的成員函數(如pCmdUI->Enable)實現菜單對象的狀態更新。
由于接口對象的多樣性,其他接口對象將從CCmdUI派生出管理自己的類來,覆蓋基類的有關成員函數如Enable等,提供對自身狀態更新的功能。例如管理狀態條和工具欄更新的CStatusCmdUI類和CToolCmdUI類。
自動更新用戶接口對象狀態的機制
MFC提供了分別用于更新菜單和工具條的兩種途徑。
更新菜單狀態
當用戶對菜單如File單擊鼠標時,就產生一條WM_INITMENUPOPUP消息,邊框窗口在菜單下拉之前響應該消息,從而更新該菜單所有項的狀態。
在應用程序開始運行時,邊框也會收到WM_INITMENUPOPUP消息。
更新工具條等狀態
當應用程序進入空閑處理狀態時,將發送WM_IDLEUPDATECMDUI消息,導致所有的工具條用戶對象的狀態處理函數被調用,從而改變其狀態。WM_IDLEUPDATECMDUI是MFC自己定義和使用的消息。
在窗口初始化時,工具條也會收到WM_IDLEUPDATECMDUI消息。
菜單狀態更新的實現
MFC讓邊框窗口來響應WM_INITMENUPOPUP消息,消息處理函數是OnInitMenuPopup,其原型如下:
afx_msg void CFrameWnd::OnInitMenuPopup( CMenu* pPopupMenu,
UINT nIndex, BOOL bSysMenu );
第一個參數指向一個CMenu對象,是當前按擊的菜單;第二個參數是菜單索引;第三個參數表示子菜單是否是系統控制菜單。
函數的處理:
如果是系統控制菜單,不作處理;否則,創建CCmdUI對象state,給它的各個成員如m_pMenu,m_pParentMenu,m_pOther等賦值。
對該菜單的各個菜單項,調函數state.DoUpdate,用CCmdUI的DoUpdate來更新狀態。DoUpdate的第一個參數是this,表示命令目標是邊框窗口;在CFrameWnd的成員變量m_bAutoMenuEnable為TRUE時(表示如果菜單m_nID沒有對應的消息處理函數或狀態更新函數,則禁止它),把DoUpdate的第二個參數bDisableIfNoHndler置為TRUE。
順便指出,m_bAutoMenuEnable缺省時為TRUE,所以,應用程序啟動時菜單經過初始化處理,沒有提供消息處理函數或狀態更新函數的菜單項被禁止。
工具條等狀態更新的實現
圖4-5表示了消息空閑時MFC更新用戶對象狀態的流程:
MFC提供的缺省空閑處理向頂層窗口(框架窗口)的所有子窗口發送消息WM_IDLEUPDATECMDUI;MFC的控制窗口(工具條、狀態欄等)實現了對該消息的處理,導致用戶對象狀態處理函數的調用。
雖然兩種途徑調用了同一狀態處理函數,但是傳遞的 CCmdUI參數從內部構成上是不一樣的:第一種傳遞的CCmdUI對象表示了一菜單對象,(pMenu域被賦值);第二種傳遞了一個窗口對象(pOther域被賦值)。同樣的狀態改變動作,如禁止、允許狀態的改變,前者調用了CMenu的成員函數EnableMenuItem,后者使用了CWnd的成員函數EnabelWindow。但是,這些不同由CCmdUI對象內部區分、處理,對用戶是透明的:不論菜單還是對應的工具條,用戶都用同一個狀態處理函數使用同樣的形式來處理。
這一節分析了用戶界面更新的原理和機制。在后面第13章討論工具條和狀態欄時,將詳細的分析這種機制的具體實現。
消息的預處理
到現在為止,詳細的討論了MFC的消息映射機制。但是,為了提高效率和簡化處理,MFC提供了一種消息預處理機制,如果一條消息在預處理時被過濾掉了(被處理),則不會被派發給目的窗口的窗口過程,更不會進入消息循環了。
顯然,能夠進行預處理的消息只可能是隊列消息,而且必須在消息派發之前進行預處理。因此,MFC在實現消息循環時,對于得到的每一條消息,首先送給目的窗口、其父窗口、其祖父窗口乃至最頂層父窗口,依次進行預處理,如果沒有被處理,則進行消息轉換和消息派發,如果某個窗口實現了預處理,則終止。有關實現見后面關于CWinThread線程類的章節,CWinThread的Run函數和PreTranslateMessage函數以及CWnd的函數WalkPreTranslateTree實現了上述要求和功能。這里要討論的是MFC窗口類如何進行消息預處理。
CWnd提供了虛擬函數PreTranslateMessage來進行消息預處理。CWnd的派生類可以覆蓋該函數,實現自己的預處理。下面,討論幾個典型的預處理。
首先,是CWnd的預處理:
預處理函數的原型為:
BOOL CWnd::PreTranslateMessage(MSG* pMsg)
CWnd類主要是處理和過濾Tooltips消息。關于該函數的實現和Tooltips消息,見后面第13章關于工具欄的討論。
然后,是CFrameWnd的預處理:
CFrameWnd除了調用基類CWnd的實現過濾Tooltips消息之外,還要判斷當前消息是否是鍵盤快捷鍵被按下,如果是,則調用函數::TranslateAccelerator(m_hWnd, hAccel, pMsg)處理快捷鍵。
接著,是CMDIChildWnd的預處理:
CMDIChildWnd的預處理過程和CFrameWnd的一樣,但是不能依靠基類CFrameWnd的實現,必須覆蓋它。因為MDI子窗口沒有菜單,所以它必須在MDI邊框窗口的上下文中來處理快捷鍵,它調用了函數::TranslateAccelerator(GetMDIFrame()->m_hWnd, hAccel,pMsg)。
討論了MDI子窗口的預處理后,還要討論MDI邊框窗口:
CMDIFrameWnd的實現除了CFrameWnd的實現的功能外,它還要處理MDI快捷鍵(標準MDI界面統一使用的系統快捷鍵)。
在后面,還會討論CDialog、CFormView、CToolBar等的消息預處理及其實現。
至于CWnd::WalkPreTranslateTree函數,它從接受消息的窗口開始,逐級向父窗回溯,逐一對各層窗口調用PreTranslateMessage函數,直到消息被處理或者到最頂層窗口為止。
MFC消息映射的回顧
從處理命令消息的過程可以看出,Windows消息和控制消息的處理要比命令消息的處理簡單,因為查找消息處理函數時,后者只要搜索當前窗口對象(this所指)的類或其基類的消息映射入口表。但是,命令消息就要復雜多了,它沿一定的順序鏈查找鏈上的各個命令目標,每一個被查找的命令目標都要搜索它的類或基類的消息映射入口表。
MFC通過消息映射的手段,以一種類似C++虛擬函數的概念向程序員提供了一種處理消息的方式。但是,若使用C++虛擬函數實現眾多的消息,將導致虛擬函數表極其龐大;而使用消息映射,則僅僅感興趣的消息才加入映射表,這樣就要節省資源、提高效率。這套消息映射機制的基礎包括以下幾個方面:
消息映射入口表的實現:采用了C++靜態成員和虛擬函數的方法來表示和得到一個消息映射類(CCmdTarget或派生類)的映射表。
消息查找的實現:從低層到高層搜索消息映射入口表,直至根類CCmdTarget。
消息發送的實現:主要以幾個虛擬函數為基礎來實現標準MFC消息發送路徑:OnComamnd、OnNotify、OnWndMsg和OnCmdMsg。、
OnWndMsg是CWnd類或其派生類的成員函數,由窗口過程調用。它處理標準的Windows消息。
OnCommand是CWnd類或其派生類的成員函數,由OnWndMsg調用來處理WM_COMMAND消息,實現命令消息或者控制通知消息的發送。如果派生類覆蓋該函數,則必須調用基類的實現,否則將不能自動的處理命令消息映射,而且必須使用該函數接受的參數(不是程序員給定值)調用基類的OnCommand。
OnNotify是CWnd類或其派生類的成員函數,由OnWndMsg調用來處理WM_NOTIFY消息,實現控制通知消息的發送。
OnCmdMsg是CCmdTarget類或其派生類的成員函數。被OnCommand調用,用來實現命令消息發送和派發命令消息到命令消息處理函數。
自動更新用戶對象狀態是通過MFC的命令消息發送機制實現的。
控制消息可以反射給控制窗口處理。
隊列消息在發送給窗口過程之前可以進行消息預處理,如果消息被MFC窗口對象預處理了,則不會進入消息發送過程
========
深度解析VC中的消息傳遞機制
?http://www.cnblogs.com/zhwx/archive/2012/05/16/2504323.html摘要:Windows編程和Dos編程,一個很大的區別就是,Windows編程是事件驅動,消息傳遞的。所以,要學好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 VaitMessage();
當一個應用程序無事可做時,該函數就將控制權交給另外的應用程序,同時將該應用程序掛起,直到一個新的消息被放入應用程
序的隊列之中才返回。
八、消息的處理?
接下來我們談一下消息的處理,首先我們來看一下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中的各種消息,以及在頭腦中根深蒂固的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(當前類, 當前類的基類)?
file://{{AFX_MSG_MAP(CMainFrame)?
消息的入口項?
file://}}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) 最后,在該文件中添加消息處理函數的實體。
========
VS2010/MFC編程入門之五(MFC消息映射機制概述)
http://www.jizhuomi.com/software/147.html? ? ??
? ? ? ?前面已經說過,Windows應用程序是消息驅動的。在MFC軟件開發中,界面操作或者線程之間通信都會經常用到消息,通過對消息的處理實現相應的操作。比較典型的過程是,用戶操作窗口,然后有消息產生,送給窗口的消息處理函數處理,對用戶的操作做出響應。
? ? ? ?什么是消息
? ? ? ?窗口消息一般由三個部分組成:1.一個無符號整數,是消息值;(2)消息附帶的WPARAM類型的參數;(3)消息附帶的LPARAM類型的參數。其實我們一般所說的消息是狹義上的消息值,也就是一個無符號整數,經常被定義為宏。
? ? ? ?什么是消息映射機制
? ? ? ?MFC使用一種消息映射機制來處理消息,在應用程序框架中的表現就是一個消息與消息處理函數一一對應的消息映射表,以及消息處理函數的聲明和實現等代碼。當窗口接收到消息時,會到消息映射表中查找該消息對應的消息處理函數,然后由消息處理函數進行相應的處理。SDK編程時需要在窗口過程中一一判斷消息值進行相應的處理,相比之下MFC的消息映射機制要方便好用的多。
? ? ? ?Windows消息分類
? ? ? ?先講下Windows消息的分類。Windows消息分為系統消息和用戶自定義消息。Windows系統消息有三種:
? ? ? ?1.標準Windows消息。除WM_COMMAND外以WM_開頭的消息是標準消息。例如,WM_CREATE、WM_CLOSE。
? ? ? ?2.命令消息。消息名為WM_COMMAND,消息中附帶了標識符ID來區分是來自哪個菜單、工具欄按鈕或加速鍵的消息。
? ? ? ?3.通知消息。通知消息一般由列表框等子窗口發送給父窗口,消息名也是WM_COMMAND,其中附帶了控件通知碼來區分控件。
? ? ? ?CWnd的派生類都可以接收到標準Windows消息、通知消息和命令消息。命令消息還可以由文檔類等接收。
? ? ? ?用戶自定義消息是實際上就是用戶定義一個宏作為消息,此宏的值應該大于等于WM_USER,然后此宏就可以跟系統消息一樣使用,窗口類中可以定義它的處理函數。
? ? ? ?消息映射表
? ? ? ?除了一些沒有基類的類或CObject的直接派生類外,其他的類都可以自動生成消息映射表。下面的講解都以前面例程HelloWorld的CMainFrame為例。消息映射表如下:
C++代碼
BEGIN_MESSAGE_MAP(CMainFrame, CFrameWndEx) ??
? ? ON_WM_CREATE() ??
? ? ON_COMMAND(ID_VIEW_CUSTOMIZE, &CMainFrame::OnViewCustomize) ??
? ? ON_REGISTERED_MESSAGE(AFX_WM_CREATETOOLBAR, &CMainFrame::OnToolbarCreateNew) ??
? ? ON_COMMAND_RANGE(ID_VIEW_APPLOOK_WIN_2000, ID_VIEW_APPLOOK_WINDOWS_7, &CMainFrame::OnApplicationLook) ??
? ? ON_UPDATE_COMMAND_UI_RANGE(ID_VIEW_APPLOOK_WIN_2000, ID_VIEW_APPLOOK_WINDOWS_7, &CMainFrame::OnUpdateApplicationLook) ??
? ? ON_WM_SETTINGCHANGE() ??
END_MESSAGE_MAP() ?
? ? ? ?在BEGIN_MESSAG_MAP和END_MESSAGE_MAP之間的內容成為消息映射入口項。消息映射除了在CMainFrame的實現文件中添加消息映射表外,在類的定義文件MainFrm.h中還會添加一個宏調用:
? ? ? ?DECLARE_MESSAGE_MAP()
? ? ? ?一般這個宏調用寫在類定義的結尾處。
VS2010/MFC編程入門之五(MFC消息映射機制概述)
? ? ? ?添加消息處理函數
? ? ? ?如何添加消息處理函數呢?不管是自動還是手動添加都有三個步驟:
? ? ? ?1.在類定義中加入消息處理函數的函數聲明,注意要以afx_msg打頭。例如MainFrm.h中WM_CREATE的消息處理函數的函數聲明:afx_msg int OnCreate(LPCREATESTRUCT lpCreateStruct);。
? ? ? ?2.在類的消息映射表中添加該消息的消息映射入口項。例如WM_CREATE的消息映射入口項:ON_WM_CREATE()。
? ? ? ?3.在類實現中添加消息處理函數的函數實現。例如,MainFrm.cpp中WM_CREATE的消息處理函數的實現:
? ? ? ? ? int CMainFrame::OnCreate(LPCREATESTRUCT lpCreateStruct)
? ? ? ? ?{
? ? ? ? ? ? ? ? ? ......
? ? ? ? ?}
? ? ? ?通過以上三個步驟以后,WM_CREATE等消息就可以在窗口類中被消息處理函數處理了。
? ? ? ?各種Windows消息的消息處理函數
? ? ? ?標準Windows消息的消息處理函數都與WM_CREATE消息類似。
? ? ? ?命令消息的消息映射入口項形式如:ON_COMMAND(ID_VIEW_CUSTOMIZE, &CMainFrame::OnViewCustomize),消息為ID_VIEW_CUSTOMIZE,消息處理函數為OnViewCustomize。
? ? ? ?如果想要使用某個處理函數批量處理某些命令消息,則可以像CMainFrame消息映射表中的ON_COMMAND_RANGE(ID_VIEW_APPLOOK_WIN_2000, ID_VIEW_APPLOOK_WINDOWS_7, &CMainFrame::OnApplicationLook)一樣添加消息映射入口項,這樣值在ID_VIEW_APPLOOK_WIN_2000到ID_VIEW_APPLOOK_WINDOWS_7之間的菜單項等的命令消息都由CMainFrame的OnApplicationLook函數處理。函數原型為afx_msg void OnApplicationLook(UINT id);,參數id為用戶操作的菜單項等的ID。
? ? ? ?在操作列表框等控件時往往會給父窗口發送WM_NOTIFY通知消息。WM_NOTIFY消息的wParam參數為發送通知消息的控件的ID,lParam參數指向一個結構體,可能是NMHDR結構體,也可能是第一個元素為NMHDR結構體變量的其他結構體。NMHDR結構體的定義如下(僅作了解):
? ? ? ?Typedef sturct tagNMHDR{
? ? ? ? ? ? ? ? HWND hwndFrom;
? ? ? ? ? ? ? ? UINT idFrom;
? ? ? ? ? ? ? ? UINT code;
? ? ? ?} NMHDR;
? ? ? ?hwndFrom為發送通知消息控件的句柄,idFrom為控件ID,code為要處理的通知消息的通知碼,例如NM_CLICK。
? ? ? ?通知消息的消息映射入口項形式如:
? ? ? ?ON_NOTIFY(wNotifyCode,id,memberFxn)
? ? ? ?wNotifyCode為要處理的通知消息通知碼,例如:NM_CLICK。id為控件標識ID。MemberFxn為此消息的處理函數。
? ? ? ?通知消息的處理函數的原型為:
? ? ? ?afx_msg void memberFxn( NMHDR * pNotifyStruct, LRESULT * result);
? ? ? ?如果需要使用用戶自定義消息,首先要定義消息宏,如:#define WM_UPDATE_WND (WM_USER+1),再到消息映射表中添加消息映射入口項:ON_MESSAGE(WM_UPDATE_WND, &CMainFrame::OnUpdateWnd),然后在MainFrm.h中添加消息處理函數的函數聲明:afx_msg LRESULT OnUpdateWnd(WPARAM wParam, LPARAM lParam);,最后在MainFrm.cpp中實現此函數。
========
總結
以上是生活随笔為你收集整理的MFC消息处理学习总结的全部內容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: C#语法糖学习总结
- 下一篇: VC++网络资源集合