摘要
作為一個程序員,我們經(jīng)常會在程序中用到Windows通用控件。比如按鈕控件,進(jìn)度條控件等等。但是有時我們需要給控件更多的特色,這就需要做控件的子類化(subclassing).
子類化一個Windows控件與子類化一個C++類不同,子類化一個控件要求你把一個窗口的一些或所有的消息映射都替換成自己的函數(shù)來響應(yīng),這樣你就有效的阻止了控件去做系統(tǒng)默認(rèn)的行為,而按自己的想法去做。子類化有兩種類型:實(shí)例子類化(instance subclassing)和全局子類化(global subclassing)。實(shí)例子類化是子類化一個窗口中的單一實(shí)例,全局子類化是把整個窗口子類化為一個特殊的類型。這里我們僅討論單一實(shí)例子類化。
記住CWnd
派生類對象與窗口本身(一個HWND
)的差別是很重要的。你的C++CWnd
-派生類對象包含了一個指向HWND
的成員函數(shù),并且包含了當(dāng)處理消息時HWND
消息泵的響應(yīng)函數(shù)(比如WM_PAINT
,WM_MOUSEMOVE
)。但你用一個C++對象子類化一個窗口時,你就把HWND
與C++對象關(guān)聯(lián)起來,并且設(shè)置了處理消息時把自定義的回調(diào)函數(shù)提供給HWND
消息使用。
子類化過程很簡單,首先創(chuàng)建一個類映射窗口的所有消息,然后把控件用作為這個類的實(shí)例。例如,下面的例子中我們做一個按鈕的子類化。
新類
為了子類化一個控件,我們需要創(chuàng)建一個新類,并映射所有我們感興趣的消息。為了簡便,我們一般都從控件標(biāo)準(zhǔn)類中派生自己的新類,這里與按鈕控件對應(yīng)的標(biāo)準(zhǔn)類為CButton。
下面假定我們要實(shí)現(xiàn)的效果是,當(dāng)鼠標(biāo)懸停在按鈕上方時,按鈕顯示為黃色。首先我們使用ClassWizard創(chuàng)建一個CButton
的派生類,叫做CMyButton
。
在MFC框架中從CButton
派生自己的類有許多好處,最大的好處是我們不用手工添加任何一行代碼就可以創(chuàng)建了一個擁有全部默認(rèn)功能的Windows控件。因為MFC實(shí)現(xiàn)了所有的默認(rèn)的消息映射,因此我們可以挑選我們感興趣的消息自己處理,而不用去管其他消息。
這里我們要為按鈕設(shè)計的功能是,鼠標(biāo)懸停時變?yōu)辄S色。
為了檢查鼠標(biāo)是否懸停于按鈕上,我們設(shè)置一個成員變量m_bOverControl ,TRUE表示鼠標(biāo)懸停,然后設(shè)置一個周期(使用定時器)跟蹤鼠標(biāo)是否已離開控件,這是因為,系統(tǒng)并沒有OnMouseEnter
和OnMouseLeave
函數(shù)供我們調(diào)用,因此我們必須使用OnMouseMove
。如果,在一個時間點(diǎn)上,發(fā)現(xiàn)鼠標(biāo)已離開按鈕,我們關(guān)閉定時器并重畫控件。
使用ClassWizard加入WM_MOUSEMOVE和WM_TIMER的消息映射,響應(yīng)函數(shù)分別是OnMouseMove
和OnTimer
。
ClassWizard將在你的按鈕類文件中加入下面的代碼:
BEGIN_MESSAGE_MAP(CMyButton, CButton)//{{AFX_MSG_MAP(CMyButton) ON_WM_MOUSEMOVE() ON_WM_TIMER()//}}AFX_MSG_MAP END_MESSAGE_MAP()/////////////////////////////////////////////////////////////////////////////// CMyButton message handlersvoid CMyButton::OnMouseMove(UINT nFlags, CPoint point) {// TODO: Add your message handler code here and/or call default CButton::OnMouseMove(nFlags, point); }void CMyButton::OnTimer(UINT nIDEvent) {// TODO: Add your message handler code here and/or call default CButton::OnTimer(nIDEvent); }
消息映射的入口(即BEGIN_MESSAGE_MAP
) 建立了窗口消息與響應(yīng)函數(shù)的對應(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的源文件中,我們不需要去看,只要按照約定來做就可以了。
假設(shè)我們已經(jīng)聲明了兩個變量m_bOverControl和m_nTimerID,類型分別是BOOL和UINT, 并且在類的構(gòu)造函數(shù)中把它們初始化,我們的消息處理應(yīng)使用下面的代碼:
void CMyButton::OnMouseMove(UINT nFlags, CPoint point) {if (!m_bOverControl)// Cursor has just moved over control { TRACE0("Entering controln"); m_bOverControl = TRUE;// Set flag telling us the mouse is in Invalidate();// Force a redraw SetTimer(m_nTimerID,100, NULL);// Keep checking back every 1/10 sec } CButton::OnMouseMove(nFlags, point);// drop through to default handler }void CMyButton::OnTimer(UINT nIDEvent) {// Where is the mouse? CPoint p(GetMessagePos()); ScreenToClient(&p);// Get the bounds of the control (just the client area) CRect rect; GetClientRect(rect);// Check the mouse is inside the controlif (!rect.PtInRect(p)) { TRACE0("Leaving controln");// if not then stop looking... m_bOverControl = FALSE; KillTimer(m_nTimerID);// ...and redraw the control Invalidate(); }// drop through to default handler CButton::OnTimer(nIDEvent); }
最后我們來畫出我們需要的效果,我們不再進(jìn)行消息映射,而是重載CWnd::DrawItem
虛函數(shù)。只有當(dāng)控件設(shè)置owner-drawn風(fēng)格時這個函數(shù)才能被調(diào)用,并且這個函數(shù)沒有默認(rèn)的實(shí)現(xiàn)代碼,虛函數(shù)的設(shè)計只為了在派生類中進(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);// draw the control edges (DrawFrameControl is handy!)if (state & ODS_SELECTED) pDC->DrawFrameControl(rect, DFC_BUTTON, DFCS_BUTTONPUSH | DFCS_PUSHED);else pDC->DrawFrameControl(rect, DFC_BUTTON, DFCS_BUTTONPUSH);// Deflate the drawing rect by the size of the button’s edges rect.DeflateRect( CSize(GetSystemMetrics(SM_CXEDGE), GetSystemMetrics(SM_CYEDGE)));// Fill the interior color if necessaryif (m_bOverControl) pDC->FillSolidRect(rect, RGB(255,255,0));// yellow// Draw the textif (!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); } }
接下來,我們剩下最后一步。為控件設(shè)置owner drawn風(fēng)格。我們可以在對話框的資源編輯器中,右鍵單擊按鈕控件,選擇“屬性”,然后在Style中選中owner drawn風(fēng)格。但是有一種更好的方法,使得使用新建類子類化的按鈕自動的設(shè)置owner drawn風(fēng)格。為了完成這個功能,我們重載最后一個函數(shù):PreSubclassWindow
。
這個函數(shù)將在子類化窗口時被調(diào)用,次序是在CWnd::Create
或DDX_Control
之后,這就是說,無論是動態(tài)的創(chuàng)建窗口實(shí)例還是使用對話框模板創(chuàng)建,這個函數(shù)都將被調(diào)用。PreSubclassWindow
在窗口子類化創(chuàng)建后和窗口被顯示前被調(diào)用,換句話說,這是我們來做窗口初始化的一個最好時機(jī)。
一個重點(diǎn)要注意的地方是: 如果你是用對話框資源創(chuàng)建一個控件,那么你要子類化的控件將不會響應(yīng)WM_CREATE消息,所以我們不能在OnCreate
函數(shù)中做初始化的工作,因為它并不是在所有的情況下都被調(diào)用。
使用ClassWizard重載PreSubclassWindow
函數(shù)并加入下面的代碼
void CMyButton::PreSubclassWindow() { CButton::PreSubclassWindow(); ModifyStyle(0, BS_OWNERDRAW);// make the button owner drawn }
祝賀 - 你的Cbutton
派生類已經(jīng)完成。
子類化
在創(chuàng)建時使用DDX子類化
在這個例子中,我們使用對話框編輯器在對話框中加入了一個新的按鈕:
然后,使用ClassWizard為你的按鈕控件添加成員變量,變量類型選擇我們剛剛建立的類CMyButton
ClassWizard g會在對話框的DoDataExchange
函數(shù)中創(chuàng)建一個DDX_Control
調(diào)用。DDX_Control
啟動了子類化過程,使得按鈕控件使用CMyButton
類進(jìn)行消息映射,而不是使用通常的CButton
。
使用沒有在ClassWizard中注冊的類子類化窗口
如果你在工程中加入了一個新的窗口類,并且希望使用這個新類類型子類化你的窗口,但是ClassWizard中并沒有提供新類的選項,那么你需要重新生成class wizard文件。
先備份以下工程中的.clw文件,然后刪除它。接下來在Visual Studio中按Ctrl+W。你將看到一個提示框,要求你加入ClassWizard中包含類的文件,確認(rèn)選擇的文件中包含了新類的文件(soarlove注:一般情況下,選擇“add all”即可。
現(xiàn)在你的新類已經(jīng)可以供選擇。如果不想這樣做,你還有一個通用的方法,就是在選擇類型的時候使用通用的類(比如CButton
),然后在頭文件中手工把通用類(CButton)改為你的新類(CMyButton
)。
子類化一個存在的窗口
使用DDX固然簡單,但是不能幫助我們實(shí)現(xiàn)一個已存在窗口的子類化。比如你想在combobox中子類化一個Edit控件,那么在你子類化Edit控件之前,你需要先創(chuàng)建combobox控件。
這種情況下,我們使用SubclassDlgItem
或者SubclassWindow
函數(shù)。這兩個函數(shù)允許你動態(tài)的子類化一個窗口,換句話說,把一個新的窗口實(shí)例與已經(jīng)存在的窗口建立關(guān)聯(lián)。
比如,假設(shè)有一個對話框中包含了一個按鈕IDIDC_BUTTON1
。這個按鈕已經(jīng)被創(chuàng)建,我們想用一個CMyButton
的實(shí)例來與之關(guān)聯(lián),以使得按鈕符合我們需要的行為。
為了做到這些,我們需要有一個新類型的實(shí)例,最后的方法是在對話框或視的頭文件中加入成員函數(shù)。
CMyButton m_btnMyButton;
然后在對話框的OnInitDialog
(或任何適當(dāng)?shù)牡胤? 中調(diào)用:
m_btnMyButton.SubclassDlgItem(IDC_BUTTON1,this);
假設(shè)你已經(jīng)有了一個窗口的指針,或者你工作在一個CView
或其他CWnd
派生類中里面的控件被動態(tài)的創(chuàng)建,或者你不想使用SubclassDlgItem
函數(shù),那么你可以使用下面的方法:
CWnd* pWnd = GetDlgItem(IDC_BUTTON1);// or use some other method to get// a pointer to the window you wish// to subclass ASSERT( pWnd && pWnd->GetSafeHwnd() ); m_btnMyButton.SubclassWindow(pWnd->GetSafeHwnd());
畫按鈕是非常簡單的,不需要考慮按鈕的風(fēng)格(比如flat風(fēng)格),也不需要考慮適應(yīng)文字,僅僅需要考慮你畫的范圍。如果你編譯運(yùn)行提供的演示代碼,那么你將看到,當(dāng)鼠標(biāo)懸停于按鈕上方時,按鈕變?yōu)辄S色。
注意,實(shí)際上我們只重載了畫的函數(shù),并截取了鼠標(biāo)移動的函數(shù)。其余的功能都還是使默認(rèn)響應(yīng)的。
結(jié)論
子類化并不難 - 你只要認(rèn)真的選擇你要子類化的類并且知道你要映射那些消息。要熟悉你要子類化的類,了解提供的消息和類中的虛函數(shù)。