天天看點

進入MFC講壇的前言(三)

MFC中的視窗建立及視窗消息映射

我經常碰到有人問我有關視窗建立的問題,他們經常把用HWND描述的系統視窗對象和用CWnd描述的MFC的視窗對象混淆不清。這兩者之間是緊密聯系在一起的,但是MFC為了自身的管理,在CWnd中加了一些額外的内容,包括如何從HWND生成CWnd。

  在MFC中,有幾種典型的視窗對象,CWnd描述的一般視窗對象,CView描述的視圖對象,CFrameWnd描述的SDI框窗對象,CMDIFrameWnd描述的MDI框窗對象等等。在這一章中,主要讨論下述内容:

  MFC中視窗的建立

  MFC的消息映射機制(MESSAGE MAP)

  對于上面兩點MFC的設計者們使用了很高的技巧來確定應用程式的代碼盡可能小,其中的技巧和隐藏在它們背後的思想值得我們學習。下面對各項内容進行讨論。

  在Window下,建立視窗可以使用兩個函數,CreateWindow()和CreateWindowEx(),它們都需要一個參數,這個參數是辨別視窗類的字元串。是以,如果要建立視窗,一般的做法是,先使用RegisterClass()或RegisterClassEx()注冊一個視窗類,然後使用該視窗類來建立視窗。在前面我也提到過,注冊視窗類的最主要目的是為系統提供視窗函數的位址,以便被DispatchMessage()之類的函數回。

  在MFC中,建立視窗的函數是CWnd或其派生類的Create()或CreateEx方法,注冊視窗類一般使用AfxRegisterWndClass(),在這個全局函數中,并沒有發現視窗函數位址這樣的參數,是以腦子裡自然就會有這樣的問題:視窗函數在哪裡?它是如何同視窗關聯的?下面我們将對MFC的一些與此有關的代碼進行仔細分析,回答上述兩個問題。

  視窗函數

  在MFC中,有一個全局的函數AfxWndProc(),正如下面的注釋所示,它就是CWnd及所有從它派生的視窗類的視窗函數,它的實作如下:

  // The WndProc for all CWnd's and derived classes

  LRESULT CALLBACK

  AfxWndProc(HWND hWnd, UINT nMsg, WPARAM wParam, LPARAM lParam)

  {

   // special message which identifies the window as using AfxWndProc

   if (nMsg == WM_QUERYAFXWNDPROC)

   return 1;

   // all other messages route through message map

   CWnd* pWnd = CWnd::FromHandlePermanent(hWnd);

   ASSERT(pWnd != NULL);

   ASSERT(pWnd->m_hWnd == hWnd);

   return AfxCallWndProc(pWnd, hWnd, nMsg, wParam, lParam);

  }

  AfxCallWndProc()調用pWnd對象的虛拟函數WindowProc(),它的代碼如下:

  LRESULT CWnd::WindowProc(UINT message, WPARAM wParam, LPARAM lParam)

   // OnWndMsg does most of the work, except for DefWindowProc call

   LRESULT lResult = 0;

   if (!OnWndMsg(message, wParam, lParam, &lResult))

   lResult = DefWindowProc(message, wParam, lParam);

   return lResult;

上面的代碼中,OnWndMsg()是用來處理該視窗消息的函數,如果某條消息沒有被OnWndMsg()處理,也就是該視窗沒有提供處理該消息的函數,它就調用DefWindowProc()進行處理,DefWindowProc()也是一個虛拟函數,看看它的代碼:

  LRESULT CWnd::DefWindowProc(UINT nMsg, WPARAM wParam, LPARAM lParam)

   if (m_pfnSuper != NULL)

   return ::CallWindowProc(m_pfnSuper, m_hWnd, nMsg, wParam, lParam);

   WNDPROC pfnWndProc;

   if ((pfnWndProc = *GetSuperWndProcAddr()) == NULL)

    return ::DefWindowProc(m_hWnd, nMsg, wParam, lParam);

   else

   return ::CallWindowProc(pfnWndProc, m_hWnd, nMsg, wParam, lParam);

DefWindowProc()的政策很簡單,調用基類的視窗函數m_pfnSuper來處理該消息。

  通過上面的分析,可以得出這樣的結論:與其說AfxWndProc()是MFC的唯一視窗函數,還不如說AfxWndProc()是MFC的視窗消息分發中心。正是由于有了這個消息分發中心,才使得MFC的應用程式能夠用有限的幾個視窗類,作出各種形形色色的視窗,使得在應用程式中,增加CWnd的派生類,并不增加系統中視窗類的個數,将對系統資源的使用控制在一個穩定的範圍之内。

  注冊視窗類除了提供視窗函數外,還指定該視窗的一些外觀,如是否有标題條,視窗預設背景等等。在MFC架構中,有框窗、視圖和控制條(CControlBar)等,它們除了操作行為不同外,外觀等也不相同,是以MFC注冊了幾種預設的視窗類。在MFC中,有一個全局函數AfxEndDeferRegisterClass(LONG fToRegister),它用來注冊MFC預定義的視窗類,包括同框窗、視圖所對應的視窗類。由于它的代碼占的篇幅很長,而且實作也很簡單,是以就不列出它的代碼了,如果你有興趣,可以在wincore.cpp中找到它的實作代碼。

  挂接視窗函數

  如果你考察過AfxEndDeferRegisterClass()的實作代碼,你會對一行代碼感到迷惑,下面列出的是AfxEndDeferRegisterClass()的部分代碼,帶陰影部分的是那一行令人迷惑的代碼:

  BOOL AFXAPI AfxEndDeferRegisterClass(LONG fToRegister)

   。。。。。。

   wndcls.lpfnWndProc = DefWindowProc;

  MFC将所有預定義的視窗類的視窗函數都設定成DefWindowProc。大家都知道,DefWindowProc是Window下為一般視窗提供消息預設處理的API,它肯定不是應用程式所需要的視窗函數,是以,MFC肯定在某個地方置換了它,置換DefWindowProc的代碼在哪裡呢?

  前面說過,在MFC中建立視窗時是用CWnd的兩個虛拟函數Create()和CreateEx(),Create()是通過調用CreateEx()實作的,是以最終視窗的建立都要歸結到CreateEx()函數上。是以,我們可以推斷MFC在CreateEx()中置換了DefWindowProc。為了證明這一點,看看CWnd::CreateEx()的代碼:

  BOOL CWnd::CreateEx(DWORD dwExStyle, LPCTSTR lpszClassName,

      LPCTSTR lpszWindowName, DWORD dwStyle,

      int x, int y, int nWidth, int nHeight,

      HWND hWndParent, HMENU nIDorHMenu, LPVOID lpParam)

   // allow modification of several common create parameters

   CREATESTRUCT cs;

   cs.dwExStyle = dwExStyle;

   cs.lpszClass = lpszClassName;

   cs.lpszName = lpszWindowName;

   cs.style = dwStyle;

   cs.x = x;

   cs.y = y;

   cs.cx = nWidth;

   cs.cy = nHeight;

   cs.hwndParent = hWndParent;

   cs.hMenu = nIDorHMenu;

   cs.hInstance = AfxGetInstanceHandle();

   cs.lpCreateParams = lpParam;

   if (!PreCreateWindow(cs)){

    PostNcDestroy();

    return FALSE;

   }

   AfxHookWindowCreate(this);

   HWND hWnd = ::CreateWindowEx(cs.dwExStyle, cs.lpszClass,

   cs.lpszName, cs.style, cs.x, cs.y, cs.cx, cs.cy,

   cs.hwndParent, cs.hMenu, cs.hInstance, cs.lpCreateParams);

  if (!AfxUnhookWindowCreate()) PostNcDestroy(); // cleanup if CreateWindowEx fails too soon

   if (hWnd == NULL) return FALSE;

   ASSERT(hWnd == m_hWnd); // should have been set in send msg hook

   return TRUE;

從上面的代碼看不出任何顯式的置換DefWindowProc的代碼,其實,它隐藏在AfxHookWindowCreate(this)之中,順藤摸瓜,再看看AfxHookWindowCreate()的代碼:

  void AFXAPI AfxHookWindowCreate(CWnd* pWnd)

   _AFX_THREAD_STATE* pThreadState = _afxThreadState.GetData();

   if (pThreadState->m_pWndInit == pWnd)

   return;

   if (pThreadState->m_hHookOldCbtFilter == NULL)

   {

    pThreadState->m_hHookOldCbtFilter = ::SetWindowsHookEx(WH_CBT,

    _AfxCbtFilterHook, NULL, ::GetCurrentThreadId());

    if (pThreadState->m_hHookOldCbtFilter == NULL)

    AfxThrowMemoryException();

    }

   ASSERT(pThreadState->m_hHookOldCbtFilter != NULL);

   ASSERT(pWnd->m_hWnd == NULL); // only do once

   ASSERT(pThreadState->m_pWndInit == NULL); // hook not already in progress

   pThreadState->m_pWndInit = pWnd;

AfxHookWindowCreate()設定了一個線程級的CBT Hook,該Hook的入口位址為_AfxCbtFilterHook,_AfxCbtFilterHook是一個全局的MFC函數,_AfxCbtFilterHook通過調用SetWindowLong()用AfxWndProc()的入口位址置換掉DefWindowProc。

  小結:

  1、在MFC中,建立一個視窗的過程是:

    1、生成一個對應視窗類的對象

    2、調用該對象的Create()或CreateEx()方法

    3、在CreateEx()中(Create()是調用CreateEx()實作的),先調用虛拟函數PreCreateWindow(),讓應用程式有一個改變視窗行為的機會,同時,在PreCreateWindow中,還作了一件很重要的事情,就是如果指向視窗類的字元串指針為NULL,就調用AfxDeferRegisterClass()注冊一個合适的視窗類,将該注冊的視窗類作為建立視窗的類型參數。AfxDeferRegisterClass()就是AfxEndDeferRegisterClass(),後者根據參數注冊相應的視窗類,并将DefWindowProc作為該視窗類的臨時視窗函數。

    4、CreateEx()在調用CreateWindowEx()建立真正的視窗對象之前,設定一個線程級的CBT Hook,該hook在視窗建立完成後被調用,MFC在hook函數中調用SetWindowLong()将該視窗的視窗函數置換成AfxWndProc。

  2、從Window的角度看,任何一個MFC應用程式都隻有一個視窗函數AfxWndProc。

  3、AfxWndProc的作用是截獲所有的發送給視窗消息,并将這些消息發送給相應的視窗對象的視窗函數WindowProc處理,是以它實質上是一個視窗消息分發器,注意,非視窗消息不被AfxWndProc所分發,它們在AfxWndProc被調用之前就被CWinThread::PreTranslateMessage()處理過了。

  4、同PreTranslateMessage()不同的地方在于,AfxWndProc()能夠截獲所有的來自于消息隊列的和非消息隊列的視窗消息(如調用SendMessage()發送的視窗消息)。

繼續閱讀