MFC的消息響應機制說明
- 2020 年 11 月 19 日
- 筆記
MFC的快速理解:
1.MFC的設計者們在設計MFC時,有一個主要的方向就是儘可能使得MFC的程式碼要小,速度儘可能快。為了這個方向,工程師們使用了許多技巧,主要表現在宏的運用上,實
現MFC的消息映射的機制就是其中之一。
2.MFC消息映射機制有關的宏有下面幾個:
1)DECLARE_MESSAGE_MAP()宏
2)BEGIN_MESSAGE_MAP(theClass, baseClass)宏
3)END_MESSAGE_MAP()宏
弄懂MFC消息映射機制的最好辦法是將找出一個具體的實例,將這些宏展開,並找出相關的數據結構。
DECLARE_MESSAGE_MAP()
DECLARE_MESSAGE_MAP()宏的定義如下:
#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;
從上面的定義可以看出,DECLARE_MESSAGE_MAP()作下面三件事:
定義一個長度不定的靜態數組變數_messageEntries];
定義一個靜態變數messageMap;
定義一個虛擬函數GetMessageMap();
在DECLARE_MESSAGE_MAP()宏中,涉及到MFC中兩個對外不公開的數據結構AFX_MSGMAP_ENTRY和AFX_MSGMAP。為了弄清楚消息映射,有必要考察一下這兩
個數據結構的定義。
AFX_MSGMAP_ENTRY的定義:
struct AFX_MSGMAP_ENTRY
{
UINT nMessage; // windows message
UINT nCode; // control code or WM_NOTIFY code
UINT nID; // control ID (or 0 for windows messages)
UINT nLastID; // used for entries specifying a range of control id’s
UINT nSig; // signature type (action) or pointer to message #
AFX_PMSG pfn; // routine to call (or special value)
};
結構中各項的含義已經注釋,從上面的定義你是否看出,AFX_MSGMAP_ENTRY結構實際上定義了消息和處理此消息的動作之間的映射關係。
因此靜態數組變數_messageEntries]實際上定義了一張表,表中的每一項指定了相應的對象所要處理的消息和處理此消息的函數的對應關係,這張表也稱為消息映射表。
再看看AFX_MSGMAP的定義。
struct AFX_MSGMAP
{
const AFX_MSGMAP* pBaseMap;
const AFX_MSGMAP_ENTRY* lpEntries;
};
不難看出,AFX_MSGMAP定義了一單向鏈表,鏈表中每一項的值是一指向消息映射表的指針(實際上就是_messageEntries的值)。通過這個鏈表,使得在某個類中調用
基類的的消息處理函數很容易,因此,「父類的消息處理函數是子類的預設消息處理函數」就「順理成章」了。
在後面的「MFC窗口的消息處理」一節中會對此作詳細的講解。
由上述可見,在類的頭文件中主要定義了兩個數據結構:消息映射表和單向鏈表。
BEGIN_MESSAGE_MAP()和END_MESSAGE_MAP()
它們的定義如下:
#define BEGIN_MESSAGE_MAP(theClass, baseClass)
const AFX_MSGMAP* theClass::GetMessageMap() const
{
return &theClass::messageMap;
}
AFX_COMDAT AFX_DATADEF const AFX_MSGMAP theClass::messageMap
{
&baseClass::messageMap, &theClass::_messageEntries0]
};
AFX_COMDAT const AFX_MSGMAP_ENTRY theClass::_messageEntries]
{
#define END_MESSAGE_MAP()
{0, 0, 0, 0, AfxSig_end, (AFX_PMSG)0 }
};
對應BEGIN_MESSAGE_MAP()的定義可能不是一下子就看得明白,不過不要緊,舉一例子就很清楚了。對於BEGIN_MESSAGE_MAP(CView, CWnd),VC預編譯器將其展
開成下面的形式:
const AFX_MSGMAP* CView::GetMessageMap() const
{
return &CView::messageMap;
}
AFX_COMDAT AFX_DATADEF const AFX_MSGMAP CView::messageMap
{
&CWnd::messageMap,
&CView::_messageEntries0
};
AFX_COMDAT const AFX_MSGMAP_ENTRY CView::_messageEntries
{
至於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
},
根據上面的定義,ON_COMMAND(ID_FILE_NEW, OnFileNew)將被VC預編譯器展開如下:
{
WM_COMMAND, CN_COMMAND, (WORD)id, (WORD)id, AfxSig_vv, (AFX_PMSG)&OnFileNew
},
3.MFC的消息映射機制已經清楚了,現在提出並解答兩個問題以作為對這一節的小結。
為什麼不直接使用虛擬函數實現消息處理函數呢?
MFC的設計者們在設計MFC時有一個很明確的目標,就是使得「MFC的程式碼儘可能小,速度儘可能快」,如果採用虛擬函數,那麼對於所有的窗口消息,都必須有一個與之對
應的虛擬函數,因而對每一個從CWnd派生的類而言,都會有一張很大的虛擬函數表vtbl。但是在實際應用中,一般只對少數的消息進行處理,大部分都交給系統預設處理,所以
表中的大部分項都是無用項,這樣做就浪費了很多記憶體資源,這同MFC設計者們的設計目標是相違背的。當然,MFC所使用的方法只是解決這類問題的方式之一,不排除還有
其他的解決方式,但就我個人觀點而言,這是一種最好的解決方式,體現了很高的技巧性,值得我們學習。
第二個問題,是由上面的問題引申出來的。如果在子類和父類中出現了相同的消息出來函數,VC編譯器會怎麼處理這個問題呢?VC不會將它們看作錯誤,而會象對
待虛擬函數類似的方式去處理,但對於消息處理函數(帶afx_msg前綴),則不會生成虛擬函數表vtbl。
MFC下一個消息的處理過程是一般是這樣的。
1)_AfxCbtFilterHook截獲消息(這是一個鉤子函數)
2)_AfxCbtFilterHook把窗口過程設定為AfxWndProc。
3)函數AfxWndProc接收Windows作業系統發送的消息。
4)函數AfxWndProc調用函數AfxCallWndProc進行消息處理。
5)函數AfxCallWndProc調用CWnd類的方法WindowProc進行消息處理。
4.如何添加自己的消息?
我們已經了解了WINDOW的消息機制,如何加入我們自己的消息呢?好我們來看一個標準的消息處理程式是這個樣子的,在 CWnd 類中預定義了標準 Windows 消息 (WM_XXXX
WM是WINDOW MESSAGE的縮寫) 的默認處理程式。類庫基於消息名命名這些處理程式。例如,WM_PAINT 消息的處理程式在 CWnd 中被聲明為:
afx_msg void OnPaint();
afx_msg 關鍵字通過使這些處理程式區別於其他 CWnd 成員函數來表明 C++ virtual 關鍵字的作用。但是請注意,這些函數實際上並不是虛擬的,而是通過消息映射實現的。
我們在本文的一開始便說明了為什麼要這樣做。
所有能夠進行消息處理的類都是基於CCmdTarget類的,也就是說CCmdTarget類是所有可以進行消息處理類的父類。CCmdTarget類是MFC處理命令消息的基礎和核心。
若要重寫基類中定義的處理程式,只需在派生類中定義一個具有相同原型的函數,並創建此處理程式的消息映射項。我們通過ClassWizard可以建立大多數窗口消息或自定義的
消息,通過ClassWizard可以自動建立消息映射,和消息處理函數的框架,我們只需要把我們要做的事情填空,添加你要做的事情到處理函數。這個非常簡單,就不細說了。但是
也許我們需要添加一些ClassWizard不支援的窗口消息或自定義消息,那麼就需要我們親自動手建立消息映射和消息處理的框架,通常步驟如下:
第一步:定義消息。Microsoft推薦用戶自定義消息至少是WM_USER+100,因為很多新控制項也要使用WM_USER消息。
#define WM_MYMESSAGE (WM_USER + 100)
第二步:實現消息處理函數。該函數使用WPRAM和LPARAM參數並返回LPESULT。
LPESULT CMainFrame::OnMyMessage(WPARAM wParam, LPARAM lParam)
{
// TODO: 處理用戶自定義消息,填空就是要填到這裡。
return 0;
}
第三步:在類頭文件的AFX_MSG塊中說明消息處理函數:
// {{AFX_MSG(CMainFrame)
afx_msg LRESULT OnMyMessage(WPARAM wParam, LPARAM lParam);
//}}AFX_MSG
DECLARE_MESSAGE_MAP()
第四步:在用戶類的消息塊中,使用ON_MESSAGE宏指令將消息映射到消息處理函數中。
ON_MESSAGE( WM_MYMESSAGE, OnMyMessage )
5.MFC的機制大概就是這些了,如需在深入學習,可以看孫鑫的《VC++深入詳解》或者是侯傑的《深入淺出MFC》。
改變自己,從現在做起———–久館