走出MFC子類化的迷宮
KEY WORDS:子類化 SUBCLASSWINDOW MFC消息機制
許多Windows程序員都是跳過SDK直接進行RAD開發工具[或VC,我想VC應不屬于RAD]的學習,有些人可能對子類化機制比較陌生。
我們先看看什么是Windows的子類化。Windows給我們或是說給它自己定義了許多豐富的通用控件,如:Edit、ComboBox 、ListBox……等,這些控件功能豐富,能為我們開發工作帶來極大方面,試想:我們單單是自己實現一個EDIT控件是多么的艱難!但是,在實際開發中還是有些情況這些標準控件也無能為力,比如:在我們的應用中要求一個EDIT得到老師對學生的評價A、B、C[不要對我說你想用ComboBox實現J],這時,要求在Edit中禁止對其它字母、數字的輸入操作,怎么辦?EDIT控件本身沒有提供這種機制,我們就可以采用子類化很好的解決這類問題。
我們知道,每一個Windows窗口[這里是EDIT]都有一個窗口處理函數負責對消息處理,子類化的辦法就是用我們自己的消息處理函數來替代窗口原有的、標準的處理函數。當然我們自己的窗口處理函數只是關心那些特定的消息[在這里當然是WM_CHAR了],而其它消息,再發給原來的窗口函數處理。在SDK中的實現方法是調用函數SetWindowLong :
WNDPROC * oldWndProc = (WNDPROC)SetWindowLong(hWnd, GWL_WNDPROC,(DWORD)AfxGetAfxWndProc());
其中AfxGetAfxWndProc()是我們自己的窗口處理函數,在其中處理過我們感興趣的消息后就可能通過返回的原窗口處理函數指針oldWndProc來把其它消息按標準方法處理掉,具體做法請查閱相關資料。
但到了MFC“時代”,一切都被包裝起來了,原來的窗口類注冊、窗口函數都不見了[或是說隱身了],我想對于那些“刨根問底”的程序員有興趣了解在MFC中的子類化機制,本人就自己做的一點“探索”作出總結,希望能給大家點啟示。
我們先用MFC實現我上面提到的要求:一個只能輸入A,B,C的EDIT控件。
啟動時界面如下:

輸入時就只能輸入A、B、C,并且只允許輸入一個字母。

實現方法:
先派生一個自己的類CsuperEdit,Ctrl + W后,在其中處理WM_CHAR,然后再編輯這個消息處理函數:
void CSuperEdit::OnChar(UINT nChar, UINT nRepCnt, UINT nFlags)
{
// TODO: Add your message handler code here and/or call default
TCHAR ch[20];
GetWindowText(ch,20);
if (strlen(ch) == 1 && (nChar <= 'C' && nChar >= 'A'))
return;
if (nChar != 'A'
&& nChar != 'B'
&& nChar != 'C'
)
return;
CEdit::OnChar(nChar, nRepCnt, nFlags);
}
然后再給我們Cprog1Dlg類中加入一個數據成員CsuperEdit m_edit,在CProg1Dlg::OnInitDialog()中加入:
m_edit.SubclassDlgItem(IDC_EDIT1,this);
m_edit.SetWindowText("<請輸入A、B、C>");
并處理EDIT向DIALOG發送的通知消息:EN_SETFOCUS:
void CProg1Dlg::OnSetfocusEdit1()
{
// TODO: Add your control notification handler code here
m_edit.SetWindowText("");
m_edit.SetFocus();
}
OK,一切搞定!和SDK的子類化方法比起來,這是多么的容易!
我們看看MFC背著我們到底做了什么!這里主要解決兩個容易讓初學者比較疑惑的問題:
1、 m_edit只是我們定義的一個C++類對象,為什么通過它調用其成員函數SetWindowText便可以控制我們程序中資源編號為:IDC_EDIT1的控件?
2、 CSuperEdit類為什么可以處理WM_CHAR消息?
大家都知道,控制Windows窗口、控件、資源……都是通過它們的句柄來實現,如
HHANDLE、HWND、HDC都是句柄,它表現為一個32位長整形數據,存放于Windows中的特定區域,我們可以把它理解為指向我們想控制的窗口、控件、資源的索引,有了它,我們就可以控制我們想要控制的對象。
這里你可以想到為什么多數API函數都有一個參數HWND hwnd了吧!
BOOL SetWindowText(
HWND hWnd, // handle to window or control
LPCTSTR lpString // title or text
);
我們的C++變量m_edit要想控制IDC_EDIT1,也要通過它的句柄,但這又是如何實現的呢?您可能注意到了m_edit.SubclassDlgItem(IDC_EDIT1,this);一句,對了,這就是關鍵所在!
在此處F9設置斷點,F5之后,程序到達此處,F11跟入SubclassDlgItem函數:
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);
#ifndef _AFX_NO_OCC_SUPPORT
if (pParent->m_pCtrlCont != NULL)
{
// normal dialog control not found
COleControlSite* pSite = pParent->m_pCtrlCont->FindItem(nID);
if (pSite != NULL)
{
ASSERT(pSite->m_hWnd != NULL);
VERIFY(SubclassWindow(pSite->m_hWnd));
#ifndef _AFX_NO_OCC_SUPPORT
// If the control has reparented itself (e.g., invisible control),
// make sure that the CWnd gets properly wired to its control site.
if (pParent->m_hWnd != ::GetParent(pSite->m_hWnd))
AttachControlSite(pParent);
#endif //!_AFX_NO_OCC_SUPPORT
return TRUE;
}
}
#endif
return FALSE; // control not found
}
代碼開始時對傳入的父窗口做些檢查,然后就是
HWND hWndControl = ::GetDlgItem(pParent->m_hWnd, nID);
if (hWndControl != NULL)
return SubclassWindow(hWndControl);
這是關鍵的代碼,先用hWndControl得到我們IDC_EDIT1控件的句柄,然后調用
SubclassWindow函數,這個函數是實現的關鍵,我們來看一下它做了什么:
BOOL CWnd::SubclassWindow(HWND hWnd)
{
if (!Attach(hWnd))
return FALSE;
// allow any other subclassing to occur
PreSubclassWindow();
// now hook into the AFX WndProc
WNDPROC* lplpfn = GetSuperWndProcAddr();
WNDPROC oldWndProc = (WNDPROC)::SetWindowLong(hWnd, GWL_WNDPROC, (DWORD)AfxGetAfxWndProc());
ASSERT(oldWndProc != (WNDPROC)AfxGetAfxWndProc());
if (*lplpfn == NULL)
*lplpfn = oldWndProc; // the first control of that type created
#ifdef _DEBUG
else if (*lplpfn != oldWndProc)
{
TRACE0("Error: Trying to use SubclassWindow with incorrect CWnd\n");
TRACE0("\tderived class.\n");
TRACE3("\thWnd = $%04X (nIDC=$%04X) is not a %hs.\n", (UINT)hWnd,
_AfxGetDlgCtrlID(hWnd), GetRuntimeClass()->m_lpszClassName);
ASSERT(FALSE);
// undo the subclassing if continuing after assert
::SetWindowLong(hWnd, GWL_WNDPROC, (DWORD)oldWndProc);
}
#endif
return TRUE;
}
函數Attach內部如下:
BOOL CWnd::Attach(HWND hWndNew)
{
ASSERT(m_hWnd == NULL); // only attach once, detach on destroy
ASSERT(FromHandlePermanent(hWndNew) == NULL);
// must not already be in permanent map
if (hWndNew == NULL)
return FALSE;
CHandleMap* pMap = afxMapHWND(TRUE); // create map if not exist
ASSERT(pMap != NULL);
pMap->SetPermanent(m_hWnd = hWndNew, this);
#ifndef _AFX_NO_OCC_SUPPORT
AttachControlSite(pMap);
#endif
return TRUE;
}
這里要說明的是pMap->SetPermanent(m_hWnd = hWndNew, this);一句,它把我們IDC_EDIT1的句柄賦值給類CsuperEdit的數據成員m_hWnd [別忘了我們的CsuperEdit類是派生于Cedit的],大家可能現在已經隱約的明白了些什么,不錯,在m_edit.SetWindowText("<請輸入A、B、C>");中正是通過這個數據成員m_hWnd實現對IDC_EDIT1控制的:
void CWnd::SetWindowText(LPCTSTR lpszString)
{
ASSERT(::IsWindow(m_hWnd));
if (m_pCtrlSite == NULL)
::SetWindowText(m_hWnd, lpszString);
else
m_pCtrlSite->SetWindowText(lpszString);
}
其它CEdit類的函數也都是圍繞 “m_hWnd + API函數” 進行包裝的。
而我們常用的DDX_Control方法說到底也是調用SubclassWindow。
怎么樣?第一個問題的來龍去脈搞明白了吧?
現在看看第二個問題:CSuperEdit類為什么可以處理WM_CHAR消息?
可能有的朋友現在疑惑,雖然通過句柄實現了m_edit對IDC_EDIT的控制,但發送給它的消息照樣跑到EDIT的標準處理函數中,對WM_CHAR的處理是如何實現的呢?
如果消息照樣跑到EDIT的標準處理函數中,那當然是不能處理了!不知您有沒有看到在上面的SubclassWindow函數中有這么一小段我加了重點標示:
// now hook into the AFX WndProc
WNDPROC* lplpfn = GetSuperWndProcAddr();
WNDPROC oldWndProc = (WNDPROC)::SetWindowLong(hWnd, GWL_WNDPROC, (DWORD)AfxGetAfxWndProc());
ASSERT(oldWndProc != (WNDPROC)AfxGetAfxWndProc());
if (*lplpfn == NULL)
*lplpfn = oldWndProc; // the first control of that type created
再和我們開始講到的SDK中子類化機制聯系起來,明白了吧?MFC在這里神不知鬼不覺的搞起偷天換日的勾當!
這個AfxGetAfxWndProc()函數是這樣的:
WNDPROC AFXAPI AfxGetAfxWndProc()
{
#ifdef _AFXDLL
return AfxGetModuleState()->m_pfnAfxWndProc;
#else
return &AfxWndProc;
#endif
}
讀過侯捷先生《深入淺出MFC》的朋友不知還是否記得MFC的命令路由機制正是以這個函數為起點的!
這樣當程序收到發給Edit的WM_CHAR時,本應調用EDIT標準窗口處理函數,現在被改為調用LRESULT CALLBACK AfxWndProc(HWND hWnd, UINT nMsg, WPARAM wParam, LPARAM lParam)了,然后WM_CHAR消息進行一系列的流竄,最終成功到達我們的處理函數CSuperEdit::OnChar(UINT nChar, UINT nRepCnt, UINT nFlags),至于是如何流竄的、怎么到達的請參考《深入淺出MFC》[如果您的書是繁體電子版,請從566頁讀起]。
終于,我們走出了FMC子類化的迷宮。