摘要
作为一个程序员,我们经常会在程序中用到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风格),也不需要考虑适应文字,仅仅需要考虑你画的范围。如果你编译运行提供的演示代码,那么你将看到,当鼠标悬停于按钮上方时,按钮变为黄色。
注意,实际上我们只重载了画的函数,并截取了鼠标移动的函数。其余的功能都还是使默认响应的。
结论
子类化并不难 - 你只要认真的选择你要子类化的类并且知道你要映射那些消息。要熟悉你要子类化的类,了解提供的消息和类中的虚函数。