窗口子類化的作用
窗口子類化技術(shù)最大的特點(diǎn)就是能夠截取Windows的消息。一旦用戶自定義的窗口函數(shù)截取了傳向原窗口函數(shù)的消息,就可以對(duì)被截取的消息進(jìn)行如下處理:
將其傳給原來的窗口函數(shù)。這是對(duì)大多數(shù)消息應(yīng)該采取的措施,因?yàn)樽宇愅ǔV粚?duì)原來的窗口特性作少量的改動(dòng)
截取該消息,阻止其向原窗口函數(shù)發(fā)送。
修改該消息,修改完畢以后再向原窗口函數(shù)發(fā)送
MFC編程中的窗口子類化
MFC窗口實(shí)際上已經(jīng)是被子類化的窗口。所有的MFC窗口共享同一個(gè)窗口函數(shù),由這個(gè)窗口函數(shù)根據(jù)窗口句柄,查找這個(gè)窗口對(duì)應(yīng)的CWnd派生類實(shí)例,再通過消息映射這個(gè)窗口類的消息處理函數(shù)。鑒于以上原因,在MFC中要子類化一個(gè)窗口就比較容易了,因?yàn)槟愕娜蝿?wù)只是編寫一個(gè)新的MFC窗口類而不需要寫一個(gè)窗口函數(shù)。
假如我們現(xiàn)在有一個(gè)對(duì)話框,里面有一個(gè)編輯控件,我們只希望在該控件中接受非數(shù)字字符輸入,我們可以攔截WM_CHAR消息,在它的處理函數(shù)中忽略任何數(shù)字的輸入。具體實(shí)現(xiàn)步驟如下。
VC中窗口子類化的應(yīng)用舉例
MFC為廣大編程者提供了很多功能豐富的窗口類,如果能在這些通用窗口類的基礎(chǔ)上進(jìn)行子類化的話,將會(huì)給編程者帶來很多便利。下面舉一個(gè)例子來說明MFC編程中的子類化是多么的簡(jiǎn)單易行。該例完成上面提到的在編輯控件只接受非數(shù)字字符輸入的功能。實(shí)現(xiàn)這個(gè)子類化的基本步驟和相關(guān)代碼如下:
(1)利用AppWziard創(chuàng)建一個(gè)基于對(duì)話框的程序SubClassing。
(2)對(duì)MFC提供的標(biāo)準(zhǔn)的對(duì)話框中的控件進(jìn)行修改,刪除MFC提供的靜態(tài)文本控件,添加自己的一個(gè)編輯控件,設(shè)置新控件的ID為IDC_EDIT。合理布置對(duì)話框上各控件的位置,使程序界面布局合理、美觀。
(3)用ClassWizard從CEdit類派生一個(gè)新的窗口類,新窗口的窗口類叫CNoNumEdit。截取CNoNumEdit類的WM_CHAR消息,在OnChar函中完成忽略任何數(shù)字的輸入的處理。實(shí)現(xiàn)代碼如下:
void CNoNumEdit::OnChar(UINT nChar, UINT nRepCnt, UINT nFlags)
{
TCHAR ch=nChar;
if(ch>=_T('0')&&ch<=_T('9'))
{
AfxMessageBox(("請(qǐng)不要輸入數(shù)字!"),MB_OK);
//當(dāng)輸入數(shù)字字符時(shí)將被忽略,并顯示警告信息
return;
}
CEdit::OnChar(nChar, nRepCnt, nFlags);//輸入為非數(shù)字字符時(shí)調(diào)用原處理函數(shù)
}
(4)在對(duì)話框窗口類CSubClassingDlg的定義中添加變量CNoNumEdit ed。在CSubClassingDlg::OnInitDialog()函數(shù)中調(diào)用CWnd類的成員函數(shù)SubClassWindow進(jìn)行子類化。
代碼:
if (ed.GetSafeHwnd() == NULL)
ed.SubclassWindow(GetDlgItem(IDC_EDIT1)->GetSafeHwnd());
(5) 在對(duì)話框窗口類CsubClassing的OnDestroy中調(diào)用UnSubClassWindow()執(zhí)行窗口類的反子類化。
代碼:
if (ed.GetSafeHwnd() != NULL)
ed.UnsubclassWindow();
現(xiàn)在可以編譯執(zhí)行這個(gè)程序了,當(dāng)用戶輸入數(shù)字字符時(shí)將會(huì)忽略該輸入,并顯示警告信息。
在Windows編程中,適當(dāng)使用窗口子類化技術(shù),可以很方便地達(dá)到改變一個(gè)窗口的特性的目的。當(dāng)然子類化也存在其局限性。實(shí)際上,子類化的概念是針對(duì)一個(gè)已經(jīng)創(chuàng)建的窗口來談的,所以修改窗口函數(shù)是在窗口創(chuàng)建之后進(jìn)行的,在窗口創(chuàng)建期間的消息無法捕獲,也就無法處理。另外有些窗口的特性與窗口類本身的屬性有關(guān)。比如如果一個(gè)窗口類沒有CS_DBLCLKS屬性的話,那么要想通過子類化這些窗口達(dá)到處理WM_LBUTTONDBLCLK消息的目的是無法實(shí)現(xiàn)的。對(duì)于子類化的以上局限性,可以通過超類化(SuperClassing)技術(shù)消除。
如果在創(chuàng)建對(duì)話框時(shí)已經(jīng)使用MFC的數(shù)據(jù)交換技術(shù)DDX將IDC_EDIT與變量ed進(jìn)行了綁定,則其效果同子類化一樣,子類化的要點(diǎn)在于一個(gè)動(dòng)態(tài)綁定。
MFC向?qū)傻膶?duì)話框?yàn)槟B(tài)對(duì)話框,當(dāng)我們?cè)谫Y源編輯器中向?qū)υ捒蛲献б粋€(gè)按鈕IDC_BTN時(shí),其布局信息將同步反映在DlgDemo.rc資源腳本文件中。
// DlgDemo.rc
IDD_MY_DIALOG DIALOGEX 0, 0, 320, 201
STYLE DS_MODALFRAME | WS_POPUP | WS_VISIBLE | WS_CAPTION | WS_SYSMENU
EXSTYLE WS_EX_APPWINDOW
CAPTION "DlgDemo"
FONT 9, "宋體"
BEGIN
DEFPUSHBUTTON "確定",IDOK,260,7,50,14
PUSHBUTTON "取消",IDCANCEL,260,23,50,14
PUSHBUTTON "MyBtn",IDC_BTN,141,79,50,14
END
CDialog的構(gòu)造函數(shù)的參數(shù)一nIDTemplate指定了對(duì)話框模板的ID,即DlgDemo.rc中的IDD_MY_DIALOG。
CDialog::CDialog(UINT nIDTemplate, CWnd* pParentWnd)
{
// ……
m_pParentWnd = pParentWnd;
m_lpszTemplateName = MAKEINTRESOURCE(nIDTemplate);
// ……
}
模態(tài)對(duì)話框調(diào)用CDialog::DoModal()創(chuàng)建并顯示對(duì)話框,CDialog::DoModal()根據(jù)對(duì)話框模板名稱m_lpszTemplateName進(jìn)行FindResource、LoadResource加載模板資源。
CDialog::DoModal()調(diào)用CDialog::CreateDlgIndirect,最終調(diào)用::CreateDialogIndirectParam完成非模態(tài)對(duì)話框的創(chuàng)建。::CreateDialogIndirectParam參數(shù)二LPCDLGTEMPLATE lpTemplate即DlgDemo.rc中的IDD_MY_DIALOG模板資源,該API將根據(jù)腳本描述創(chuàng)建對(duì)話框及其上的子控件(底層調(diào)用CreateWindowEx,傳入風(fēng)格、標(biāo)題和布局大小等參數(shù))。
對(duì)于外部而言,可見的只是一些子控件的ID,而沒有具體的子類(例如按鈕IDC_BTNàCButton)。實(shí)際上,對(duì)話框內(nèi)部維護(hù)了一個(gè)“控件IDà控件HWND”的映射,這樣我們就可以通過::GetDlgItem(hDlg, nIDDlgItem)獲取子控件的窗口句柄,進(jìn)行相關(guān)Get/Set操作。
下面在點(diǎn)擊按鈕IDC_BTN時(shí),修改其標(biāo)題。
ON_BN_CLICKED(IDC_BTN, OnBtn)
void CMyDlg::OnBtn()
{
// TODO: Add your control notification handler code here
GetDlgItem(IDC_BTN)->SetWindowText("FXM"); // change button caption
}
CWnd* GetDlgItem(int nID)調(diào)用CWnd::FromHandle(::GetDlgItem(m_hWnd, nID)),FromHandle創(chuàng)建一個(gè)臨時(shí)的CWnd(子類)對(duì)象,并把Windows對(duì)象(HWND)映射到臨時(shí)的MFC對(duì)象上,然后返回臨時(shí)MFC對(duì)象。MFC框架在線程的Idle處理中刪除臨時(shí)對(duì)象。
利用向?qū)?/span>按鈕IDC_BTN添加CButton類型的控件變量,內(nèi)部調(diào)用了Attach函數(shù)建立了控件變量(CButton)與窗口(HWND)之間的永久映射(SetPermanent)。在整個(gè)對(duì)話框的生存周期中,可以通過這個(gè)控件變量實(shí)現(xiàn)對(duì)窗口的訪問。至此,我們對(duì)按鈕IDC_BTN的操作依然局限在相關(guān)屬性的Get/Set訪問上,而其后續(xù)狀態(tài)行為依然故我地輪回著CButton的DefWindowProc。
怎樣實(shí)現(xiàn)XP風(fēng)格按鈕、釘子按鈕甚至任意形狀按鈕呢?這里涉及到一個(gè)重要的概念——窗口子類化。
所謂窗口子類化,實(shí)際上就是改變窗口內(nèi)存塊中的有關(guān)參數(shù)。由于這種修改只涉及到一個(gè)窗口的內(nèi)存塊,因此它不會(huì)影響到屬于同一窗口類的其它窗口的功能和表現(xiàn)(IDàHWNDàCWnd)。窗口子類化中最常見的是修改窗口內(nèi)存塊中的窗口函數(shù)地址(lpfnWndProc),使其指向一個(gè)新的窗口函數(shù),從而改變?cè)翱诤瘮?shù)的處理方法,做出特定功能適應(yīng)。
在實(shí)際開發(fā)中,有些情況標(biāo)準(zhǔn)控件的標(biāo)準(zhǔn)過程是無能為力的。比如:在我們的應(yīng)用中要求一個(gè)EDIT控件接收老師對(duì)學(xué)生的評(píng)價(jià),評(píng)價(jià)分三個(gè)等級(jí)A、B、C(不要對(duì)我說你想用ComboBox實(shí)現(xiàn)),這就要求在EDIT中禁止對(duì)其它字母、數(shù)字的輸入操作,怎么辦?EDIT控件本身沒有提供這種機(jī)制,采用子類化可以很好的解決這類問題。
我們知道,每一個(gè)Windows窗口(這里指EDIT)都有一個(gè)窗口處理函數(shù)負(fù)責(zé)對(duì)消息的處理,子類化通常就是用我們自己的消息處理函數(shù)來替代窗口原有的、標(biāo)準(zhǔn)的處理函數(shù)。當(dāng)然我們自己的窗口處理過程只是關(guān)心那些特定的消息(在這里是WM_CHAR),而其它消息將發(fā)給原來的窗口函數(shù)做默認(rèn)處理。在SDK中的實(shí)現(xiàn)方法是調(diào)用函數(shù)SetWindowLong,其原型如下:
WNDPROC oldWndProc = (WNDPROC)::SetWindowLong(hWnd, GWL_WNDPROC, (DWORD)AfxGetAfxWndProc());
其中AfxGetAfxWndProc()是我們自己的窗口處理函數(shù),在其中處理我們感興趣的消息后,然后調(diào)用原窗口函數(shù)oldWndProc來對(duì)其它消息做標(biāo)準(zhǔn)處理。
我們先來梳理一下一個(gè)窗口創(chuàng)建過程中的附加和子類化過程。
CWnd::CreateàCWnd::CreateExàAfxHookWindowCreate(this)à_AfxCbtFilterHook。在鉤子函數(shù)_AfxCbtFilterHook中,將已創(chuàng)建的窗口(HWND)附加到當(dāng)前正在初始化的CWnd(CEdit)對(duì)象(_AFX_THREAD_STATE:: m_hWndInit)上。然后再調(diào)用::SetWindowLong改變窗口的過程為AfxWndProc。窗口函數(shù)AfxWndProc從AFX_MODULE_THREAD_STATE::m_pmapHWND中查詢hWnd對(duì)應(yīng)的CWnd對(duì)象,AfxCallWndProc將消息委托給具體窗口對(duì)象的WindowProc函數(shù)處理。
利用MFC實(shí)現(xiàn)上面提到的EDIT控件過濾輸入要求,只能輸入A、B、C中的一個(gè)字母。
從CEdit派生一個(gè)自己的類CSuperEdit,在其中處理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類中加入一個(gè)數(shù)據(jù)成員CSuperEdit m_edit,在CProg1Dlg::OnInitDialog()中加入以下兩行代碼:
m_edit.SubclassDlgItem(IDC_EDIT1, this);
m_edit.SetWindowText("<請(qǐng)輸入A、B、C>");
處理EDIT向DIALOG發(fā)送的通知消息EN_SETFOCUS:
ON_EN_SETFOCUS(IDC_EDIT1, OnSetfocusEdit1)
void CProg1Dlg::OnSetfocusEdit1()
{
// TODO: Add your control notification handler code here
m_edit.SetWindowText("");
m_edit.SetFocus();
}
OK,一切搞定!和SDK的子類化方法比起來,這是多么的容易!
我們看看MFC背著我們到底做了什么!這里主要解決兩個(gè)容易讓初學(xué)者比較疑惑的問題:
1、m_edit只是我們定義的一個(gè)C++類對(duì)象,為什么通過它調(diào)用其成員函數(shù)SetWindowText便可以控制我們程序中資源編號(hào)為IDC_EDIT1的控件?
2、CSuperEdit類為什么可以處理WM_CHAR消息?
大家都知道,控制Windows窗口、控件、資源……都是通過它們的句柄來實(shí)現(xiàn),如HANDLE、HWND、HDC都是句柄,它表現(xiàn)為一個(gè)32位長(zhǎng)整形數(shù)據(jù),存放于Windows中的特定區(qū)域,可以把它理解為指向我們想控制的窗口、控件、資源的索引,有了它,我們就可以控制想要控制的對(duì)象。
這里你應(yīng)該聯(lián)想到為什么大多數(shù)窗口API函數(shù)都有一個(gè)參數(shù)HWND hwnd了吧!
// WINUSER.H
BOOL
SetWindowTextW(
HWND hWnd, // handle to window or control
LPCWSTR lpString // title or text
);
變量m_edit要想控制IDC_EDIT1,必通過EDIT控件窗口的句柄,但這又是如何實(shí)現(xiàn)的呢?您可能注意到了m_edit.SubclassDlgItem(IDC_EDIT1, this);一句,對(duì)了,這就是關(guān)鍵所在!
在此處F9設(shè)置斷點(diǎn),F5之后,程序到達(dá)此處,F11跟入SubclassDlgItem函數(shù):
// WINCORE.CPP
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
}
代碼開始時(shí)對(duì)傳入的父窗口做些檢查,然后就是
HWND hWndControl = ::GetDlgItem(pParent->m_hWnd, nID);
if (hWndControl != NULL)
return SubclassWindow(hWndControl);
這是關(guān)鍵的代碼,先用hWndControl得到我們IDC_EDIT1控件的句柄,然后調(diào)用SubclassWindow函數(shù),這個(gè)函數(shù)是實(shí)現(xiàn)的關(guān)鍵,我們來看一下它做了什么:
// WINCORE.CPP
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;
}
函數(shù)Attach建立窗口對(duì)象CWnd與窗口句柄HWND的關(guān)聯(lián)(映射),其內(nèi)部實(shí)現(xiàn)如下:
// WINCORE.CPP
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的數(shù)據(jù)成員m_hWnd(別忘了我們的CSuperEdit類是派生于CEdit的),即建立hWndNew(IDC_EDIT1)與m_edit(this)對(duì)象之間的關(guān)聯(lián)。大家可能現(xiàn)在已經(jīng)隱約的明白了些什么,不錯(cuò),在m_edit.SetWindowText("<請(qǐng)輸入A、B、C>");中正是通過這個(gè)數(shù)據(jù)成員m_hWnd實(shí)現(xiàn)對(duì)IDC_EDIT1控制的:
// WINOCC.CPP
void CWnd::SetWindowText(LPCTSTR lpszString)
{
ASSERT(::IsWindow(m_hWnd));
if (m_pCtrlSite == NULL)
::SetWindowText(m_hWnd, lpszString);
else
m_pCtrlSite->SetWindowText(lpszString);
}
其它CEdit類的函數(shù)也都是圍繞“API函數(shù)+HWND參數(shù)(m_hWnd)”進(jìn)行包裝的。常用的DDX_Control方法說到底也是調(diào)用SubclassWindow: OnInitDialogàUpdateDataàDoDataExchangeàDDX_ControlàSubclassWindow。
故一般在派生了CSuperEdit類后,可利用向?qū)?/span>CProg1Dlg添加CSuperEdit類型控件變量,向?qū)⒃?/span>void CProg1Dlg::DoDataExchange(CDataExchange* pDX)中自動(dòng)添加DDX_Control(pDX, IDC_EDIT1, m_edit); 在進(jìn)行子類化時(shí),SubclassDlgItem和DDX_Control兩種方式擇其一。
怎么樣?第一個(gè)問題的來龍去脈搞明白了吧?
現(xiàn)在看看第二個(gè)問題:CSuperEdit類為什么可以處理WM_CHAR消息?
可能有的朋友現(xiàn)在疑惑,雖然通過句柄實(shí)現(xiàn)了m_edit對(duì)IDC_EDIT的控制,但發(fā)送給它的消息照樣跑到EDIT的標(biāo)準(zhǔn)處理函數(shù)中,對(duì)WM_CHAR的處理是如何實(shí)現(xiàn)的呢?
如果消息照樣跑到EDIT的標(biāo)準(zhǔn)處理函數(shù)中,那當(dāng)然是不能處理了!不知您有沒有看到在上面的SubclassWindow函數(shù)中有這么一小段我加了重點(diǎn)標(biāo)示:
// 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中子類化機(jī)制聯(lián)系起來,明白了吧?MFC在這里神不知鬼不覺的搞起偷天換日的勾當(dāng)!
這個(gè)AfxGetAfxWndProc()函數(shù)是這樣的:
// WINCORE.CPP
// always indirectly accessed via AfxGetAfxWndProc
WNDPROC AFXAPI AfxGetAfxWndProc()
{
#ifdef _AFXDLL
return AfxGetModuleState()->m_pfnAfxWndProc;
#else
return &AfxWndProc;
#endif
}
讀過侯捷先生《深入淺出MFC》的朋友不知還是否記得MFC的命令路由機(jī)制正是以這個(gè)函數(shù)為起點(diǎn)的!
我們可以對(duì)對(duì)話框CProg1Dlg進(jìn)行WM_CREATE的消息響應(yīng),但在CProg1Dlg::OnCreate函數(shù)中對(duì)子控件所作的任何操作都會(huì)導(dǎo)致內(nèi)存非法訪問。OnCreate函數(shù)成功返回后,創(chuàng)建主對(duì)話框的CreateWindowEx接著返回,這時(shí)::CreateDialogIndirectParam過程中才開始創(chuàng)建對(duì)話框子控件窗口。等所有子控件創(chuàng)建完畢后,::CreateDialogIndirectParam發(fā)出WM_INITDIALOG消息,調(diào)用對(duì)話框的OnInitDialog的函數(shù)。因此,在OnInitDialog之后子類化,只能處理一些創(chuàng)建之后的狀態(tài)行為。通過子類化可對(duì)既有窗口特定消息進(jìn)行行為和狀態(tài)的自繪制處理。
當(dāng)程序收到發(fā)給Edit的WM_CHAR消息時(shí),本應(yīng)調(diào)用EDIT標(biāo)準(zhǔn)窗口處理函數(shù),現(xiàn)在被改為調(diào)用LRESULT CALLBACK AfxWndProc(HWND, UINT, WPARAM, LPARAM);了,然后WM_CHAR消息進(jìn)行一系列的流竄,最終成功到達(dá)我們的處理函數(shù)void CSuperEdit::OnChar(UINT nChar, UINT nRepCnt, UINT nFlags);關(guān)于消息的分流派發(fā)請(qǐng)參考《深入淺出MFC》第9章《消息映射與命令繞行》。
終于,我們走出了FMC子類化的迷宮。
聯(lián)系客服