摘要
作為一個程序員,我們經常會在程序中用到Windows通用控件。比如按鈕控件,進度條控件等等。但是有時我們需要給控件更多的特色,這就需要做控件的子類化(subclassing).
子類化一個Windows控件與子類化一個C++類不同,子類化一個控件要求你把一個窗口的一些或所有的消息映射都替換成自己的函數來響應,這樣你就有效的阻止了控件去做系統默認的行為,而按自己的想法去做。子類化有兩種類型:實例子類化(instance subclassing)和全局子類化(global subclassing)。實例子類化是子類化一個窗口中的單一實例,全局子類化是把整個窗口子類化為一個特殊的類型。這里我們僅討論單一實例子類化。
記住CWnd
派生類對象與窗口本身(一個HWND
)的差別是很重要的。你的C++CWnd
-派生類對象包含了一個指向HWND
的成員函數,并且包含了當處理消息時HWND
消息泵的響應函數(比如WM_PAINT
,WM_MOUSEMOVE
)。但你用一個C++對象子類化一個窗口時,你就把HWND
與C++對象關聯起來,并且設置了處理消息時把自定義的回調函數提供給HWND
消息使用。
子類化過程很簡單,首先創建一個類映射窗口的所有消息,然后把控件用作為這個類的實例。例如,下面的例子中我們做一個按鈕的子類化。
新類
為了子類化一個控件,我們需要創建一個新類,并映射所有我們感興趣的消息。為了簡便,我們一般都從控件標準類中派生自己的新類,這里與按鈕控件對應的標準類為CButton。
下面假定我們要實現的效果是,當鼠標懸停在按鈕上方時,按鈕顯示為黃色。首先我們使用ClassWizard創建一個CButton
的派生類,叫做CMyButton
。
在MFC框架中從CButton
派生自己的類有許多好處,最大的好處是我們不用手工添加任何一行代碼就可以創建了一個擁有全部默認功能的Windows控件。因為MFC實現了所有的默認的消息映射,因此我們可以挑選我們感興趣的消息自己處理,而不用去管其他消息。
這里我們要為按鈕設計的功能是,鼠標懸停時變為黃色。
為了檢查鼠標是否懸停于按鈕上,我們設置一個成員變量m_bOverControl ,TRUE表示鼠標懸停,然后設置一個周期(使用定時器)跟蹤鼠標是否已離開控件,這是因為,系統并沒有OnMouseEnter
和OnMouseLeave
函數供我們調用,因此我們必須使用OnMouseMove
。如果,在一個時間點上,發現鼠標已離開按鈕,我們關閉定時器并重畫控件。
使用ClassWizard加入WM_MOUSEMOVE和WM_TIMER的消息映射,響應函數分別是OnMouseMove
和OnTimer
。
ClassWizard將在你的按鈕類文件中加入下面的代碼:
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
) 建立了窗口消息與響應函數的對應關系。ON_WM_MOUSEMOVE
把WM_MOUSEMOVE消息與OnMouseMove
函數建立響應的關系,ON_WM_TIMER
m把WM_TIMER消息與OnTimer
函數建立了響應的關系。這些宏定義在MFC的源文件中,我們不需要去看,只要按照約定來做就可以了。
假設我們已經聲明了兩個變量m_bOverControl和m_nTimerID,類型分別是BOOL和UINT, 并且在類的構造函數中把它們初始化,我們的消息處理應使用下面的代碼:
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);
}
最后我們來畫出我們需要的效果,我們不再進行消息映射,而是重載CWnd::DrawItem
虛函數。只有當控件設置owner-drawn風格時這個函數才能被調用,并且這個函數沒有默認的實現代碼,虛函數的設計只為了在派生類中進行實現。
使用ClassWizard重載DrawItem
函數,并加入下面的代碼
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);
}
}
接下來,我們剩下最后一步。為控件設置owner drawn風格。我們可以在對話框的資源編輯器中,右鍵單擊按鈕控件,選擇“屬性”,然后在Style中選中owner drawn風格。但是有一種更好的方法,使得使用新建類子類化的按鈕自動的設置owner drawn風格。為了完成這個功能,我們重載最后一個函數:PreSubclassWindow
。
這個函數將在子類化窗口時被調用,次序是在CWnd::Create
或DDX_Control
之后,這就是說,無論是動態的創建窗口實例還是使用對話框模板創建,這個函數都將被調用。PreSubclassWindow
在窗口子類化創建后和窗口被顯示前被調用,換句話說,這是我們來做窗口初始化的一個最好時機。
一個重點要注意的地方是: 如果你是用對話框資源創建一個控件,那么你要子類化的控件將不會響應WM_CREATE消息,所以我們不能在OnCreate
函數中做初始化的工作,因為它并不是在所有的情況下都被調用。
使用ClassWizard重載PreSubclassWindow
函數并加入下面的代碼
void CMyButton::PreSubclassWindow()
{
CButton::PreSubclassWindow();
ModifyStyle(0, BS_OWNERDRAW);
}
祝賀 - 你的Cbutton
派生類已經完成。
子類化
在創建時使用DDX子類化
在這個例子中,我們使用對話框編輯器在對話框中加入了一個新的按鈕:
然后,使用ClassWizard為你的按鈕控件添加成員變量,變量類型選擇我們剛剛建立的類CMyButton
ClassWizard g會在對話框的DoDataExchange
函數中創建一個DDX_Control
調用。DDX_Control
啟動了子類化過程,使得按鈕控件使用CMyButton
類進行消息映射,而不是使用通常的CButton
。
使用沒有在ClassWizard中注冊的類子類化窗口
如果你在工程中加入了一個新的窗口類,并且希望使用這個新類類型子類化你的窗口,但是ClassWizard中并沒有提供新類的選項,那么你需要重新生成class wizard文件。
先備份以下工程中的.clw文件,然后刪除它。接下來在Visual Studio中按Ctrl+W。你將看到一個提示框,要求你加入ClassWizard中包含類的文件,確認選擇的文件中包含了新類的文件(soarlove注:一般情況下,選擇“add all”即可。
現在你的新類已經可以供選擇。如果不想這樣做,你還有一個通用的方法,就是在選擇類型的時候使用通用的類(比如CButton
),然后在頭文件中手工把通用類(CButton)改為你的新類(CMyButton
)。
子類化一個存在的窗口
使用DDX固然簡單,但是不能幫助我們實現一個已存在窗口的子類化。比如你想在combobox中子類化一個Edit控件,那么在你子類化Edit控件之前,你需要先創建combobox控件。
這種情況下,我們使用SubclassDlgItem
或者SubclassWindow
函數。這兩個函數允許你動態的子類化一個窗口,換句話說,把一個新的窗口實例與已經存在的窗口建立關聯。
比如,假設有一個對話框中包含了一個按鈕IDIDC_BUTTON1
。這個按鈕已經被創建,我們想用一個CMyButton
的實例來與之關聯,以使得按鈕符合我們需要的行為。
為了做到這些,我們需要有一個新類型的實例,最后的方法是在對話框或視的頭文件中加入成員函數。
CMyButton m_btnMyButton;
然后在對話框的OnInitDialog
(或任何適當的地方) 中調用:
m_btnMyButton.SubclassDlgItem(IDC_BUTTON1,this);
假設你已經有了一個窗口的指針,或者你工作在一個CView
或其他CWnd
派生類中里面的控件被動態的創建,或者你不想使用SubclassDlgItem
函數,那么你可以使用下面的方法:
CWnd* pWnd = GetDlgItem(IDC_BUTTON1);
ASSERT( pWnd && pWnd->GetSafeHwnd() );
m_btnMyButton.SubclassWindow(pWnd->GetSafeHwnd());
畫按鈕是非常簡單的,不需要考慮按鈕的風格(比如flat風格),也不需要考慮適應文字,僅僅需要考慮你畫的范圍。如果你編譯運行提供的演示代碼,那么你將看到,當鼠標懸停于按鈕上方時,按鈕變為黃色。
注意,實際上我們只重載了畫的函數,并截取了鼠標移動的函數。其余的功能都還是使默認響應的。
結論
子類化并不難 - 你只要認真的選擇你要子類化的類并且知道你要映射那些消息。要熟悉你要子類化的類,了解提供的消息和類中的虛函數。