控件子类化






4.94/5 (73投票s)
本文解释了如何对控件进行子类化,使其表现和外观如你所愿。文章以列表框为例。
引言
尽管Windows提供了各种常用控件,如编辑框和组合框,但它们的某些功能往往有限,外观不适合,或者根本不符合我们的需求。为了解决这个问题,我们可以创建一个新的自定义控件,或者对现有控件进行子类化,这在很多情况下可以减少工作量。
有两种子类化类型:实例子类化和全局子类化。实例子类化意味着每个控件或实例都单独修改。相反,全局子类化会创建一个钩子,以便在运行时修改多个实例,甚至所有实例。例如,全局子类化的一个例子是为软件中创建的所有按钮或所有线程添加皮肤,而实例类型的子类化则是手动创建一个单独的CEdit
控件,使其显示数字为红色。
在本教程中,我们将对CListBox
类的一个实例进行子类化,如上所示,使其能够更改颜色、包含图标、拥有自定义滚动条和MouseOver
效果。如果时间允许,我将很快撰写一篇关于如何执行全局子类化的文章。有关子类化的更多信息,您可以查阅Chris Maunder的教程:创建自己的控件 - 子类化的艺术。
新类的诞生
首先,我们必须启动一个新项目,以便开发和调试新类。一旦它完善,就可以轻松地将其添加到任何项目中。在本例中,我创建了一个MFC对话框,并将其命名为ListDemo。现在,我们必须创建一个新的派生类,以CListBox
作为基类。转到菜单View->Classwizard,然后点击Add Class -> New Button。然后输入类名CListBoxEx
,并选择CListBox
作为基类名。这就是新类的名称;它可以是任何你想要的名称。红色的圆圈显示了你应该去的地方。
现在点击OK,我们就可以开始工作了。转到Dialog Editor并添加一个列表框,也就是我们将要子类化的那个。我们将使用这个列表来测试我们的代码。右键单击并转到Properties,然后在Styles选项卡中,将Owner Draw设置为Variable。每当我们想子类化一个控件时,都必须将其设置为Owner Drawn。由于列表框由多个项组成,我们将其设置为variable,以便调用一个函数MeasureItem
来检索每个项的高度,因此我们可以修改它。我们必须勾选Has strings,因为我们正在创建的可以包含字符串,而不仅仅是图标。现在取消勾选Vertical Scroll,因为我们不希望出现常规的滚动条。
再次转到ClassWizard并点击Member Variables选项卡。选择列表框的ID,然后点击Add Variable。确保将Category选择为control,将Variable type选择为CListBoxEx
。我将其命名为m_DemoList。
在开始之前,让我们将#include <ListBoxEx.h>
添加到ListDemoDlg.h中。即使ClassWizard也会对此发出警告。
从现在开始,我们将按以下方式进行:
编码:有趣的部分
- 列表框边框:边框和鼠标悬停效果。
- 背景
- 列表框项:文本和位图
- 滚动条
编码:有趣的部分
现在我们准备开始这段神秘的代码的伟大旅程。其实不然。多亏了子类化,这一切都相对容易。对于无窗口控件(如我们正在使用的控件),Create
和PreCreateWindow
消息以及许多其他消息不会被调用。因此,我们必须依赖构造函数和PresubclassWindow
等函数来执行初始化过程。我更喜欢后者,因为在构造函数中调用某些窗口函数(如m_bEnabled = IsWindowEnabled();
)会导致非法操作,因为控件的HWND (m_hWnd)
仍然是NULL
。然而,PresubclassWindow
在对象附加到窗口后被调用。因此,让我们为这个虚拟函数添加一个处理程序。
在ClassWizard中,选择CListBoxEx
作为Class Name,然后滚动消息直到找到PresubclassWindow
,选择它,然后双击它或点击Add Function。我们将得到
void CListBoxEx::PreSubclassWindow() { // TODO: Add your specialized code here and/or call the base class CListBox::PreSubclassWindow(); }
现在我们准备绘制边框了。
1. 列表框边框
我们希望创建一个看起来像正常边框的边框,但在鼠标光标进入列表框时,它会发生变化,从而使其更具交互性。您可能想查看顶部的图片。正如您所看到的,围绕第一个列表框的边框(鼠标光标悬停在其上)看起来更暗,呈2D效果,而第二个列表框则给人3D和向后凹陷的印象。
我们首先需要创建一个变量来跟踪鼠标是否在上面。我们称之为m_bOver
,其类型为BOOL
(TRUE
/FALSE
)。我们应该在protected
部分声明它,因为我们不希望除派生类或类本身以外的其他来源直接访问该变量。有两种简单的方法可以做到这一点。您可以选择在ListBoxEx.h的protected部分声明它,或者在VC++ 6中,点击工作区中的ClassView,右键单击CListBoxEx
,然后点击Add Member Variable。
我们现在必须创建一个函数来绘制边框。在protected部分使用以下声明:void DrawBorders ();
或者执行相同的操作来添加变量,但改为点击Add Member Function。我们将得到以下实现。
void CListBoxEx::DrawBorders()
{
}
我们现在开始输入代码。
void CListBoxEx::DrawBorders() { //Gets the Controls device context used for drawing CDC *pDC=GetDC(); //Gets the size of the control's client area CRect rect; GetClientRect(rect); /* Inflates the size of rect by the size of the default border Suppose rect is (0,0,100,200) and the default border is 2 pixels, after InflateRect, rect should be (-2,-2, 102,202) and the border will be drawn from -2 to 0, -2 -> 0, 102->100, 202->200. */ rect.InflateRect(CSize(GetSystemMetrics(SM_CXEDGE), GetSystemMetrics(SM_CYEDGE))); //Draws the edge of the border depending on whether the mouse is //over or not if (m_bOver)pDC->DrawEdge(rect,EDGE_BUMP ,BF_RECT ); else pDC->DrawEdge(rect,EDGE_SUNKEN,BF_RECT ); ReleaseDC(pDC); //Frees the DC }
DrawEdge
函数通常用于绘制边框。例如,EDGE_BUMP
用于绘制默认列表框边框,其中内部部分凹陷。其他常用的有EDGE_ETCHED
,EDGE_SUNKEN
,和EDGE_RAISED
。
上面的代码目前不会产生任何效果。我们仍然需要确定鼠标何时进入以及何时离开,以便修改m_bOver。我们还需要调用DrawBorders()
。由于我们将使用m_bOver,让我们初始化它。在PreSubclassWindow
中添加m_bOver = FALSE;
。
确定鼠标何时进入的各种方法之一是使用WM_MOUSEMOVE的消息处理程序,如果m_bOver是FALSE
,则意味着它是第一次进入。然后,我们必须将m_bOver = TRUE
并调用DrawBorders()
函数来更改边框样式。要做到这一点,请转到ClassWizard并为WM_MOUSEMOVE
添加一个函数。然后,我们添加其余代码。最后,我们得到
void CListBoxEx::OnMouseMove(UINT nFlags, CPoint point) { // TODO: Add your message handler code here and/or call default //If m_bOver==FALSE, and this function is called, it means that the //mouse entered. if (!m_bOver){ m_bOver=TRUE; //Now the mouse is over DrawBorders(); //Self explanatory } CListBox::OnMouseMove(nFlags, point); }
现在运行程序,您会注意到当鼠标进入时边框会改变,但当鼠标离开时不会。这是因为我们必须确定何时将m_bOver设置为FALSE
并重绘它们。
然而,确定鼠标何时离开区域并不那么简单,因为列表框不会收到外部移动的通知。到目前为止,我知道有三种方法可以做到这一点:使用定时器、捕获鼠标或手动添加OnMouseLeave
函数。使用定时器在开头提到的文章中有解释。当鼠标进入时,在OnMouseMove
中,我们可以调用SetCapture()
,并且每次移动时,使用PtInRect
来查看它是否在列表框上方。如果不在,则调用ReleaseCapture()
并设置m_bOver=FALSE;
。听起来太简单了,不是吗?嗯,它既是也不是。此方法可用于其他不具有项目列表或其他类似内容的控件。尽管我们可以在列表框中使用它,但这需要更多的知识和额外的工作。为了防止应用程序垄断光标,Windows会在更改焦点后自动释放捕获。因此,一旦选择了一个项目,我们的捕获就会失效。因此,我将重点关注第三种技术。
由于没有实现此消息的宏,我们将不得不做一些工作。找到
BEGIN_MESSAGE_MAP(CListBoxEx, CListBox)
在ON_WM_MOUSEMOVE()
之后,插入ON_MESSAGE(WM_MOUSELEAVE,OnMouseLeave)
。
BEGIN_MESSAGE_MAP(CListBoxEx, CListBox) //{{AFX_MSG_MAP(CListBoxEx) ON_WM_MOUSEMOVE() ON_MESSAGE(WM_MOUSELEAVE,OnMouseLeave) //Add this //}}AFX_MSG_MAP END_MESSAGE_MAP()
请注意,不需要分号。我们所做的基本上是使用ON_MESSAGE
宏,以便当控件接收到WM_MOUSELEAVE
时,它会转到OnMouseLeave
函数(可以是任何名称)。我们现在必须创建声明。在protected的ListBoxEx.h部分,转到
//{{AFX_MSG(CListBoxEx) afx_msg void OnMouseMove(UINT nFlags, CPoint point); //}}AFX_MSG
然后插入afx_msg LRESULT OnMouseLeave(WPARAM wParam, LPARAM lParam);
//{{AFX_MSG(CListBoxEx) afx_msg void OnMouseMove(UINT nFlags, CPoint point); afx_msg LRESULT OnMouseLeave(WPARAM wParam, LPARAM lParam); //Add this //}}AFX_MSG
我们继续在ListBoxEx.cpp中实现该函数。
LRESULT CListBoxEx::OnMouseLeave(WPARAM wParam, LPARAM lParam){ m_bOver=FALSE; DrawBorders(); return 0; }
由于鼠标不再在上面,我们将m_bOver
设置为FALSE
并调用DrawBorders()
来注意变化。如果我们现在运行此程序,我们将看不到任何内容发生。这是因为消息没有被发送到控件。因此,我们必须告诉Windows通知我们。为此,我们使用TrackMouseEvent
函数,该函数接受TRACKMOUSEEVENT
结构(我们必须声明它)作为参数。此结构中的dwFlags
必须包含TME_LEAVE
。当鼠标进入时,让我们在OnMouseMove
中添加它。
if (!m_bOver){ m_bOver=TRUE; //Now the mouse is over DrawBorders(); //Self explanatory //Add here... TRACKMOUSEEVENT track; //Declares structure track.cbSize=sizeof(track); track.dwFlags=TME_LEAVE; //Notify us when the mouse leaves track.hwndTrack=m_hWnd; //Assigns this window's hwnd TrackMouseEvent(&track); //Tracks the events like WM_MOUSELEAVE }
为了结束本节,请在#include <afxwin.h>
之前,在StdAfx.h
中添加#define _WIN32_WINNT 0x0400
,以便使TrackMouseEvent
函数可用。这仅在Windows 32位和NT框架下有效。如果您的应用程序面向旧的16位系统,则必须使用定时器或捕获鼠标。
2. 背景
设置背景颜色是最简单的事情之一。我们不是提供RGB颜色,而是使用CBrush
创建一个画笔。这更好,因为可以使用画笔轻松创建图案和加载位图,而不是仅仅设置背景颜色。
为了更改颜色,我们必须有一个CBrush
变量。在头文件的protected部分创建一个,CBrush m_BkBrush;
。我们不需要为其设置默认值,因为如果NULL
,Windows将使用默认画笔。现在我们可以添加一个函数,以便其父窗口可以更改颜色。为了让它使用,我们必须在public部分声明它。记住,我们还希望更改高亮显示单元格的颜色。让我们一石二鸟,并将此值也包含为参数。
//Declare under public in header file void SetBkColor( COLORREF crBkColor, COLORREF crSelectedColor = GetSysColor(COLOR_HIGHLIGHT));
现在,当我们想更改颜色时,可以简单地调用SetBkColor
。第二个参数是可选的。如果未输入,则将其设置为默认的高亮颜色,我认为它为深蓝色(没有桌面主题),由GetSysColor
检索。我们将在下一节中使用第二个参数。在函数的实现中,我们必须使用当前颜色创建一个画笔,并调用Invalidate()
以强制重绘。
void CListBoxEx::SetBkColor(COLORREF crBkColor,COLORREF crSelectedColor) { //Deletes previous brush. Must do in order to create a new one m_BkBrush.DeleteObject(); //Sets the brush the specified background color m_BkBrush.CreateSolidBrush(crBkColor); Invalidate(); //Forces Redraw }
WM_CTLCOLOR
消息在控件绘制之前发送。其返回值是用于绘制窗口背景的画笔。因此,我们必须拦截它以返回m_BkBrush。在ClassWizard中,为=WM_CTLCOLOR
添加一个处理程序。我们使用反射消息(=),因为这样父窗口会收到绘制消息,并将其反射回给我们来完成工作。最初,返回值是NULL。我们必须将NULL更改为m_BkBrush。
我们也应该确保在控件未启用时更改画笔。
HBRUSH CListBoxEx::CtlColor(CDC* pDC, UINT nCtlColor) { // TODO: Change any attributes of the DC here if (!IsWindowEnabled()){ CBrush br(GetSysColor(COLOR_INACTIVEBORDER)); return br; } // TODO: Return a non-NULL brush if the parent's handler should not // be called return m_BkBrush; }
最后一件事情是在画笔退出时将其删除。我们将在析构函数(~CListBoxEx()
)中这样做。
CListBoxEx::~CListBoxEx() { m_BkBrush.DeleteObject(); //Deletes the brush }
现在一切都完成了,我们必须尝试一下。在ListDemoDlg.cpp的InitDialog()
中,我们添加
m_DemoList.SetBkColor(RGB(0,0,128));
这将使背景设置为深蓝色。
有时控件会在运行时启用或禁用。因此,我们应该接收WM_ENABLE
消息,该消息指示控件的启用状态已更改。使用ClassWizard为此添加一个函数。我们应该在状态更改时强制重绘。
void CListBoxEx::OnEnable(BOOL bEnable) { CListBox::OnEnable(bEnable); // TODO: Add your message handler code here Invalidate(); }
3. 项
这可能是最长的一节。我们有几个目标。其中有生命、自由和追求幸福。无论如何,我们希望使列表能够显示彩色文本,并在需要时为每个项显示位图。当用户进行选择时,选定的项也必须高亮显示。
我们现在将声明各种变量。我们需要一个变量来跟踪文本的颜色、高亮显示时的文本颜色、每个项的大小、选定时的项的背景颜色以及要使用的位图的尺寸。所有这些都应该在protected部分。
short m_ItemHeight; //Height of each item COLORREF m_crTextHlt; //Color of the text when highlighted COLORREF m_crTextClr; //Color of the text COLORREF m_HBkColor; //Color of the highlighted item background int m_BmpWidth; //Width of the bitmap int m_BmpHeight; //Height of the bitmap
然后我们在PreSubclassWindow
下为它们设置初始值。
m_bOver = FALSE; m_ItemHeight=18; m_crTextHlt=GetSysColor(COLOR_HIGHLIGHTTEXT); m_crTextClr=GetSysColor(COLOR_WINDOWTEXT); m_HBkColor=GetSysColor(COLOR_HIGHLIGHT); m_BmpWidth=16; m_BmpHeight=16;
要设置每个项的高度,我们必须重写OnMeasureItem
。在ClassWizard中,为MeasureItem
添加一个函数。然后,将itemHeight
字段设置为m_ItemHeight
。
void CListBoxEx::MeasureItem(LPMEASUREITEMSTRUCT lpMeasureItemStruct) { // TODO: Add your code to determine the size of specified item lpMeasureItemStruct->itemHeight=m_ItemHeight; }
我们现在需要添加一个允许修改m_ItemHeight
的函数。我们称之为void SetItemHeight (int newHeight)
,并使其为public。
void CListBoxEx::SetItemHeight(int newHeight) { m_ItemHeight=newHeight; Invalidate(); }
在我们开始绘制每个单独的项之前,我们必须创建一个函数,该函数接受一个字符串和位图的资源ID。我们将调用它void AddItem(UINT IconID, LPCTSTR lpszText)
。由于它旨在供任何其他代码使用,我们将使其为public。此函数类似于AddString,但允许我们添加位图。IconID
是位图资源的ID,例如IDB_MYBITMAP
,另一个是显示在图像旁边的文本。
我们将使用AddString
函数来添加文本。但是,我们需要另一个方法来将该项与ID关联起来,以便在DrawItem
(稍后解释)中,我们可以绘制位图。AddString
和InsertString
返回当前项的索引。因此,我们将使用SetItemData
将索引与资源关联起来。然后,我们可以轻松地获取指定索引的ID。
void CListBoxEx::AddItem(UINT IconID, LPCTSTR lpszText) { //Adds a string ans assigns nIndex the index of the current item int nIndex=AddString(lpszText); //If no error, associates the index with the bitmap ID if (nIndex!=LB_ERR&&nIndex!=LB_ERRSPACE) SetItemData(nIndex, IconID); }
为了扩展其可用性,我们也希望使其能够将项插入到指定索引中,从而将其余单元格向下移动。这个函数将类似于AddItem,除了它还会接收一个额外的变量,即插入的索引。原型将是void InsertItem(int nIndex, UINT nID, LPCTSTR lpszText)
,实现如下:
void CListBoxEx::InsertItem(int nIndex, UINT nID, LPCTSTR lpszText) { int result=InsertString(nIndex,lpszText); //Inserts the string //Associates the ID with the index if (result!=LB_ERR||result!=LB_ERRSPACE) SetItemData(nIndex,nID); }
如果您仔细查看开头的三个列表框的图像,您应该看到可以像任何列表框一样添加普通文本。此外,您可以添加缩进但没有图片的文本。要做到这一点,您需要输入一个特殊值作为ID。让我们这样做:如果ID是NO_BMP_ITEM
或NULL
,文本将像在普通列表框中一样。但是,如果将BLANK_BMP
传递给此ID参数,文本将缩进以匹配带有位图的文本,但不会显示任何位图。在类开始之前的头文件中添加此内容。
#define NO_BMP_ITEM 0 #define BLANK_BMP 1
注意:我们将NO_BMP_ITEM
设置为0,因为0也等于NULL。因此,要显示普通文本,位图ID可以是NULL或NO_BMP_ITEM
。对于BLANK_BMP
,任何数字都可以。
当每个项需要绘制时,会发送=WM_DRAWITEM
消息。例如,在一个有10个项的列表中,它将被调用10次,每次都带有关于当前要绘制的项的信息。添加一个反射消息=WM_DRAWITEM
的处理程序。我们将得到
void CListBoxEx::DrawItem(LPDRAWITEMSTRUCT lpDrawItemStruct) { // TODO: Add your message handler code here }
lpDrawItemStruct
结构包含有关要绘制的项的所有信息。
下面是我们绘制项的方式。
我们首先需要从结构中获取DC(设备上下文)以及其他一些信息。添加
CDC* pDC = CDC::FromHandle(lpDrawItemStruct->hDC); //Gets the item DC //Retrieves the ID added using SetItemData UINT nID=(UINT)lpDrawItemStruct->itemData; CRect rect=lpDrawItemStruct->rcItem; //Gets the rect of the item UINT action=lpDrawItemStruct->itemAction; //What it wants to do UINT state=lpDrawItemStruct->itemState; //The item current state COLORREF TextColor=m_crTextClr; //Text color that we'll use
itemAction
字段包含我们必须执行的操作,itemState
字段包含执行操作后应有的状态。例如,假设一个项被选中并具有焦点,但用户单击了一个编辑框。焦点必须改变。在这种情况下,action将是!focus(移除焦点),state将是非焦点。我们还声明了一个变量,该变量将具有要绘制的文本的颜色。
以下是我们应考虑的操作和状态。起初它们有些令人困惑,但随着实践,您应该能够理解它们。插入
//Action statements if ((state & ODS_SELECTED) && (action & ODA_SELECT)) //Used when an item needs to be selected { //Since it will be highlighted, we create a brush with the //highlighted color CBrush brush(m_HBkColor); //Draws the highlighted rect pDC->FillRect(rect, &brush); } if (!(state & ODS_SELECTED) && (action & ODA_SELECT)) //The item needs to be deselected { //We draw the background color pDC->FillRect(rect, &m_BkBrush); } if ((action & ODA_FOCUS) && (state & ODS_FOCUS)&&(state&ODS_SELECTED)){ //It has the focus, //Draws a 3D focus rect pDC->Draw3dRect(rect,RGB(255,255,255),RGB(0,0,0)); TextColor=m_crTextHlt; } if ((action & ODA_FOCUS) && !(state & ODS_FOCUS)&&(state&ODS_SELECTED)){ //If the focus needs to be removed. CBrush brush(m_HBkColor); pDC->FillRect(rect, &brush); TextColor=m_crTextHlt; } //If the control is disabled if (state&ODS_DISABLED) TextColor=GetSysColor(COLOR_3DSHADOW);
现在我们必须检索要显示的文本,并设置其颜色和背景模式。
CString text; GetText(lpDrawItemStruct->itemID, text); //Gets the item text pDC->SetTextColor(TextColor); //No need to explain pDC->SetBkMode(TRANSPARENT); //Sets text background transparent
我们现在可以绘制位图并显示文本了。
if (nID!=NO_BMP_ITEM){ //If the item has a bitmap CDC dcMem; //New device context used as the source DC //Creates a deice context compatible to pDC dcMem.CreateCompatibleDC(pDC); CBitmap bmp; //Bitmap object //Loads the bitmap with the specified resource ID bmp.LoadBitmap(nID); //Saves the old bitmap object so that the GDI resources are not //depleted CBitmap* oldbmp=dcMem.SelectObject(&bmp); if (nID!=BLANK_BMP) //Draws the bitmap if it is not blank //Copies the bitmap to the screen pDC->BitBlt(rect.left+5,rect.top,m_BmpWidth,m_BmpHeight, &dcMem,0,0,SRCCOPY); //Selects the saved bitmap object dcMem.SelectObject(oldbmp); bmp.DeleteObject(); //Deletes the bitmap //Displays the text pDC->TextOut(rect.left+10+m_BmpWidth,rect.top,text); } //Displays the text without indenting it else pDC->TextOut(rect.left+5,rect.top,text);
我们已经完成了本节中的大部分代码。尽管如此,我们仍然缺少一些成员函数,例如更改文本颜色的函数。
回到void CListBoxEx::SetBkColor(COLORREF crBkColor,COLORREF crSelectedColor)
,并添加m_HBkColor=crSelectedColor;
以更改高亮颜色。
在头文件中,在public实体中添加以下原型:void SetTextColor(COLORREF crTextColor, COLORREF crHighlight);
。函数代码应为
void CListBoxEx::SetTextColor(COLORREF crTextColor, COLORREF crHighlight)
{
m_crTextClr=crTextColor;
m_crTextHlt=crHighlight;
Invalidate();
}
最后,我们需要能够更改位图的尺寸。为此声明适当的原型
void CListBoxEx::SetBMPSize(int Height, int Width) { m_BmpHeight=Height; m_BmpWidth=Width; Invalidate(); }
完成!是时候测试了。
回到ListDemoDlg.cpp的OnInitialUpdate()
,删除m_DemoList.SetBkColor(RGB(0,0,128));
。现在添加以下内容:
m_DemoList.SetBkColor(RGB(0,0,128),RGB(190,0,0)); m_DemoList.SetBMPSize(16,30); m_DemoList.SetTextColor(RGB(0,255,10),RGB(255,255,0)); m_DemoList.SetItemHeight(17); m_DemoList.AddString("Hey World"); m_DemoList.AddItem (IDB_COOL,"Hello World!"); m_DemoList.AddItem (NO_BMP_ITEM,"Hi World!"); m_DemoList.InsertItem(2,BLANK_BMP, "Greetings");
这检查了我们迄今为止制造的每个函数。IDIDB_COOL
是我创建的一个位图。其宽度为30像素,高度为16像素。这是我创建的图片(不具创意,但对我们的例子有效):。
如果您运行它,当它具有焦点且鼠标悬停在上面时,您将得到以下结果:
项是排序的,除非您使用InsertItem
。如果您想创建自己的排序算法,您应该重写CompareItem
。
我们现在可以继续进行最后一部分,滚动条。
4. 滚动条
为了简化起见,我们将要制作的滚动条将是静态的,无论是否需要始终显示。我不认为我们使用了正确的术语,因为它们没有滚动条,但谁在乎呢?正如我们所知,我们必须绘制它们。然而,问题是如何做到这一点,使其位于列表框的矩形内,并且不覆盖任何项。有一个简单的解决方案,我们可以调整客户区的尺寸。这可以通过接收WM_NCCALCSIZE
消息来实现。为此添加一个函数,我们将得到void CListBoxEx::OnNcCalcSize(BOOL bCalcValidRects, NCCALCSIZE_PARAMS FAR* lpncsp) { // TODO: Add your message handler code here and/or call default CListBox::OnNcCalcSize(bCalcValidRects, lpncsp); }
参数lpncsp
包含三个矩形。第一个(rgrc[0]
)是客户矩形。其他矩形在大多数情况下用处不大。假设我们希望一个滚动条的高度为16像素,我们将添加
lpncsp->rgrc[0].top += 16; //Top lpncsp->rgrc[0].bottom -= 16; //Bottom
在大多数情况下,此函数不会自动调用。我们将调用SetWindowsPos
,以便发送WM_NCCALCSIZE
。只有当我们向该函数传递SWP_FRAMECHANGED
标志时,它才会起作用。由于滚动条是非客户区域的一部分,我们将添加一个WM_NCPAINT
的处理程序。当非客户区域需要绘制时,将执行此处理程序。我们也应该绘制边框。SetWindowPos
仅在非客户区域首次绘制时调用。
void CListBoxEx::OnNcPaint() { // TODO: Add your message handler code here static BOOL before=FALSE; if (!before) { //If first time, the OnNcCalcSize function will be called SetWindowPos(NULL,0,0,0,0, SWP_FRAMECHANGED|SWP_NOMOVE|SWP_NOSIZE); before=TRUE; } DrawBorders(); // Do not call CListBox::OnNcPaint() for painting messages }
现在是时候创建一个受保护的函数来绘制滚动条了:void DrawScrolls(UINT WhichOne, UINT State);
。如您所见,它有两个参数。第一个指定绘制哪个滚动条(向下或向上),第二个指定状态,如按下。为了简化,让我们在头文件中#define一些东西。您将在头文件中看到类似这样的内容:
// ListBoxEx.h : header file // #define NO_BMP_ITEM 0 #define BLANK_BMP 1 #define SC_UP 2 //Up scroll #define SC_DOWN 3 //Down Scroll #define SC_NORMAL NULL //Normal scroll #define SC_PRESSED DFCS_PUSHED //The scroll is pressed #define SC_DISABLED DFCS_INACTIVE //The scroll is disabled ///////////////////////////////////////////////////////////////////////////// // CListBoxEx window
DFCS_PUSHED
之类的内容是我们将在下一个函数中使用的状态:DrawFrameControl
。您可能会认为SC_PRESSED
是具有不同名称的克隆。现在是DrawScrolls的实现。
void CListBoxEx::DrawScrolls(UINT WhichOne, UINT State) { CDC *pDC=GetDC(); CRect rect; GetClientRect(rect); //Gets the dimensions //If the window is not enabled, set state to disabled if (!IsWindowEnabled())State=SC_DISABLED; //Expands the so that it does not draw over the borders rect.left-=GetSystemMetrics(SM_CYEDGE); rect.right+=GetSystemMetrics(SM_CXEDGE); if (WhichOne==SC_UP){ //The one to draw is the up one //Calculates the rect of the up scroll rect.bottom=rect.top-GetSystemMetrics(SM_CXEDGE); rect.top=rect.top-16-GetSystemMetrics(SM_CXEDGE); //Draws the scroll up pDC->DrawFrameControl(rect,DFC_SCROLL,State|DFCS_SCROLLUP); } else{ //Needs to draw down rect.top=rect.bottom+GetSystemMetrics(SM_CXEDGE);; rect.bottom=rect.bottom+16+GetSystemMetrics(SM_CXEDGE); pDC->DrawFrameControl(rect,DFC_SCROLL,State|DFCS_SCROLLDOWN); } ReleaseDC(pDC); }
DrawFrameControl
通常用于绘制所有者绘制控件中的滚动条和按钮。现在回到OnNcPaint
并在(if语句之外)添加:
DrawScrolls(SC_UP,SC_NORMAL); DrawScrolls(SC_DOWN,SC_NORMAL);
我们应该得到
现在我们必须使其滚动并在按下时更改滚动条的外观。由于它不是边框或默认滚动条,非客户消息(如NcLButtonDown
)默认情况下将不起作用。因此,我们必须对代码做一些小的调整。尽管大多数不起作用,但WM_NCHITTEST
(表示鼠标移动)将在鼠标进入滚动条时发送。因此,为此添加一个消息处理程序。
返回值表示鼠标所在的位置。为了使用OnNcLButtonDown
来查看左键何时按下,我们将伪造鼠标位于原始滚动条上。如果它位于顶部滚动条上,我们将返回HTVSCROLL
;如果位于底部,则返回HTHSCROLL
。在此函数中,鼠标位置是相对于父窗口而不是客户区。因此,我们必须进行转换。
UINT CListBoxEx::OnNcHitTest(CPoint point) { // TODO: Add your message handler code here and/or call default CRect rect,top,bottom; //Gets the windows rect, relative to the parent, so rect.left and //rect.top might not be 0. GetWindowRect(rect); ScreenToClient(rect); //Converts the rect to the client //Calculates the rect of the bottom and top scrolls top=bottom=rect; top.bottom=rect.top+16; bottom.top=rect.bottom-16; //Obtains where the mouse is UINT where = CListBox::OnNcHitTest(point); //Converts the point so its relative to the client area ScreenToClient(&point); if (where == HTNOWHERE) //If mouse is not in a place it recognizes if (top.PtInRect(point)) //Check to see if the mouse is on the top where = HTVSCROLL; else if (bottom.PtInRect(point)) //Check to see if its on the bottom where=HTHSCROLL; return where; //Returns where it is }
为WM_NCLBUTTONDOWN
添加一个处理程序。现在当鼠标按下滚动条时就会调用它,我们可以检查nHitTest
来查看按下了哪个滚动条。
我们将使用SendMessage
函数来伪造真实垂直滚动条的点击。我们也可以使用ScrollWindow
。如果您在浏览器中点击并按住滚动条的左键,您会注意到只要不释放按钮,它就会滚动。为了实现这一点,我们将使用一个定时器,该定时器每100毫秒滚动一次。实际上,在大多数系统上,将是110毫秒。这是因为Windows工作的硬件定时器每54.9秒(约)滴答一次。因此,Windows会将传递给SetTimer的值四舍五入到下一个55毫秒的倍数。
我们将得到
void CListBoxEx::OnNcLButtonDown(UINT nHitTest, CPoint point) { // TODO: Add your message handler code here and/or call default if (nHitTest==HTVSCROLL) //Up scroll Pressed { DrawScrolls(SC_UP,SC_PRESSED); //Scroll up 1 line SendMessage(WM_VSCROLL,MAKEWPARAM(SB_LINEUP,0),0); SetTimer(1,100,NULL); //Sets the timer ID 1 } else if (nHitTest==HTHSCROLL) //Down scroll Pressed { DrawScrolls(SC_DOWN,SC_PRESSED); //Scroll down 1 line SendMessage(WM_VSCROLL,MAKEWPARAM(SB_LINEDOWN,0),0); SetTimer(2,100,NULL); //Sets the timer ID 2 } CListBox::OnNcLButtonDown(nHitTest, point); }
当然,我们现在必须添加一个WM_TIMER
函数。我们知道,如果ID是1,我们将向上滚动。否则,向下滚动。此外,如果调用定时器时,左键不再按下,我们必须销毁定时器并重绘普通滚动条。
void CListBoxEx::OnTimer(UINT nIDEvent) { // TODO: Add your message handler code here and/or call default //Gets the state of the left button to see if it is pressed short result=GetKeyState(VK_LBUTTON); if (nIDEvent==1){ //Up timer //If it returns negative then it is pressed if (result<0){ SendMessage(WM_VSCROLL,MAKEWPARAM(SB_LINEUP,0),0); } else { //No longer pressed KillTimer(1); DrawScrolls(SC_UP,SC_NORMAL); } } else { //Down timer //If it returns negative then it is pressed if (result<0){ SendMessage(WM_VSCROLL,MAKEWPARAM(SB_LINEDOWN,0),0); } else { KillTimer(2); DrawScrolls(SC_DOWN,SC_NORMAL); } } CListBox::OnTimer(nIDEvent); }
最后,我们完成了CListBoxEx
。
我们现在应该检查滚动条是否正常工作。因此,让我们添加更多项。您可以在ListDemoDlg.cpp的InitialUpdate
中执行一个循环来测试它,或者添加更多内容。我修改了InitialUpdate
以重复它们。
m_DemoList.SetBkColor(RGB(0,0,128),RGB(190,0,0)); m_DemoList.SetBMPSize(16,30); m_DemoList.SetTextColor(RGB(0,255,10),RGB(255,255,0)); m_DemoList.SetItemHeight(17); for (int i=0;i<=5;i++){ m_DemoList.AddString("Hey World"); m_DemoList.AddItem (IDB_COOL,"Hello World!"); m_DemoList.AddItem (NO_BMP_ITEM,"Hi World!"); m_DemoList.InsertItem(2,BLANK_BMP, "Greetings"); m_DemoList.AddItem (IDB_COOL,"Vacation's Great!"); }
在点击向下滚动条时,我们应该得到类似这样的结果:(*我取消了Sort属性)
您应该记住,在整个代码中,我们添加了一些行以备控件禁用。回到OnEnable
并添加:
//SC_NORMAL will be changed to SC_DISABLED if the window is disabled DrawScrolls(SC_UP,SC_NORMAL); DrawScrolls(SC_DOWN,SC_NORMAL);
我们必须禁用该控件并查看它是否正常工作。我们将得到这个:
结论
现在代码已完成,通过将源文件添加到项目中,可以轻松地将其集成到其他项目中。然后,您应该包含头文件,并且不创建CListbox
变量,而是创建一个CListBoxEx
。这必须手动完成,因为新类不会出现在ClassWizard中。不要失去希望,有一个使用ClassWizard的技巧。关闭项目,在其文件夹中,您将找到一个带有CLW扩展名的文件。删除它并重新打开项目。现在转到ClassWizard,您将被要求重建它。然后,您将能够使用此更新版本的向导,并添加本文开头所示的列表框变量。
正如您所见,子类化可能不像最初看起来那样困难。它只需要一点知识、耐心和实践。有时,在子类化时,您需要依赖其他工具。例如,在许多情况下,您可能不确定某个部分会收到哪些消息。为此,您可以使用“工具”菜单中的Spy++。其他时候,TRACE宏对于捕捉隐藏在大多数代码中的小错误非常有用。现在,您应该能够将这些知识应用于其他控件,因为框架几乎是相同的。
编程愉快。