深入理解MFC子類化


子類化,通俗來講就是用自己的窗口處理函數來處理特定消息,並將自己其他消息還給標准(默認)窗口處理函數。在SDK中,通過SetWindowLong來指定一個自定義窗口處理函數:SetWindowLong(hwnd, GWL_WNDPROC, (LONG)UserWndProc);。可是到了MFC中,大部分基礎的東西都被封裝起來了,那么,這是該怎么實現子類化呢?
       先來看一個例子:
       要求:定義一個Edit控件,讓它能夠對輸入進行特定的處理輸入進行處理-----只能輸入英文字母,對其他輸入作出提示。
       分析:1)處理輸入當然是響應WM_CHAR消息了,然后對輸入字符進行判斷,並做相應處理。那么,我們有怎么才能讓Edit自己處理輸入呢?
                  2)我們知道Windows為我們設計Edit控件時,已經將常用操作通過成員函數的形式封裝在CEdit類中了,直接由CEdit生成的對象自己並不能改變原有方法或是定制自己的方法(除了虛函數,但有時我們想實現的並不是虛函數啊!),那么現在想達到這些情況應該怎么辦呢?這就用到本篇文章的主題-----MFC子類化。
                  3)我們可以從CEdit類派生一個新類CSuperEdit,然后通過子類化方法是Edit窗口來使用我們指定的消息處理函數。
        實現:先CSuperEdit,並為其添加WM_CHAR消息響應函數,這樣CSuperEdit對象就擁有了自己WM_CHAR響應函數(這正是子類化的效果所在----面向對象----自己的方法封裝在自己的類中),然后在其父窗口類(這里我們用一個基於對話框的MFC程序)中聲明一個CSuperEdit類對象m_edit,當然m_edit需要和一個實際存在的窗口關聯起來,因此,在CXXXDialog::OnInitDialogj中添加:m_edit.SubclassDlgItem(IDC_EDIT1,this);這樣
就將m_edit這個c++對象和IDC_EDIT1窗口關聯起來了,然后我們只需要在CSuperEdit::OnChar()中添加相應的操作就OK了。

 

原理探討

    追溯的目標:在整個程序中的哪個位置改變了m_edit關聯窗口的消息處理函數。

        首先,來探討一下m_edit和窗口關聯實現:m_edit.SetclassDlgItem(IDC_EDIT1,this); 我們進入該函數中看看:

BOOL CWnd::SubclassDlgItem(UINT nID, CWnd* pParent)

{

     ASSERT(pParent != NULL);

     ASSERT(::IsWindow(pParent->m_hWnd));

     // check for normal dialog control first

     HWND hWndControl = ::GetDlgItem(pParent->m_hWnd, nID);

     if (hWndControl != NULL)

            return SubclassWindow(hWndControl);

     // 省略無關代碼 

       … …

     return FALSE;   // control not found

}

查看MSDN

CWnd::SubclassDlgItem

This method dynamically subclasses a control created from a dialog box template, and attach it to this CWnd object. When a control is dynamically subclassed, windows messages will route through the CWnd message map and call message handlers in the CWnd class first. Messages that are passed to the base class will be passed to the default message handler in the control.

This method attaches the Windows control to a CWnd object and replaces the WndProc and AfxWndProc functions of the control. The function stores the old WndProc in the location returned by the CWnd::GetSuperWndProcAddrmethod.

翻譯:

該方法動態子類化一個從對話框模板創建的控件,然后將它與一個CWnd對象(記為A)關聯。當一個控件被動態子類化后,Windows消息將會根據CWnd消息地圖路由並首先響應CWnd對象A的消息響應函數。被路由到基類的消息將會被該控件的默認消息處理函數處理。

該方法將一個Windows控件和一個CWnd對象相關聯,並替換了這個控件原來的WndProcAfxWndProc函數。這個函數儲存了原先的WndProc的地址,該地址由CWnd::GetSuperWndProcAddr返回。

 

好!那么,該函數是怎樣替換掉這個控件的原先WndProcAfxWndProc函數的呢?在SubclassDlgItem函數中我們發現它返回的是SubclassWindow(hWndControl)這個函數的執行結果。

繼續查看MSDN

This method dynamically subclasses a window and attach it to this CWnd object. When a window is dynamically subclassed, windows messages will route through the CWnd message map and call message handlers in theCWnd class first. Messages that are passed to the base class will be passed to the default message handler in the window.

 

This method attaches the Windows CE control to a CWnd object and replaces the WndProc and AfxWndProc functions of the window.

 

The function stores a pointer to the old WndProc in the CWnd object.

發現SubclassWindowSubclassDlgItemMSDN說明驚人的相似。可見,SubclassDlgItem函數功能的實現是通過SubclassWindow實現的。那么,對於上面的問題等於沒有任何發現。現在查看SubclassWindow源代碼:

BOOL CWnd::SubclassWindow(HWND hWnd)

{

    if (!Attach(hWnd))

       return FALSE;

 

    // allow any other subclassing to occur

    PreSubclassWindow();

 

    // now hook into the AFX WndProc

1 WNDPROC* lplpfn = GetSuperWndProcAddr();

2 WNDPROC oldWndProc = (WNDPROC)::SetWindowLong(hWnd, GWL_WNDPROC,

      (DWORD)AfxGetAfxWndProc());

    ASSERT(oldWndProc != (WNDPROC)AfxGetAfxWndProc());

 

    if (*lplpfn == NULL)

3    *lplpfn = oldWndProc;   // the first control of that type created

    // 省略無關代碼

    … …

    return TRUE;

}

K這段代碼之前,先回顧一下“MFC消息的起點”

MFC消息起點和流動

    Windows消息怎樣從產生到響應函數收到該消息?

消息的起點

    不管MFC是什么機理,其本質還是對Windows編程進行了整合封裝,僅此而已!對Windows系統來說都是一樣的,它都是不斷地用GetMessage(或其他)從消息隊列中取出消息,然后用DispatchMessage將消息發送到窗口函數中去。在“窗口類的誕生”中知道,MFC將所有的窗口處理函數都注冊成DefWndProc,那么,是不是MFC將所有的消息都發送到DefWndProc中去了呢?答案是“不是”,而是都發送到AfxWndProc函數中去了(您可以回想一下前面我們查看MSDN是提到的AfxWndProc)。那么,MFC為什么要這樣做呢?那就查看MFC代碼,讓它來告訴我們:

BOOL CWnd::CreateEx(……)
{
……
PreCreateWindow(cs);
AfxHookWindowCreate(this);
HWND hWnd = ::CreateWindowEx(……);
……
}
void AFXAPI AfxHookWindowCreate(CWnd *pWnd)
{
……
pThreadState->m_hHookOldCbtFilter = 
::SetWindowsHookEx(WH_CBT,_AfxCbtFilterHook,NULL,::GetCurrentThreadId());
……
}
_AfxCbtFilterHook(int code, WPARAM wParam, LPARAM lParam)
{
……
if(!afxData.bWin31)
{
_AfxStandardSubclass((HWND)wParam);
}
……
}
void AFXAPI _AfxStandardSubclass(HWND hWnd)
{
……
oldWndProc =
 (WNDPROC)SetWindowLong(hWnd,GWL_WNDPROC,(DWORD)AfxGetAfxWndProc());
}
WNDPROC AFXAPI AfxGetAfxWndProc()
{
……
return &AfxWndProc;
}

仔細分析上面的代碼,發現MFC在創建窗口之前,通過AfxHookWindowCreate設置了鈎子(這樣有消息滿足所設置的消息時,系統就發送給你設置的函數,在這里就是_AfxCbtFilterHook),這樣每次創建窗口時,該函數就將窗口函數修改成AfxWndProc。至於為什么要這樣做?那是因為包含3D控件和兼容MFC2.5

   消息的起點都是AfxWndProc,所有消息都被發送到AfxWndProc中,然后在從AfxWndProc流向各自的消息響應函數。AfxWndProc的作用就和車站的作用是一樣的,人們都要先到車站來,然后流向各種的目的地。那么自己的目的地在哪,只有自己才會知道。當然對於消息,也只有它自己才會知道要去哪!而“這種只有自己知道”在MFC中反映為“MFC根據不同類型的消息設置不同的消息路由路徑,然后不同類型的消息走自己的路就OK了(計算機嘛!自己當然不會知道,要用算法嘛!)”。

消息的流動

LRESULT CALLBACK AfxWndProc(…….)
{
……
return AfxCallWndProc(pWnd,hWnd,nMsg,wParam,lParam);
}
LRESULT AFXAPI AfxCallWndProc(……)
{
……
lResult = pWnd->WindowProc(nMsg,wParam,lParam);
……
}
LRESULT CWnd::WindowProc(……)
{
……
if(!OnWndMsg(message,wParam,lParam,&lResult))
      lResult = DefWindowProc(message,wParam,lParam);
……
}
BOOL CWnd::OnWndMsg(……)
//該函數原來太過龐大,為了只表達意思,將其改造如下
{
    ……
    if(message == WM_COMMAND)
         OnCommand(wParam,lParam);
    if(message == WM_NOTIFY)
         OnNotify(wParam,lParam,&lResult);
 
    //每個CWnd類都有它自己的消息地圖
pMessage = GetMessageMap();
//在消息地圖中查找當前消息的消息處理函數
for(; pMessageMap!=NULL; pMessageMap = pMessageMap->pBaseMap)
{
        if((lpEntry=AfxFindMessageEntry(pMessageMap->lpEntries,
                    message,0,0))!=NULL) 
         break;
}
    
(this->*(lpEntry->pnf))(……);//調用消息響應函數
}
AFX_MSGMAP_ENTRY AfxFindMessageEntry(……)
{
……
while(lpEntry->nSign!=AfxSig_end)
{
       if(lpEntry->nMessage==nMsg&&lpEntry->nCode==nCode&&nID>=lpEntry->nID
              &&nID<=lpEntry->nLastID)
       {
             return lpEntry;
       }
       lpEntry++;
}
……
}

仔細分析上面的代碼,發現消息的路由關鍵在於OnCmdMsg函數。OnCmdMsg或者WM_COMMAND消息調用OnCommand(),或者為WM_NOTIFY消息調用OnNotify()。它將沒有被處理的消息都是為窗口消息OnCmdMsg()搜索類的消息映射,以便找到一個能處理窗口消息的處理函數。

    這樣我們就找到了消息的處理函數了。

    回顧完這些知識,我們回到SubclassWindow的實現代碼中。

    我們發現了三個關鍵的語句:

1 WNDPROC* lplpfn = GetSuperWndProcAddr();

2 WNDPROC oldWndProc = (WNDPROC)::SetWindowLong(hWnd, GWL_WNDPROC,

      (DWORD)AfxGetAfxWndProc());

3)                *lplpfn = oldWndProc;  

對這三個語句進行分析:

1)            獲取原窗口處理過程地址,隨便說一下是怎么獲得的:窗口通過CreateEx創建,在調用CreateEx中又調用了CreateWindowEx,調用該函數后將原來的窗口處理函數地址保存在了窗口類的成員函數m_pfnSuper中了。

2)            看到了沒:用SetWindowLong將窗口處理函數改為AfxGetWndProc,根據前面的分析,它會調用AfxWndProc,再通過OnCmdMsg進行消息的路由。結合本例,CSuperEdit有它自己的消息地圖,WM_CHAR消息路由時就會找了CSuperEdit類它自己的OnChar()函數,這樣子類化的目的----封裝自己的消息處理函數----就達到了。

3)            當然CSuperEdit只定義了一部分自己的消息處理函數,大部分還是要由原來的函數(CWnd)完成,所以要保存原來函數地址,這句代碼就完成此功能。

至此,我們關於子類化的來龍去脈就搞清楚了。

補充:我們在用ClassWizard將一個控件與一個自定義的類型關聯起來后,我們並沒有添加像xxx.SubclassDlgItem(xxx,this);這樣的代碼,但是也實現了以上的功能?原因是ClassWizard已經為我們實現了上面的關聯,其原理是一樣的。

 


免責聲明!

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



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