MFC消息響應機制分析


---- MFC是Windows下程序設計的最流行的一個類庫,但是該類庫比較龐雜,尤其是它的消息映射機制,更是涉及到很多低層的東西,我們在這里,對它的整個消息映射機制進行了系統的分析,可以幫助程序開發人員對MFC的消息映射機制有一個比較透徹的了解。

1.引言

---- VC++的MFC類庫實際上是Windows下C++編程的一套最為流行的類庫。MFC的框架結構大大方便了程序員的編程工作,但是為了更加有效、靈活的使用MFC編程,了解MFC的體系結構往往可以使編程工作事半功倍。它合理的封裝了WIN32 API函數,並設計了一套方便的消息映射機制。但這套機制本身比較龐大和復雜,對它的分析和了解無疑有助於我們寫出更為合理的高效的程序。這里我們簡單的分析MFC的消息響應機制,以了解MFC是如何對Windows的消息加以封裝,方便用戶的開發。

2. SDK下的消息機制實現
---- 這里簡單的回顧一下SDK下我們是如何進行Windows的程序開發的。一般來說,Windows的消息都是和線程相對應的。即Windows會把消息發送給和該消息相對應的線程。在SDK的模式下,程序是通過GetMessage函數從和某個線程相對應的消息隊列里面把消息取出來並放到一個特殊的結構里面,一個消息的結構是一個如下的STRUCTURE。
typedef struct tagMSG {
HWND hwnd;
UINT message;
WPARAM wParam;
LPARAM lParam;
DWORD time;
POINT pt;
}MSG;
---- 其中hwnd表示和窗口過程相關的窗口的句柄,message表示消息的ID號,wParam和lParam表示和消息相關的參數,time表示消息發送的時間,pt表示消息發送時的鼠標的位置。

---- 然后TranslateMessage函數用來把虛鍵消息翻譯成字符消息並放到響應的消息隊列里面,最后DispatchMessage函數把消息分發到相關的窗口過程。然后窗口過程根據消息的類型對不同的消息進行相關的處理。在SDK編程過程中,用戶需要在窗口過程中分析消息的類型和跟消息一起的參數的含義,做不同的處理,相對比較麻煩,而MFC把消息調用的過程給封裝起來,使用戶能夠通過ClassWizard方便的使用和處理Windows的各種消息。

3MFC的消息實現機制
---- 我們可以看到,在MFC的框架結構下,可以進行消息處理的類的頭文件里面都會含有DECLARE_MESSAGE_MAP()宏,這里主要進行消息映射和消息處理函數的聲明。可以進行消息處理的類的實現文件里一般都含有如下的結構。
BEGIN_MESSAGE_MAP(CInheritClass, CBaseClass)
//{{AFX_MSG_MAP(CInheritClass)
//}}AFX_MSG_MAP
END_MESSAGE_MAP()


---- 這里主要進行消息映射的實現和消息處理函數的實現。
---- 所有能夠進行消息處理的類都是基於CCmdTarget類的,也就是說CCmdTarget類是所有可以進行消息處理類的父類。CCmdTarget類是MFC處理命令消息的基礎和核心。

---- 同時MFC定義了下面的兩個主要結構:

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
struct AFX_MSGMAP
{
#ifdef _AFXDLL
const AFX_MSGMAP* (PASCAL* pfnGetBaseMap)();
#else
const AFX_MSGMAP* pBaseMap;
#endif
const AFX_MSGMAP_ENTRY* lpEntries;
};

其中AFX_MSGMAP_ENTRY結構包含了
一個消息的所有相關信息,其中

nMessage為Windows消息的ID號
nCode為控制消息的通知碼
nID為Windows控制消息的ID
nLastID表示如果是一個指定范圍的消息被映射的話,
nLastID用來表示它的范圍。
nSig表示消息的動作標識
AFX_PMSG pfn 它實際上是一個指向和該消息相應的執行函數的指針。

---- 而AFX_MSGMAP主要作用是兩個,一:用來得到基類的消息映射入口地址。二:得到本身的消息映射入口地址。

---- 實際上,MFC把所有的消息一條條填入到AFX_MSGMAP_ENTRY結構中去,形成一個數組,該數組存放了所有的消息和與它們相關的參數。同時通過AFX_MSGMAP能得到該數組的首地址,同時得到基類的消息映射入口地址,這是為了當本身對該消息不響應的時候,就調用其基類的消息響應。

---- 現在我們來分析MFC是如何讓窗口過程來處理消息的,實際上所有MFC的窗口類都通過鈎子函數_AfxCbtFilterHook截獲消息,並且在鈎子函數_AfxCbtFilterHook中把窗口過程設定為AfxWndProc。原來的窗口過程保存在成員變量m_pfnSuper中。

---- 所以在MFC框架下,一般一個消息的處理過程是這樣的。

函數AfxWndProc接收Windows操作系統發送的消息。

函數AfxWndProc調用函數AfxCallWndProc進行消息處理,這里一個進步是把對句柄的操作轉換成對CWnd對象的操作。

函數AfxCallWndProc調用CWnd類的方法WindowProc進行消息處理。注意AfxWndProc和AfxCallWndProc都是AFX的API函數。而WindowProc已經是CWnd的一個方法。所以可以注意到在WindowProc中已經沒有關於句柄或者是CWnd的參數了。

方法WindowProc調用方法OnWndMsg進行正式的消息處理,即把消息派送到相關的方法中去處理。消息是如何派送的呢?實際上在CWnd類中都保存了一個AFX_MSGMAP的結構,而在AFX_MSGMAP結構中保存有所有我們用ClassWizard生成的消息的數組的入口,我們把傳給OnWndMsg的message和數組中的所有的message進行比較,找到匹配的那一個消息。實際上系統是通過函數AfxFindMessageEntry來實現的。找到了那個message,實際上我們就得到一個AFX_MSGMAP_ENTRY結構,而我們在上面已經提到AFX_MSGMAP_ENTRY保存了和該消息相關的所有信息,其中主要的是消息的動作標識和跟消息相關的執行函數。然后我們就可以根據消息的動作標識調用相關的執行函數,而這個執行函數實際上就是通過ClassWizard在類實現中定義的一個方法。這樣就把消息的處理轉化到類中的一個方法的實現上。舉一個簡單的例子,比如在View中對WM_LButtonDown消息的處理就轉化成對如下一個方法的操作。
void CInheritView::OnLButtonDown
(UINT nFlags, CPoint point)
{
// TODO: Add your message
handler code here and/or call default
CView::OnLButtonDown(nFlags, point);
}

注意這里CView::OnLButtonDown(nFlags, point)實際上就是調用CWnd的Default()方法。 而Default()方法所做的工作就是調用DefWindowProc對消息進行處理。這實際上是調用原來的窗口過程進行缺省的消息處理。

如果OnWndMsg方法沒有對消息進行處理的話,就調用DefWindowProc對消息進行處理。這是實際上是調用原來的窗口過程進行缺省的消息處理。
---- 所以如果正常的消息處理的話,MFC窗口類是完全脫離了原來的窗口過程,用自己的一套體系結構實現消息的映射和處理。即先調用MFC窗口類掛上去的窗口過程,再調用原先的窗口過程。並且用戶面對和消息相關的參數不再是死板的wParam和lParam,而是和消息類型具體相關的參數。比如和消息WM_LbuttonDown相對應的方法OnLButtonDown的兩個參數是nFlags和point。nFlags表示在按下鼠標左鍵的時候是否有其他虛鍵按下,point更簡單,就是表示鼠標的位置。
---- 同時MFC窗口類消息傳遞中還提供了兩個函數,分別為WalkPreTranslateTree和PreTranslateMessage。我們知道利用MFC框架生成的程序,都是從CWinApp開始執行的,而CWinapp實際繼承了CWinThread類。在CWinThread的運行過程中會調用窗口類中的WalkPreTranslateTree方法。而WalkPreTranslateTree方法實際上就是從當前窗口開始查找願意進行消息翻譯的類,直到找到窗口沒有父類為止。在WalkPreTranslateTree方法中調用了PreTranslateMessage方法。實際上PreTranslateMessage最大的好處是我們在消息處理前可以在這個方法里面先做一些事情。舉一個簡單的例子,比如我們希望在一個CEdit對象里,把所有的輸入的字母都以大寫的形式出現。我們只需要在PreTranslateMessage方法中判斷message是否為WM_CHAR,如果是的話,把wParam(表示鍵值)由小寫字母的值該為大寫字母的值就實現了這個功能。

---- 繼續上面的例子,根據我們對MFC消息機制的分析,我們很容易得到除了上面的方法,我們至少還可以在另外兩個地方進行操作。

---- 一:在消息的處理方法里面即OnChar中,當然最后我們不再調用CEdit::OnChar(nChar, nRepCnt, nFlags),而是直接調用DefWindowProc(WM_CHAR,nChar,MAKELPARAM (nRepCnt,nFlags))。因為從我們上面的分析可以知道CEdit::OnChar(nChar, nRepCnt, nFlags)實際上也就是對DefWindowProc方法的調用。

---- 二:我們可以直接重載DefWindowProc方法,對message類型等於WM_CHAR的,直接修改nChar的值即可。

4.小結
---- 通過對MFC類庫的分析和了解,不僅能夠使我們更好的使用MFC類庫,同時,對於我們自己設計和實現框架和類,無疑也有相當大的幫助。

 

 

二.MFC的消息映射機制 

MFC的設計者們在設計MFC時,緊緊把握一個目標,那就是盡可能使得MFC的代碼要小,速度盡可能快。為了這個目標,他們使用了許多技巧,其中很多技巧體現在宏的運用上,實現MFC的消息映射的機制就是其中之一。
  同MFC消息映射機制有關的宏有下面幾個:
  DECLARE_MESSAGE_MAP()宏
  BEGIN_MESSAGE_MAP(theClass, baseClass)和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的定義。
  (2)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::_messageEntries[0] }; \
  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::_messageEntries[0]
  };
  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},

到此,MFC的消息映射機制已經清楚了,現在提出並解答兩個問題以作為對這一節的小結。
  為什么不直接使用虛擬函數實現消息處理函數呢?這是一個GOOD QUESTION。前面已經說過,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進行消息處理。

如何添加自己的消息?
我們已經了解了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 )


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM