深刻理解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了。c++

 

原理探討web

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

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

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

{函數

     ASSERT(pParent != NULL);this

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

     // 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已經爲咱們實現了上面的關聯,其原理是同樣的。

相關文章
相關標籤/搜索