摘要
作為一個(gè)程序員,我們經(jīng)常會(huì)在程序中用到Windows通用控件。比如按鈕控件,進(jìn)度條控件等等。但是有時(shí)我們需要給控件更多的特色,這就需要做控件的子類(lèi)化(subclassing).
子類(lèi)化一個(gè)Windows控件與子類(lèi)化一個(gè)C++類(lèi)不同,子類(lèi)化一個(gè)控件要求你把一個(gè)窗口的一些或所有的消息映射都替換成自己的函數(shù)來(lái)響應(yīng),這樣你就有效的阻止了控件去做系統(tǒng)默認(rèn)的行為,而按自己的想法去做。子類(lèi)化有兩種類(lèi)型:實(shí)例子類(lèi)化(instance subclassing)和全局子類(lèi)化(global subclassing)。實(shí)例子類(lèi)化是子類(lèi)化一個(gè)窗口中的單一實(shí)例,全局子類(lèi)化是把整個(gè)窗口子類(lèi)化為一個(gè)特殊的類(lèi)型。這里我們僅討論單一實(shí)例子類(lèi)化。
記住CWnd
派生類(lèi)對(duì)象與窗口本身(一個(gè)HWND
)的差別是很重要的。你的C++CWnd
-派生類(lèi)對(duì)象包含了一個(gè)指向HWND
的成員函數(shù),并且包含了當(dāng)處理消息時(shí)HWND
消息泵的響應(yīng)函數(shù)(比如WM_PAINT
,WM_MOUSEMOVE
)。但你用一個(gè)C++對(duì)象子類(lèi)化一個(gè)窗口時(shí),你就把HWND
與C++對(duì)象關(guān)聯(lián)起來(lái),并且設(shè)置了處理消息時(shí)把自定義的回調(diào)函數(shù)提供給HWND
消息使用。
子類(lèi)化過(guò)程很簡(jiǎn)單,首先創(chuàng)建一個(gè)類(lèi)映射窗口的所有消息,然后把控件用作為這個(gè)類(lèi)的實(shí)例。例如,下面的例子中我們做一個(gè)按鈕的子類(lèi)化。
新類(lèi)
為了子類(lèi)化一個(gè)控件,我們需要?jiǎng)?chuàng)建一個(gè)新類(lèi),并映射所有我們感興趣的消息。為了簡(jiǎn)便,我們一般都從控件標(biāo)準(zhǔn)類(lèi)中派生自己的新類(lèi),這里與按鈕控件對(duì)應(yīng)的標(biāo)準(zhǔn)類(lèi)為CButton。
下面假定我們要實(shí)現(xiàn)的效果是,當(dāng)鼠標(biāo)懸停在按鈕上方時(shí),按鈕顯示為黃色。首先我們使用ClassWizard創(chuàng)建一個(gè)CButton
的派生類(lèi),叫做CMyButton
。
在MFC框架中從CButton
派生自己的類(lèi)有許多好處,最大的好處是我們不用手工添加任何一行代碼就可以創(chuàng)建了一個(gè)擁有全部默認(rèn)功能的Windows控件。因?yàn)镸FC實(shí)現(xiàn)了所有的默認(rèn)的消息映射,因此我們可以挑選我們感興趣的消息自己處理,而不用去管其他消息。
這里我們要為按鈕設(shè)計(jì)的功能是,鼠標(biāo)懸停時(shí)變?yōu)辄S色。
為了檢查鼠標(biāo)是否懸停于按鈕上,我們?cè)O(shè)置一個(gè)成員變量m_bOverControl ,TRUE表示鼠標(biāo)懸停,然后設(shè)置一個(gè)周期(使用定時(shí)器)跟蹤鼠標(biāo)是否已離開(kāi)控件,這是因?yàn)椋到y(tǒng)并沒(méi)有OnMouseEnter
和OnMouseLeave
函數(shù)供我們調(diào)用,因此我們必須使用OnMouseMove
。如果,在一個(gè)時(shí)間點(diǎn)上,發(fā)現(xiàn)鼠標(biāo)已離開(kāi)按鈕,我們關(guān)閉定時(shí)器并重畫(huà)控件。
使用ClassWizard加入WM_MOUSEMOVE和WM_TIMER的消息映射,響應(yīng)函數(shù)分別是OnMouseMove
和OnTimer
。
ClassWizard將在你的按鈕類(lèi)文件中加入下面的代碼:
BEGIN_MESSAGE_MAP(CMyButton, CButton)
ON_WM_MOUSEMOVE()
ON_WM_TIMER()
END_MESSAGE_MAP()void CMyButton::OnMouseMove(UINT nFlags, CPoint point)
{
CButton::OnMouseMove(nFlags, point);
}void CMyButton::OnTimer(UINT nIDEvent)
{
CButton::OnTimer(nIDEvent);
}
消息映射的入口(即BEGIN_MESSAGE_MAP
) 建立了窗口消息與響應(yīng)函數(shù)的對(duì)應(yīng)關(guān)系。ON_WM_MOUSEMOVE
把WM_MOUSEMOVE消息與OnMouseMove
函數(shù)建立響應(yīng)的關(guān)系,ON_WM_TIMER
m把WM_TIMER消息與OnTimer
函數(shù)建立了響應(yīng)的關(guān)系。這些宏定義在MFC的源文件中,我們不需要去看,只要按照約定來(lái)做就可以了。
假設(shè)我們已經(jīng)聲明了兩個(gè)變量m_bOverControl和m_nTimerID,類(lèi)型分別是BOOL和UINT, 并且在類(lèi)的構(gòu)造函數(shù)中把它們初始化,我們的消息處理應(yīng)使用下面的代碼:
void CMyButton::OnMouseMove(UINT nFlags, CPoint point)
{if (!m_bOverControl)
{
TRACE0("Entering controln");
m_bOverControl = TRUE;
Invalidate();
SetTimer(m_nTimerID,100, NULL);
}
CButton::OnMouseMove(nFlags, point);
}void CMyButton::OnTimer(UINT nIDEvent)
{
CPoint p(GetMessagePos());
ScreenToClient(&p);
CRect rect;
GetClientRect(rect);if (!rect.PtInRect(p))
{
TRACE0("Leaving controln");
m_bOverControl = FALSE;
KillTimer(m_nTimerID);
Invalidate();
}
CButton::OnTimer(nIDEvent);
}
最后我們來(lái)畫(huà)出我們需要的效果,我們不再進(jìn)行消息映射,而是重載CWnd::DrawItem
虛函數(shù)。只有當(dāng)控件設(shè)置owner-drawn風(fēng)格時(shí)這個(gè)函數(shù)才能被調(diào)用,并且這個(gè)函數(shù)沒(méi)有默認(rèn)的實(shí)現(xiàn)代碼,虛函數(shù)的設(shè)計(jì)只為了在派生類(lèi)中進(jìn)行實(shí)現(xiàn)。
使用ClassWizard重載DrawItem
函數(shù),并加入下面的代碼
void CMyButton::DrawItem(LPDRAWITEMSTRUCT lpDrawItemStruct)
{
CDC* pDC = CDC::FromHandle(lpDrawItemStruct->hDC);
CRect rect = lpDrawItemStruct->rcItem;
UINT state = lpDrawItemStruct->itemState;
CString strText;
GetWindowText(strText);if (state & ODS_SELECTED)
pDC->DrawFrameControl(rect, DFC_BUTTON, DFCS_BUTTONPUSH | DFCS_PUSHED);else
pDC->DrawFrameControl(rect, DFC_BUTTON, DFCS_BUTTONPUSH);
rect.DeflateRect( CSize(GetSystemMetrics(SM_CXEDGE), GetSystemMetrics(SM_CYEDGE)));if (m_bOverControl)
pDC->FillSolidRect(rect, RGB(255,255,0));if (!strText.IsEmpty())
{
CSize Extent = pDC->GetTextExtent(strText);
CPoint pt( rect.CenterPoint().x - Extent.cx/2,
rect.CenterPoint().y - Extent.cy/2 );if (state & ODS_SELECTED)
pt.Offset(1,1);int nMode = pDC->SetBkMode(TRANSPARENT);if (state & ODS_DISABLED)
pDC->DrawState(pt, Extent, strText, DSS_DISABLED, TRUE,0, (HBRUSH)NULL);else
pDC->TextOut(pt.x, pt.y, strText);
pDC->SetBkMode(nMode);
}
}
接下來(lái),我們剩下最后一步。為控件設(shè)置owner drawn風(fēng)格。我們可以在對(duì)話(huà)框的資源編輯器中,右鍵單擊按鈕控件,選擇“屬性”,然后在Style中選中owner drawn風(fēng)格。但是有一種更好的方法,使得使用新建類(lèi)子類(lèi)化的按鈕自動(dòng)的設(shè)置owner drawn風(fēng)格。為了完成這個(gè)功能,我們重載最后一個(gè)函數(shù):PreSubclassWindow
。
這個(gè)函數(shù)將在子類(lèi)化窗口時(shí)被調(diào)用,次序是在CWnd::Create
或DDX_Control
之后,這就是說(shuō),無(wú)論是動(dòng)態(tài)的創(chuàng)建窗口實(shí)例還是使用對(duì)話(huà)框模板創(chuàng)建,這個(gè)函數(shù)都將被調(diào)用。PreSubclassWindow
在窗口子類(lèi)化創(chuàng)建后和窗口被顯示前被調(diào)用,換句話(huà)說(shuō),這是我們來(lái)做窗口初始化的一個(gè)最好時(shí)機(jī)。
一個(gè)重點(diǎn)要注意的地方是: 如果你是用對(duì)話(huà)框資源創(chuàng)建一個(gè)控件,那么你要子類(lèi)化的控件將不會(huì)響應(yīng)WM_CREATE消息,所以我們不能在OnCreate
函數(shù)中做初始化的工作,因?yàn)樗⒉皇窃谒械那闆r下都被調(diào)用。
使用ClassWizard重載PreSubclassWindow
函數(shù)并加入下面的代碼
void CMyButton::PreSubclassWindow()
{
CButton::PreSubclassWindow();
ModifyStyle(0, BS_OWNERDRAW);
}
祝賀 - 你的Cbutton
派生類(lèi)已經(jīng)完成。
子類(lèi)化
在創(chuàng)建時(shí)使用DDX子類(lèi)化
在這個(gè)例子中,我們使用對(duì)話(huà)框編輯器在對(duì)話(huà)框中加入了一個(gè)新的按鈕:
然后,使用ClassWizard為你的按鈕控件添加成員變量,變量類(lèi)型選擇我們剛剛建立的類(lèi)CMyButton
ClassWizard g會(huì)在對(duì)話(huà)框的DoDataExchange
函數(shù)中創(chuàng)建一個(gè)DDX_Control
調(diào)用。DDX_Control
啟動(dòng)了子類(lèi)化過(guò)程,使得按鈕控件使用CMyButton
類(lèi)進(jìn)行消息映射,而不是使用通常的CButton
。
使用沒(méi)有在ClassWizard中注冊(cè)的類(lèi)子類(lèi)化窗口
如果你在工程中加入了一個(gè)新的窗口類(lèi),并且希望使用這個(gè)新類(lèi)類(lèi)型子類(lèi)化你的窗口,但是ClassWizard中并沒(méi)有提供新類(lèi)的選項(xiàng),那么你需要重新生成class wizard文件。
先備份以下工程中的.clw文件,然后刪除它。接下來(lái)在Visual Studio中按Ctrl+W。你將看到一個(gè)提示框,要求你加入ClassWizard中包含類(lèi)的文件,確認(rèn)選擇的文件中包含了新類(lèi)的文件(soarlove注:一般情況下,選擇“add all”即可。
現(xiàn)在你的新類(lèi)已經(jīng)可以供選擇。如果不想這樣做,你還有一個(gè)通用的方法,就是在選擇類(lèi)型的時(shí)候使用通用的類(lèi)(比如CButton
),然后在頭文件中手工把通用類(lèi)(CButton)改為你的新類(lèi)(CMyButton
)。
子類(lèi)化一個(gè)存在的窗口
使用DDX固然簡(jiǎn)單,但是不能幫助我們實(shí)現(xiàn)一個(gè)已存在窗口的子類(lèi)化。比如你想在combobox中子類(lèi)化一個(gè)Edit控件,那么在你子類(lèi)化Edit控件之前,你需要先創(chuàng)建combobox控件。
這種情況下,我們使用SubclassDlgItem
或者SubclassWindow
函數(shù)。這兩個(gè)函數(shù)允許你動(dòng)態(tài)的子類(lèi)化一個(gè)窗口,換句話(huà)說(shuō),把一個(gè)新的窗口實(shí)例與已經(jīng)存在的窗口建立關(guān)聯(lián)。
比如,假設(shè)有一個(gè)對(duì)話(huà)框中包含了一個(gè)按鈕IDIDC_BUTTON1
。這個(gè)按鈕已經(jīng)被創(chuàng)建,我們想用一個(gè)CMyButton
的實(shí)例來(lái)與之關(guān)聯(lián),以使得按鈕符合我們需要的行為。
為了做到這些,我們需要有一個(gè)新類(lèi)型的實(shí)例,最后的方法是在對(duì)話(huà)框或視的頭文件中加入成員函數(shù)。
CMyButton m_btnMyButton;
然后在對(duì)話(huà)框的OnInitDialog
(或任何適當(dāng)?shù)牡胤? 中調(diào)用:
m_btnMyButton.SubclassDlgItem(IDC_BUTTON1,this);
假設(shè)你已經(jīng)有了一個(gè)窗口的指針,或者你工作在一個(gè)CView
或其他CWnd
派生類(lèi)中里面的控件被動(dòng)態(tài)的創(chuàng)建,或者你不想使用SubclassDlgItem
函數(shù),那么你可以使用下面的方法:
CWnd* pWnd = GetDlgItem(IDC_BUTTON1);
ASSERT( pWnd && pWnd->GetSafeHwnd() );
m_btnMyButton.SubclassWindow(pWnd->GetSafeHwnd());
畫(huà)按鈕是非常簡(jiǎn)單的,不需要考慮按鈕的風(fēng)格(比如flat風(fēng)格),也不需要考慮適應(yīng)文字,僅僅需要考慮你畫(huà)的范圍。如果你編譯運(yùn)行提供的演示代碼,那么你將看到,當(dāng)鼠標(biāo)懸停于按鈕上方時(shí),按鈕變?yōu)辄S色。
注意,實(shí)際上我們只重載了畫(huà)的函數(shù),并截取了鼠標(biāo)移動(dòng)的函數(shù)。其余的功能都還是使默認(rèn)響應(yīng)的。
結(jié)論
子類(lèi)化并不難 - 你只要認(rèn)真的選擇你要子類(lèi)化的類(lèi)并且知道你要映射那些消息。要熟悉你要子類(lèi)化的類(lèi),了解提供的消息和類(lèi)中的虛函數(shù)。