介绍
作为一个程序员有许多普通的windows控件可以用在应用程序的外观上。许多的控件的从列表到按钮再到进程条都是可以现成的用。尽管如此,在如此多的控件中我们还是会碰到那些标准的控件不够用的时候。欢迎进入子分类控件的艺术。
子分类一个windows控件不像子分类一个C++类。子分类一个控件意味这你用你自己的消息处理函数取代了改控件的一些或者所有的消息处理函数。你可以有效的截获改控件的消息并使它按照你的意愿行事,而非windows的默认方式。这可以让改控件实现大多数而非全部的你想得到的行为,并且使它表现得很完美。有两种类型的子分类,局部子分类和全局子分类。局部子分类就是子分类一个实体,全局子分类就是将一个特定类型的控件全部子分成你的类型。
记住一个从CWnd类派生的类对象和一个与它相联的窗口(hwnd)的区别是很重要的。CWnd的派生类对象包含一个成员变量指向hwnd,而且包含那些通过hwnd作为参数的处理消息的函数(比如,WM_PAINT, WM_MOUSEMOVE)。当你子分类一个控件通过你的C++对象时,你就是将相应的hwnd连接到你的C++对象上并把改控件将激发的消息回调函数改成你的。
子分类是很容易的。首先,你创建一个处理了你感兴趣的所以消息的类,然后将该类来子分一个已经存在的控件使它按照你的新类的行事。某中方面上改控件已经变成了你所拥有的了。在这个例子中我们将子分一个按钮控件并且使它做一些它从来都没能够做的事。
一个新类
子分一个控件我们需要创建一个新类改类应该处理了所有我们感兴趣的消息。由于我们很懒,最好是使我们处理的消息最少,而且最好的方式是从你将要子分的控件派生你的新类,我们这里选择的是CButton。
我们设想的是使按钮在鼠标每次经过时显示出高亮的黄色。奇怪的事已经产生了。首先我们通过向导创建一个从CButton派生的新类CMyButton。
通过MFC框架派生CButton类有许多优点,最大的就是我们不需要实际的为我们的类添加一行控件的代码。假如我们愿意我们可以通过我们的新类在下一步子分一个按钮控件,尽管有些烦琐。这是因为MFC实现了所有缺省的消息处理函数,所以我们可以简单的选择一个我们感兴趣的消息忽略其他的。
However for this example we have loftier plans for our control - making it bright yellow.
无论怎样在这个例子中我们将使我们的控件表面高亮黄色显示。
为了检查鼠标是否是经过了控件我们将设置一个布尔变量m_bOverControl为TRUE当鼠标进入控件边界时,并且通过定时器实时的检查跟踪鼠标什么时候离开了控件。不幸的是我们没有平台提供的OnMouseEnter 和 OnMouseLeave函数可用,我们将通过OnMouseMove。加入我么在某个时候发现鼠标不再在控件上,我们将关闭定时器并重画该控件。
通过类向导添加WM_MOUSEMOVE和WM_TIMER消息处理函数OnMouseMove和OnTimer 。
类向导将会添加如下代码到你的新button类:
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);
}
消息映像的入口将消息映射成函数。ON_WM_MOUSEMOVE映射成函数OnMouseMove,ON_WM_TIMER映射成OnTimer。这些宏在MFC源代码中定义,但是它们不需要阅读。这个练习仅仅知道它们这样处理就可以了。
我们定义了两个布尔变量m_bOverControl和m_nTimer,一个UNIT变量,在构造函数里面初始化它们,我们的消息处理函数如下:
IDvoid 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。这个方法仅仅是在自绘控件时调用,而且没有一个缺省的实现可以让你调用。(可以通过ASSERT'试试)这个方法被设计成仅被重载以及被派生类使用。
通过向导添加一个DrawItem
方法并且添加如下代码:
Collapse
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的“drop-in”。为了实现这点我们需要重写最后一个方法:PreSubclassWindow
.
这个方法被SubclassWindow
调用,并依次被CWnd::Create
或者DDX_Control
调用,这意味着加入你动态或者通过对话框模板创建一个新类的对象,PreSubclassWindow仍然会被调用。PreSubclassWindow将在你子分的控件已经产生但是还为显示之前调用。换句话说这就是控件的一个完美的初始化时刻。
需要重点注意的一点是:假如你的控件是通过对话框编辑器创建,那么你子分的控件将不会有WM_CREATE消息,因此我们不能通过使用OnCreate
来初始化,因为它根本不会被调用。
通过向导重载PreSubclassWindow并添加如下代码:
void CMyButton::PreSubclassWindow()
{
CButton::PreSubclassWindow();
ModifyStyle(0, BS_OWNERDRAW); // make the button owner drawn
}
祝贺你-你现在已经有一个CButton的派生类了!
子类
通过DDX在创建的时候子分一个窗口
在这个例子中我们要子分的控件是在对话框上放置的:
我们让正常的对话框创建流程创建一个带有控件的对话框,然后通过DDX和我们的新类子分改控件。为了做到这点,我们仅仅需要通过向导添加一个控件变量使其成为对话框类的成员(在这里它的ID是IDC_BUTTON1
),类名为CMyButton。
向导在你的对话框成员方法DoDataExchange中产生了一个DDX_Control调用。DDX_Control会调用SubclassWindow使得该按钮使用CMyButton代替常规的CButton的处理方法。此按钮已经被劫持而且会按照你的设想形式了。
子分类一个窗口但是不被向导识别
加入你添加一个窗口类到你的工程并且希望通过这个类的一个对象子分一个窗口,但是向导并不允许你的新类作为一个类型选项,这时你也许需要重新构建类向导文件了。
先备份工程中的.clw文件,然后删除之,接着在Visual Studio上按CTRL+W。你将会看到一个提示你哪些文件将被包含到类扫描过程中。确信你的新类文件在其中。
现在你的新类将可以作为一个类选项了,如果不是,你仍然可以通过类向导产生一个控件(比如说CButton),然后在头文件中手动修改其类名(比如CMyButton)。
子分一个存在的窗口
使用DDX很简单,但是它不能帮助我们子分一个已经存在的控件。比如说,你想子分一个组合框中的编辑框控件。你需要在你子分编辑框控件之前已经创建了组合框(因此它的子编辑框窗口也就创建了)。
在这种情况下你可以使用非常好用的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());
绘制按钮非常简单,除了不能将按钮的样式设为flat或者两端对齐的文本外其他任何你想要的样式都可以。当你编译运行改程序时你会看见一个简单的按钮当你的鼠标经过它时会呈现出亮黄色。
注意到我们仅仅真正的重载了自绘的方法,以及鼠标移动消息的处理函数,这意味这该控件仍然是下按式的按钮。给对话框类添加一个单击的处理函数你会发现它仍然可以被调用。
结尾
子分类并不难-你仅仅需要仔细选择你需要子分的类,而且要意识到你需要处理的消息函数。仔细研究你需要子分的控件—学习相关消息的处理函数和该类实现的虚成员方法。一旦你深入一个控件并且掌握了它的内部工作机制,你会发现一切都是那么容易!