创建您自己的控件 - 子类化艺术






4.85/5 (100投票s)
MFC 中 Windows 公共控件子类化的介绍
引言
作为程序员,你可以使用许多常见的 Windows 控件来为应用程序提供用户界面。从列表、按钮到进度控件,应有尽有。即便如此,也常常会遇到这样的情况:尽管标准控件种类繁多,但它们仍然不够用。欢迎来到子类化控件的精妙艺术。
子类化控件与子类化 C++ 类并非一回事。子类化控件意味着你用自己的代码替换掉一个窗口的部分或全部消息处理程序。你有效地“劫持”了这个控件,让它按照你的意愿行事,而不是按照 Windows 的默认方式。这使你能够将一个基本符合要求但又不完全满足需求的控件,打磨到完美。子类化有两种类型:*实例子类化*和*全局子类化*。实例子类化是指你只对一个窗口实例进行子类化。全局子类化则会用你的版本替换掉*所有*特定类型的窗口。这里我们只讨论实例子类化。
重要的是要区分派生自 CWnd
的对象与窗口本身(一个 HWND
)。你的 C++ CWnd
派生对象包含一个指向 HWND
的成员变量,并包含当处理消息(例如 WM_PAINT
、WM_MOUSEMOVE
)时 HWND
的消息泵会调用的函数。当你用 C++ 对象对窗口进行子类化时,你就是将该 HWND
附加到你的 C++ 对象上,并将该对象的函数设置为该 HWND
的消息泵将要调用的回调函数。
子类化很容易。首先,你创建一个类来处理你感兴趣的所有窗口消息,然后,你物理地子类化一个现有窗口,让它按照你的新类的指示行事。这个窗口在某种意义上被“附身”了。在本例中,我们将子类化一个按钮控件,并让它做一些它自己都不知道能做的事情。
新类
要子类化一个控件,我们需要创建一个新类来处理我们感兴趣的所有窗口消息。既然我们比较懒,最好尽量减少需要处理的消息数量,最简单的方法就是让你的类派生自你要子类化的控件类。在我们的例子中,就是 CButton
。
假设我们想做一些奇怪的事情,比如每次鼠标移到按钮上时,让按钮变成明亮的黄色。这比这更奇怪的事情都做过。我们要做的第一件事是使用 ClassWizard 创建一个派生自 CButton
的新类,命名为 CMyButton
。
在 MFC 框架中派生自 CButton
有很多优点,最大的优点是我们不需要为我们的类添加一行代码,它就能成为一个功能齐全的 Windows 控件。如果愿意,我们可以直接进行下一步,用我们的新类子类化一个按钮控件,我们将得到一个功能完善,但略显无聊的按钮控件。这是因为 MFC 为它所有的消息实现了默认处理程序,所以我们可以只选择我们感兴趣的消息,而忽略其他消息。
然而,在这个例子中,我们对我们的控件有更宏伟的计划——让它变成明亮的黄色。
为了检查鼠标是否在控件上,当鼠标进入控件时,我们将设置一个名为 m_bOverControl 的变量为 TRUE,然后(使用定时器)定期检查,以跟踪鼠标何时离开控件。不幸的是,没有像 OnMouseEnter
和 OnMouseLeave
这样的函数可以在跨平台使用,所以我们只能使用 OnMouseMove
。如果在定时器滴答时,我们发现鼠标不再控件内,我们就关闭定时器并重绘控件。
使用 ClassWizard 为 OnMouseMove
和 OnTimer
分别添加映射到 OnMouseMove
和 OnTimer
的 WM_MOUSEMOVE 和 WM_TIMER 消息处理程序。
ClassWizard 会将以下代码添加到你的新按钮类中
BEGIN_MESSAGE_MAP(CMyButton, CButton) //{{AFX_MSG_MAP(CMyButton) ON_WM_MOUSEMOVE() ON_WM_TIMER() //}}AFX_MSG_MAP END_MESSAGE_MAP() ///////////////////////////////////////////////////////////////////////////// // CMyButton message handlers void 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
部分)将窗口消息映射到函数。ON_WM_MOUSEMOVE
将 WM_MOUSEMOVE 映射到你的 OnMouseMove
函数,ON_WM_TIMER
将 WM_TIMER 映射到 OnTimer
。这些宏定义在 MFC 源代码中,但*不是*必读内容。对于这个练习,只要相信它们能完成它们的工作就行。
假设我们已经声明了两个变量 m_bOverControl 和 m_nTimerID,类型分别为 BOOL 和 UINT,并在构造函数中进行了初始化,那么我们的消息处理程序将如下所示
void CMyButton::OnMouseMove(UINT nFlags, CPoint point) { if (!m_bOverControl) // Cursor has just moved over control { TRACE0("Entering control\n"); 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 control if (!rect.PtInRect(p)) { TRACE0("Leaving control\n"); // if not then stop looking... m_bOverControl = FALSE; KillTimer(m_nTimerID); // ...and redraw the control Invalidate(); } // drop through to default handler CButton::OnTimer(nIDEvent); }
我们新类的最后一部分是绘图,为此我们不处理消息,而是重写 CWnd::DrawItem
虚拟函数。这个函数*仅*为拥有绘图(owner-drawn)的控件调用,并且没有可以调用的默认实现(如果你尝试调用,它会进行 ASSERT)。这个函数是为派生类重写和使用的。
使用 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); // 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 necessary if (m_bOverControl) pDC->FillSolidRect(rect, RGB(255, 255, 0)); // yellow // Draw the text 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); } }
一切就绪——但还有最后一步。DrawItem
函数要求控件是拥有绘图的。这可以通过在对话框资源编辑器中勾选相应的框来实现——但一种更优雅的方式是让类本身自动设置它所子类化的窗口的样式,以使该类成为 CButton
的真正“即插即用”的替代品。为此,我们重写最后一个函数:PreSubclassWindow
。
该函数由 SubclassWindow
调用,而 SubclassWindow
又由 CWnd::Create
或 DDX_Control
调用。这意味着,如果你动态创建了你的新类实例,或者通过对话框模板创建,PreSubclassWindow
仍然会被调用。PreSubclassWindow
会在你子类化的窗口创建后,但在它可见之前被调用。换句话说,这是执行需要窗口存在的初始化的绝佳时机。
这里有一个重要的说明:如果你使用对话框资源创建控件,那么你的子类化控件*将不会*收到 WM_CREATE 消息,因此我们不能将 OnCreate
用于我们的初始化,因为它不会在所有情况下都被调用。
使用 ClassWizard 重写 PreSubclassWindow
并添加以下代码
void CMyButton::PreSubclassWindow() { CButton::PreSubclassWindow(); ModifyStyle(0, BS_OWNERDRAW); // make the button owner drawn }
恭喜——你现在拥有了一个 Cbutton
派生类!
子类化
使用 DDX 在创建时子类化窗口
在本例中,我正在处理一个对话框,并在上面放置了一个按钮控件
我们让正常的对话框创建例程用控件创建对话框,并使用 DDX_...
例程用我们的新类子类化控件。为此,只需使用 ClassWizard 将一个成员变量添加到你的对话框类中,并将其关联到你的按钮控件(在本例中,它的 ID 是 IDC_BUTTON1
),然后选择变量类型为 Control,类名 CMyButton
。
ClassWizard 会在你的对话框的 DoDataExchange
函数中生成一个 DDX_Control
调用。DDX_Control
调用 SubclassWindow
,这会使按钮使用 CMyButton
的消息处理程序,而不是通常的 CButton
处理程序。该按钮已被劫持,从现在起将按照我们想要的方式运行。
使用 ClassWizard 未识别的类子类化窗口
如果你已将一个窗口类添加到你的项目中,并且想用该新类的对象类型来子类化一个窗口,但 ClassWizard 没有提供该新对象的类型作为选项,那么你可能需要重新生成 class wizard 文件。
备份你项目的 .clw 文件,删除原始文件,然后进入 Visual Studio 并按 Ctrl+W。届时会提示你选择要包含在类扫描中的文件。确保包含新类文件!
你的新类现在应该可以作为选项使用了。如果不行,你也可以始终使用 classwizard 将你的控件子类化为通用控件(例如 CButton
),然后手动进入头文件,将其更改为你想要的类(例如 CMyButton
)。
子类化现有窗口
使用 DDX 非常简单,但如果我们需要子类化一个已经存在的控件,它就帮不上忙了。例如,假设你想子类化组合框(combobox)中的编辑控件。你需要先创建组合框(从而创建其子编辑窗口),然后才能子类化编辑窗口。
在这种情况下,你可以利用方便的 SubclassDlgItem
或 SubclassWindow
函数。这两个函数允许你动态地子类化窗口——换句话说,将你新窗口类型的对象附加到一个现有窗口。
例如,假设我们有一个对话框,其中包含一个 ID 为 IDC_BUTTON1
的按钮。这个按钮已经创建了,我们想将这个按钮关联到一个 CMyButton
类型的对象,以便按钮按照我们想要的方式运行。
要做到这一点,我们需要已经创建了一个我们新类型的对象。对话框或视图类中的成员变量是完美的。
CMyButton m_btnMyButton;
然后在你的对话框的 OnInitDialog
(或任何合适的地方)中调用
m_btnMyButton.SubclassDlgItem(IDC_BUTTON1, this);
或者,假设你已经有了一个你想要子类化的窗口的指针,或者你在 CView
或其他 CWnd
派生类中工作,其中的控件是动态创建的,或者你不想使用 SubclassDlgItem
。只需调用
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());
按钮绘图非常简单,没有考虑按钮样式,如扁平按钮或文本对齐,但为你提供了实现任何你想要的功能的空间。如果你编译并运行附带的代码,你将看到一个简单的按钮,当鼠标移过它时,它会变成亮黄色。
请注意,我们实际上只重写了绘图功能,并拦截了鼠标移动函数(但将这些函数传递给了默认处理程序)。这意味着控件的底层仍然是一个按钮。在你的对话框类中添加一个按钮点击处理程序,你就会发现它仍然会被调用。
结论
子类化并不难——你只需要仔细选择你想子类化的类,并注意你需要处理哪些消息。阅读你正在子类化的控件的文档——了解它处理的消息以及它实现类的虚拟成员函数。一旦你连接到一个控件并接管了它的内部运作,天空才是你的极限。
历史
2001 年 10 月 26 日——在 SubclassWindow 和 SubclassDlgItem 中添加了信息