使用自定义绘制定制 CSliderCtrl 的外观






4.88/5 (59投票s)
2004年12月7日
24分钟阅读

249207

10451
如何使用自定义绘图来更改轨迹条控件和 CSliderCtrl 的外观。
- 下载颜色选择器演示项目 - 57.4 Kb (包含一个发布版可执行文件,无需编译即可运行)
- 仅 CCustomDrawSliderCtrl 源代码 - 4.99 Kb
目录
引言
“自定义绘图”是 Microsoft 所有通用控件共享的一个功能,它允许您在控件绘制自身时插入自己的代码。通过这种方式,您可以自定义控件的外观,根据您的需求和喜好进行更改。
关于自定义绘图,在各种不同控件的背景下已经有很多精彩的文章。其中最好的一篇是 Michael Dunn 的“使用自定义绘图在列表控件中做些有趣的事情”,它演示了如何驯服一个难以驾驭的自定义绘图接口来定制 CListCtrl
(一个常见的列表视图控件)的外观。其他示例和很好的解释可以在 MSDN 上找到,从文章“使用自定义绘图自定义控件的外观”开始。
“自定义绘图”与“自绘”不是一回事。自定义绘图通知会自动发送(由您来捕获通知并正确处理),而对于“自绘”,您需要设置一个样式标志(例如列表视图控件的 LVS_OWNERDRAWFIXED
),然后控件才会向您发送 WM_DRAWITEM
消息。在自定义绘图中,控件几乎完全自行绘制,您的代码只是在这里那里自定义其外观,而对于自绘,您必须绘制所有内容,通常在 OnDrawItem
处理程序中,即使您只想进行最微小的更改。自定义绘图仅适用于通用控件(如标题控件、列表视图控件、Rebar 控件、工具栏控件、工具提示控件、轨迹条控件和树视图控件),但不适用于标准(和较旧的)Windows 控件(如按钮控件、组合框控件、列表框控件、进度条控件、静态控件和选项卡控件);另一方面,自绘适用于所有控件。
您可以将自定义绘图视为一种“轻量级”的自绘(Michael Dunn 的话),其中控件完成了大部分工作,而您只修改了一小部分。在事物的层次结构中,如果您喜欢控件的外观和功能,那么就“按原样”使用该控件;如果您喜欢控件的大部分外观和功能,那么就使用自定义绘图;如果您不喜欢控件的外观但喜欢其功能,那么就使用自绘;如果您既不喜欢它的外观也不喜欢它的功能,那么就编写自己的自定义控件。
这里显示的代码是使用 VC++ 6.0 版 SP5 在 WinXP 上编写的,并使用了通用控件 DLL 的 5.80 版。但是,所有代码都在 Win95、Win98 和 Win2000 上进行了测试,并且在所有这些系统上都能正常工作。此外,这里演示的功能相当基础,因此您应该能够轻松地在您的设置上运行它。但是,您需要一个至少是 2003 年 2 月的平台 SDK,在撰写本文时(2004 年 12 月),这是最新的平台 SDK。有关获取平台 SDK 的详细信息,请参阅 MSDN 的“平台 SDK:简介”。如果您没有最新版本,下载的可执行文件仍然可以运行,但您将无法编译项目。
最后,为文章的篇幅致歉。我最初并没有打算写这么长,但在撰写过程中它不断增长。如果您只对阅读上面所示的最终结果感兴趣,请跳到“一个更复杂的示例:在轨迹条通道中进行高亮显示和绘图”。
一个简单示例
我刚开始开发上面所示的颜色选择对话框时,收到一个使用 CSliderCtrl
指定文档边距(页眉和页脚,左和右)的人提出的问题。他最终得到了一个像这样的视图,其中四个 CSliderCtrl
用于显示边距。顺便说一句,尽管这些控件看起来像滑块,并且 MFC 将它们封装在 CSliderCtrl
类中,但在官方的 Microsoft 术语中,它们被称为“轨迹条控件”;我们将它们互换地称为“滑块控件”或 CSliderCtrl
或“轨迹条控件”。
我突然想到,对于这个应用程序,以及(更重要的是)我的颜色选择器对话框,如果能给用户一些基于颜色的反馈,让他们确切地知道正在控制什么,那将是极好的。就像这样,每个轨迹条都使用不同颜色的滑块来显示正在控制哪个边距。
- 下载演示项目 - 31.4 Kb(包含一个发布版可执行文件,无需编译即可运行)
这 оказалось出奇地容易编码。我从 CSliderCtrl
派生了一个类,然后为反射的 NM_CUSTOMDRAW
通知消息添加了一个消息处理程序。VC++ 6.0 会自动为您从 Class Wizard 或 Class View 执行此操作:单击添加消息处理程序,然后选择 =NM_CUSTOMDRAW
;“=”等号很重要,因为它告诉 VC++ 将反射处理程序添加到控件的类中,而不是父类的类中。您也可以通过在控件的消息映射中添加 ON_NOTIFY_REFLECT
宏,然后向头文件添加原型函数,向 .cpp 文件添加实际函数体(Class Wizard 会自动为您完成此操作)来手动执行此操作。有关消息反射的详细信息,请参阅 MSDN 的“TN062:Windows 控件的消息反射”。然后我添加了一个名为 SetColor(COLORREF cr)
的简单函数来设置滑块的颜色。这是整个类的代码,考虑到它的短长度,您可以看到为什么我说它“出奇地容易”。我们将在下面进一步讨论代码。
// XSliderCtrl.cpp : implementation file // #include "stdafx.h" #include "TrackTest.h" #include "XSliderCtrl.h" #ifdef _DEBUG #define new DEBUG_NEW #undef THIS_FILE static char THIS_FILE[] = __FILE__; #endif ///////////////////////////////////////////////////////////////////////////// // CXSliderCtrl CXSliderCtrl::CXSliderCtrl() { } CXSliderCtrl::~CXSliderCtrl() { } BEGIN_MESSAGE_MAP(CXSliderCtrl, CSliderCtrl) //{{AFX_MSG_MAP(CXSliderCtrl) // NOTE - the ClassWizard will add and remove mapping macros here. ON_NOTIFY_REFLECT( NM_CUSTOMDRAW, OnCustomDraw ) //}}AFX_MSG_MAP END_MESSAGE_MAP() ///////////////////////////////////////////////////////////////////////////// // CXSliderCtrl message handlers void CXSliderCtrl::SetColor(COLORREF cr) { m_crThumb = cr; m_Brush.CreateSolidBrush( cr ); m_Pen.CreatePen( PS_SOLID, 1, RGB(128,128,128) ); // dark gray } afx_msg void CXSliderCtrl::OnCustomDraw ( NMHDR * pNotifyStruct, LRESULT* result ) { NMCUSTOMDRAW nmcd = *(LPNMCUSTOMDRAW)pNotifyStruct; if ( nmcd.dwDrawStage == CDDS_PREPAINT ) { // return CDRF_NOTIFYITEMDRAW so that we will get subsequent // CDDS_ITEMPREPAINT notifications *result = CDRF_NOTIFYITEMDRAW ; return; } else if ( nmcd.dwDrawStage == CDDS_ITEMPREPAINT ) { if ( nmcd.dwItemSpec == TBCD_THUMB ) { CDC* pDC = CDC::FromHandle( nmcd.hdc ); pDC->SelectObject( m_Brush ); pDC->SelectObject( m_Pen ); pDC->Ellipse( &(nmcd.rc) ); pDC->Detach(); *result = CDRF_SKIPDEFAULT; } } }
那么,这里发生了什么?
首先,驾驭自定义绘图的关键是响应控件发送的第一个 NM_CUSTOMDRAW
通知。基本上,在每个绘制周期的开始,控件都会发送一个 NM_CUSTOMDRAW
通知。如果该通知被忽略(99.9% 的情况下是这样),那么控件将不会发送任何进一步的 NM_CUSTOMDRAW
通知(直到下一个绘制周期开始),并且会简单地自行绘制控件,呈现标准外观。
CDRF_NOTIFYITEMDRAW
,那么控件将在每个绘制阶段继续发送 NM_CUSTOMDRAW
通知。更具体地说,通用控件会分阶段(就像您自己可能已经编程过它一样)绘制自己,并且每次绘制一个项目(或一部分)。例如,轨迹条控件以三个独立的项绘制自己:轨迹条控件的滑块沿其滑动的通道(由 TBCD_CHANNEL
标识符标识)、轨迹条控件的滑块(即用户拖动的部分,由 TBCD_THUMB
标识符标识)以及沿轨迹条控件边缘出现的增量刻度标记(由 TBCD_TICS
标识符标识)。在这种情况下,我们有兴趣改变滑块的外观,因此我们在 CDDS_PREPAINT
阶段返回 CDRF_NOTIFYITEMDRAW
,以便在随后的绘制阶段继续接收 NM_CUSTOMDRAW
通知。然后我们等待直到收到一个 NM_CUSTOMDRAW
通知,表明已经到达绘制滑块的预绘制阶段。此时,我们介入并插入我们自己的绘图代码。我们不让控件绘制自己的滑块,而是在滑块本应占据的矩形中绘制一个彩色椭圆,然后返回 CDRF_SKIPDEFAULT
值,告诉控件不要自行进行任何进一步的绘制(否则会导致标准滑块绘制在我们绘制的滑块之上)。
我们执行绘图所需的所有信息都包含在作为第一个参数传递到 OnCustomDraw
消息处理程序的 NMCUSTOMDRAW
结构中。(参见脚注 1。)这是从 MSDN 的“NMCUSTOMDRAW 结构”中获取的 NMCUSTOMDRAW
结构的外观。
typedef struct tagNMCUSTOMDRAWINFO { NMHDR hdr; DWORD dwDrawStage; HDC hdc; RECT rc; DWORD_PTR dwItemSpec; UINT uItemState; LPARAM lItemlParam; } NMCUSTOMDRAW, *LPNMCUSTOMDRAW;
以下是每个字段的含义
成员名称 | 含义 |
hdr |
NMHDR 结构,包含有关此通知消息的附加信息。 |
dwDrawStage |
当前绘制阶段。这可以是 CDDS_xxxxx 形式的全局值,表示整个控件的绘制阶段(预擦除、后绘制等),也可以是 CDDS_ITEMxxxxx 形式的特定项目值,表示控件中每个项目的绘制阶段(例如 TBCD_THUMB 项目的预擦除阶段,或 TBCD_CHANNEL 项目的后绘制阶段)。 |
hdc |
控件设备上下文的句柄。使用此 HDC 执行任何 GDI 函数。 |
rc |
RECT 结构,描述要绘制区域的边界矩形。 |
dwItemSpec |
项目标识符,在轨迹条控件的情况下是 TBCD_CHANNEL 、TBCD_THUMB 或 TBCD_TICS 标识符之一。 |
uItemState |
当前项目状态。此值是 CDIS_xxxxx 形式的值的组合,表示项目的状态(例如已选中、灰色、已聚焦或已选择)。 |
lItemlParam |
应用程序定义的项目数据,这是所有通用控件的一个功能。 |
因此,我们有一个可以绘制的 HDC
设备上下文,以及一个告诉我们绘制位置的 RECT
。顺便说一句,NMCUSTOMDRAW
结构还告诉我们绘制阶段和正在绘制的项目,这在上面的讨论中我没有提到(所以我犯了“本末倒置”的解释错误 <g>)。
一个更复杂的示例:拖动期间的用户反馈
包括创建自定义绘图 CSliderCtrl 的分步指南
在我看来,这么少的代码就能实现如此多的自定义,这很不错,我希望您能受到鼓励,继续深入一点。
我注意到使用该控件的第一个问题是,当用户拖动滑块时,滑块没有“亮起”。相比之下,标准控件的滑块在拖动时会亮起。
![]() |
![]() |
在标准轨迹条控件上,滑块在拖动时会“亮起”。 | 使用上述代码,滑块在拖动时不会“亮起”。 |
此外,在所有情况下都用深灰色绘制滑块边框似乎也很随意。尽管当滑块颜色相对较深时(如上面所示的深蓝色滑块),它看起来很好,但当滑块颜色较浅时,它看起来就不那么好了。
![]() |
深灰色边框在浅色滑块周围显得太暗。 |
第一个问题通过检测用户何时拖动滑块(使用 NMCUSTOMDRAW
结构的 uItemState
成员——参见上文)并用图案画刷而不是实心画刷绘制滑块来解决。图案画刷的位模式可以使用 ucc801 发布的一个名为“带 C++ 源代码的酷 GDI 图案画刷工具”的工具创建。
第二个问题可以用我在“Shell 轻量级实用程序函数”的“Shell 调色板处理函数”标题下找到的三个非常巧妙的颜色坐标函数来解决。这些函数是 ColorAdjustLuma
、ColorHLSToRGB
和 ColorRGBToHLS
,MSDN 对它们的描述如下:
ColorAdjustLuma |
更改 RGB 值的亮度。色调和饱和度不受影响。 |
ColorHLSToRGB |
将颜色从色调-亮度-饱和度 (HLS) 转换为 RGB 格式。 |
ColorRGBToHLS |
将颜色从 RGB 转换为色调-亮度-饱和度 (HLS) 格式。 |
基本上,这些函数在标准 RGB 色彩空间和 HLS(色调-亮度-饱和度)色彩空间之间进行转换,并且还独立于调整色调和饱和度来调整亮度。简单来说,RGB 只是众多可能不同的色彩空间(或色彩坐标系统)之一。其他色彩空间的例子包括 CIELab、CIELUV、Jch、HLS 和 CIEXYZ。每个都有其优点和缺点。RGB 在处理彩色显示器时非常有用,因为每个坐标都与显示器上每个像素的红色、绿色或蓝色颜色元素匹配。但是,当您试图以直观或感知视觉方式“思考”颜色时,它就非常糟糕。(例如,请参阅脚注 2。)原因在于 RGB 不是一个很好的感知色彩空间,而其他色彩空间是。HLS 是感知色彩空间的一个例子(尽管可能存在更好的色彩空间)。在 HLS 空间中,如果您想要更亮/更浅的颜色,只需增加亮度(“L”值)。您想要一种更鲜艳的颜色,更接近纯色而不是灰色吗?增加饱和度(“S”值)。想要一种在彩虹上发生偏移的颜色,使其更偏向紫色或红色吗?增加或减少色调角度(“H”值)。它更直观,更接近我们对颜色的思考方式。
我们可以使用上述三个 RGB 到 HLS 颜色函数(实际上,我们只需要第一个)轻松创建更亮和更暗的颜色,我们在这里这样做是为了创建我们用于图案画刷的强调色,以及我们用于滑块边框的阴影色。
编写自定义绘图 CSliderCtrl 的分步指南
这次我们一起来编写代码,针对一个对话框(而不是像第一个示例那样是视图)。这里的步骤适用于 VC++ 6.0 版,但如果您使用其他 IDE,应该会看到相似之处。
创建一个新的基于对话框的项目,我将其命名为“HiliteSlider”,并接受所有默认设置。从 Class Wizard 中,添加一个派生自 CSliderCtrl
的新类,我将其命名为 CCDSliderCtrl
。仍在 Class Wizard 中,从“消息映射”选项卡中,为 =NM_CUSTOMDRAW
消息添加一个新的处理程序,我将接受(几乎)默认名称 OnCustomDraw
。添加函数后,单击“确定”关闭 Class Wizard。现在将该函数留空;我们稍后会填充这个骨架。
您应该正在查看主对话框资源的模板(如果不是,请切换到它)。删除“TODO: Place controls here”提醒,并从控件工具箱中插入一个轨迹条控件。接受默认的资源 ID IDC_SLIDER1
,并将样式更改为垂直方向的轨迹条。您的对话框资源应该看起来像这样:
再次打开类向导,并转到主对话框类的“成员变量”选项卡。双击 IDC_SLIDER1
添加一个成员变量。为变量命名,选择“控件”(而不是“值”)作为类别,然后单击下拉框选择您的 CCDSliderCtrl
类。
点击所有“确定”以接受新变量并关闭类向导。您需要将 CCDSliderCtrl
类的 .h 头文件添加到主对话框的头文件中(正如类向导的警告所提示的)。打开 CHiliteSliderDlg
类的头文件(其名称应为 HiliteSliderDlg.h)并添加以下行:
#include "CDSliderCtrl.h"
到顶部附近。此时,如果您愿意,项目将编译并运行,但还有更多工作要做。
将以下五个 protected
成员变量添加到 CCDSliderCtrl
类中:
变量类型 | 变量名 |
COLORREF |
m_crPrimary , m_crShadow , m_crHilite |
CBrush |
m_normalBrush , m_focusBrush |
添加一个具有以下代码的 public
SetPrimaryColor
成员函数,该函数派生高亮色和阴影色,并创建一个实心画刷和图案画刷:
void CCDSliderCtrl::SetPrimaryColor(COLORREF cr) { // sets primary color of control, and derives shadow and hilite colors // also initializes brushes that are used in custom draw functions m_crPrimary = cr; // get hilite and shadow colors m_crHilite = ::ColorAdjustLuma( cr, 500, TRUE ); // increase by 50% m_crShadow = ::ColorAdjustLuma( cr, -333, TRUE ); // decrease by 33.3% // create normal (solid) brush if ( m_normalBrush.m_hObject ) m_normalBrush.DeleteObject(); m_normalBrush.CreateSolidBrush( cr ); // create a hatch-patterned pixel pattern for patterned brush // (used when thumb has focus/is selected) WORD bitsBrush1[8] = { 0x0055,0x00aa,0x0055,0x00aa,0x0055,0x00aa,0x0055,0x00aa }; CBitmap bm; bm.CreateBitmap( 8, 8, 1, 1, bitsBrush1); LOGBRUSH logBrush; logBrush.lbStyle = BS_PATTERN; logBrush.lbHatch = (int) bm.GetSafeHandle(); logBrush.lbColor = 0; // ignored anyway; must set DC background and text colors if ( m_focusBrush.m_hObject ) m_focusBrush.DeleteObject(); m_focusBrush.CreateBrushIndirect(&logBrush); }
这使用了上面描述的 shell 轻量级 API 中的 ColorAdjustLumina
函数,因此我们需要包含其头文件并链接到 .lib 文件。在 CDSliderCtrl.cpp 文件的顶部插入以下内容:
// the following are needed for the HLS to RGB // (and RGB to HLS) conversion functions #include <shlwapi.h> #pragma comment( lib, "shlwapi.lib" )
我们快完成了。剩下要做的就是插入自定义绘图代码,这当然是整个练习的目的。将以下代码插入到类向导已经为我们创建的骨架中:
void CCDSliderCtrl::OnCustomDraw(NMHDR* pNMHDR, LRESULT* pResult) { NMCUSTOMDRAW nmcd = *(LPNMCUSTOMDRAW)pNMHDR; *pResult = 0; if ( nmcd.dwDrawStage == CDDS_PREPAINT ) { // return CDRF_NOTIFYITEMDRAW so that we will // get subsequent CDDS_ITEMPREPAINT notifications *pResult = CDRF_NOTIFYITEMDRAW ; return; } else if ( nmcd.dwDrawStage == CDDS_ITEMPREPAINT ) { if ( nmcd.dwItemSpec == TBCD_THUMB ) { CDC* pDC = CDC::FromHandle( nmcd.hdc ); int iSaveDC = pDC->SaveDC(); CBrush* pB = &m_normalBrush; // if thumb is selected/focussed, switch brushes if ( nmcd.uItemState && CDIS_FOCUS ) { pB = &m_focusBrush; pDC->SetBrushOrg( nmcd.rc.right%8, nmcd.rc.top%8 ); pDC->SetBkColor( m_crPrimary ); pDC->SetTextColor( m_crHilite ); } pDC->SelectObject( *pB ); CPen penShadow; penShadow.CreatePen( PS_SOLID, 1, m_crShadow ); pDC->SelectObject( penShadow ); pDC->Ellipse( &(nmcd.rc) ); pDC->RestoreDC( iSaveDC ); pDC->Detach(); *pResult = CDRF_SKIPDEFAULT; } } }
如前所述,这段代码截获 NM_CUSTOMDRAW
通知并返回 CDRF_NOTIFYITEMDRAW
,以便我们能收到后续的 NM_CUSTOMDRAW
通知。我们忽略所有这些通知,直到我们收到一个指示滑块已准备好绘制的通知(即 nmcd.dwItemSpec == TBCD_THUMB
)。最初,我们选择一个实心画刷来绘制滑块,但如果用户已选择滑块进行拖动(即 nmcd.uItemState && CDIS_FOCUS
),那么我们切换到图案画刷,并正确设置其原点。然后我们用我们决定使用的任何画刷绘制椭圆,并返回 CDRF_SKIPDEFAULT
,告诉控件不要在我们刚刚绘制的内容上进行绘制。非常简单 <g>。
最后一步,在 OnInitDialog
中插入以下代码以选择滑块的主色:
BOOL CHiliteSliderDlg::OnInitDialog() { CDialog::OnInitDialog(); // .. other stuff added by the AppWizard .. // TODO: Add extra initialization here m_ctlSlider.SetPrimaryColor( RGB( 167, 50, 205 ) ); // similar to Dark Orchid return TRUE; // return TRUE unless you set the focus to a control }
它将主色设置为深紫罗兰色,一种紫罗兰色。编译并运行,您将得到以下对话框。请注意,滑块现在由一个漂亮的互补阴影色边框(类似于主色但更深)。还要注意,滑块现在在用户拖动时会“亮起”。
- 下载演示项目 - 22.7 Kb(包含一个发布版可执行文件,无需编译即可运行)
- 仅 CCDSliderCtrl 源代码 - 2.17 Kb
CCDSliderCtrl
如此简单,您可能不想在其他项目中使用它,因此我这里不解释如何在其他项目中使用它。但我将解释自定义绘制滑块的最终版本(请参阅下一节),因此如果您确实想在项目中使用 CCDSliderCtrl
的源代码,请参阅下面“如何使用”部分中的解释。使用说明是相似的。
绘制不同形状的滑块
如上所示,绘制椭圆形滑块很好,但在某些情况下,您可能喜欢与标准控件滑块形状更相似的形状。在标准轨迹条控件中,当刻度线绘制在两侧时,滑块是矩形的;当刻度线在左侧或右侧时,滑块是尖矩形的。
对我来说,我更喜欢菱形滑块,无论刻度线配置如何。使用以下代码很容易绘制菱形:
void CCDSliderCtrlDiamond::OnCustomDraw(NMHDR* pNMHDR, LRESULT* pResult) { NMCUSTOMDRAW nmcd = *(LPNMCUSTOMDRAW)pNMHDR; // .. // same as above: return CDRF_NOTIFYITEMDRAW in the CDDS_PREPAINT // stage, and then later when dwItemSpec == TBCD_THUMB, select // desired pen and brush. else if ( nmcd.dwDrawStage == CDDS_ITEMPREPAINT ) { if ( nmcd.dwItemSpec == TBCD_THUMB ) { // .. now draw a diamond-shaped thumb int xx, yy, dx, dy, cx, cy; xx = nmcd.rc.left; yy = nmcd.rc.top; dx = 2; dy = 2; cx = nmcd.rc.right - xx - 1; cy = nmcd.rc.bottom - yy - 1; POINT pts[8] = { {xx+dx, yy}, {xx, yy+dy}, {xx, yy+cy-dy}, {xx+dx, yy+cy}, {xx+cx-dx, yy+cy}, {xx+cx, yy+cy-dy}, {xx+cx, yy+dy}, {xx+cx-dx, yy} }; pDC->Polygon( pts, 8 ); // diamond shape pDC->RestoreDC( iSaveDC ); pDC->Detach(); *pResult = CDRF_SKIPDEFAULT; } } }
这段代码生成了一个菱形滑块,无论滑块是垂直还是水平方向,都看起来很好。在这里,滑块的主色设置为 RGB( 20, 179, 97 )
,类似于春绿色(使用 CCDSliderCtrl::SetPrimaryColor
函数)。
尽管菱形滑块的源代码在上述可下载文件中提供,但生成此形状的代码也包含在上面主要的下载文件中。要获得菱形而不是椭圆形,请转到主对话框的头文件(即 HiliteSliderDlg.h 文件),然后通过更改注释来选择所需的滑块形状,方法是更改此内容:
CCDSliderCtrl m_ctlSlider;
// CCDSliderCtrlDiamond m_ctlSlider;
改为这样。
// CCDSliderCtrl m_ctlSlider;
CCDSliderCtrlDiamond m_ctlSlider;
一个更复杂的示例:在轨迹条通道中进行高亮显示和绘图
对于我的颜色选择器对话框,我希望滑块控件有额外两个小物件:一个颜色编码(并适当突出显示)的轨迹条通道,以及一对分别位于通道两端的彩色圆点。第一个旨在强调轨迹条控件与其所控制的颜色通道之间的连接性。第二个旨在强调为了增加或减少颜色通道的值,控件应该移动的方向。(参见脚注 3。)我的意思是这样的:
这意味着,是时候在一个新项目上进行绘制了:轨迹条控件的滑块沿其滑动的通道(由 TBCD_CHANNEL
标识符标识),而到目前为止,我们只在滑块本身上进行绘制。绘制本身是 GDI 功能的练习,我们稍后将讨论。
这也意味着是时候重新审视代码结构了,通过它我们可以捕获我们感兴趣的少数 NM_CUSTOMDRAW
通知,并忽略控件发送给我们的数十个其他通知。在我们之前的示例中,由于我们只在滑块上绘制,我们简单的 if
-then
-else
结构足以找到我们想要的 NM_CUSTOMDRAW
通知。但控件确实发送了数十个这样的通知,每个绘制阶段针对控件中的每个项目(即滑块、通道和刻度),以及针对整个控件本身的每个绘制阶段。我喜欢将各种 NM_CUSTOMDRAW
通知视为在一个矩阵中发送,我们需要从中筛选出我们想要的少数通知,并忽略其余的。这是一个表格,显示我的意思,并显示我们到目前为止捕获的 NM_CUSTOMDRAW
通知,以及我们需要捕获以获得我们想要的两个新功能的通知。
项目名称 → | 整体控制 | 通道TBCD_CHANNEL |
滑块TBCD_THUMB |
刻度线TBCD_TICS |
绘图阶段 ↓ | ||||
预绘制CDDS_PREPAINT (用于整体控件)或CDDS_ITEMPREPAINT (用于每个项目) |
到目前为止,我们已经返回 CDRF_NOTIFYITEMDRAW 以继续接收 NM_CUSTOMDRAW 通知。我们现在还必须返回 CDRF_NOTIFYPOSTPAINT 以获取整个控件的绘制后通知,以便我们能够绘制彩色圆点。 |
现在,我们需要在此处捕获并返回 CDRF_DODEFAULT | CDRF_NOTIFYPOSTPAINT ,以允许通道自行绘制,然后在 CDDS_ITEMPOSTPAINT 阶段给我们机会在其上绘制,以添加我们的高亮。 |
到目前为止,我们只在此阶段/项目组合上捕获,以按我们想要的方式绘制滑块,然后返回 CDRF_SKIPDEFAULT 以跳过控件的任何默认绘制。 |
|
预擦除CDDS_PREERASE (用于整体控件)或CDDS_ITEMPREERASE (用于每个项目) |
||||
后擦除CDDS_POSTERASE (用于整体控件)或CDDS_ITEMPOSTERASE (用于每个项目) |
||||
后绘制CDDS_POSTPAINT (用于整体控件)或CDDS_ITEMPOSTPAINT (用于每个项目) |
现在绘制彩色圆点并返回 CDRF_SKIPDEFAULT |
用高亮色和阴影色对通道的矩形进行颜色编码,并返回 CDRF_SKIPDEFAULT |
||
可能的项目状态 (uItemState ) |
CDIS_CHECKED , CDIS_DEFAULT , CDIS_DISABLED , CDIS_FOCUS , CDIS_GRAYED , CDIS_HOT , CDIS_INDETERMINATE , CDIS_MARKED , CDIS_SELECTED , 或 CDIS_SHOWKEYBOARDCUES | |||
可能的返回值 | CDRF_DODEFAULT , CDRF_NOTIFYITEMDRAW , CDRF_NOTIFYPOSTERASE , CDRF_NOTIFYPOSTPAINT , CDRF_NOTIFYSUBITEMDRAW , CDRF_NEWFONT , 或 CDRF_SKIPDEFAULT |
有这么多种可能性,如果我们继续依赖 if
-then
-else
结构,代码很快就会变得难以管理。我决定改为使用 switch
-case
结构,这让我更容易地可视化上述表格(以及可能的 NM_CUSTOMDRAW
通知)。
至于绘图本身,就像我说的,这是一项 GDI 绘图功能的练习。彩色圆点指示器通常很简单,但由于 CSliderCtrl
可以朝四个不同方向(即垂直或水平,以及上/左或下/右)定位,我们需要测试这四个方向,以便将圆点绘制到正确的位置。下面是一个摘录,它定位了上下圆点(在代码中分别称为 rrcFirst
和 rrcLast
)的边界矩形。定位矩形后,我们只需使用所需的画笔和画刷在其中绘制圆形。
// ... inside the OnCustomDraw function ... // ... lots of other stuff before here ... if ( dwStyle & TBS_VERT ) { if ( dwStyle & TBS_LEFT ) { rrcFirst = CRect( rClient.right-cx, 1, rClient.right-1, cx ); rrcLast = CRect( rClient.right-cx, rClient.bottom-cx, rClient.right-1, rClient.bottom-1 ); } else { rrcFirst = CRect( 1, 1, cx, cx ); rrcLast = CRect( 1, rClient.bottom-cx, cx, rClient.bottom-1 ); } } else { if ( dwStyle & TBS_TOP ) { rrcFirst = CRect( 1, rClient.bottom-cx, cx, rClient.bottom-1 ); rrcLast = CRect( rClient.right-cx, rClient.bottom-cx, rClient.right-1, rClient.bottom-1 ); } else { rrcFirst = CRect( 1, 1, cx, cx ); rrcLast = CRect( rClient.right-cx, 1, rClient.right-1, cx ); } }
通道矩形的颜色编码旨在模仿控件本身赋予通道的高亮显示。这里是通道的特写,与控件本身给出的高亮显示以及我们试图实现的高亮显示并排显示:
这种高亮显示正是 CDC::Draw3dRect()
函数所设计的。要在我们的 OnCustomDraw()
函数中使用它,我们只需提供一些从控件主色派生出来的额外颜色(例如中间阴影色)。绘图代码结果非常简单:
{ CDC* pDC = CDC::FromHandle( nmcd.hdc ); RECT rrc = {nmcd.rc.left+1, nmcd.rc.top+1, nmcd.rc.right-1, nmcd.rc.bottom-1}; pDC->Draw3dRect( &rrc, m_crMidShadow, m_crHilite ); pDC->Detach(); }
综上所述,我们得到了以下 OnCustomDraw()
函数(由于我现在记不清的原因,我实际上将其命名为 OnReflectedCustomDraw()
)。请注意,该函数在几个 switch
-case
语句中列出了所有可能的绘制阶段和所有可能的绘图项,作为未来扩展的骨架。您可以看到上面讨论的绘图代码。
void CCustomDrawSliderCtrl::OnReflectedCustomDraw(NMHDR* pNMHDR, LRESULT* pResult) { NMCUSTOMDRAW nmcd = *(LPNMCUSTOMDRAW)pNMHDR; UINT drawStage = nmcd.dwDrawStage; UINT itemSpec = nmcd.dwItemSpec; switch ( drawStage ) { case CDDS_PREPAINT: // Before the paint cycle begins. // This is the most important of the drawing stages, where we // must return CDRF_NOTIFYITEMDRAW or else we will not get further // NM_CUSTOMDRAW notifications for this drawing cycle // we also return CDRF_NOTIFYPOSTPAINT so that we will get post-paint // notifications *pResult = CDRF_NOTIFYITEMDRAW | CDRF_NOTIFYPOSTPAINT ; break; case CDDS_PREERASE: // Before the erase cycle begins case CDDS_POSTERASE: // After the erase cycle is complete case CDDS_ITEMPREERASE: // Before an item is erased case CDDS_ITEMPOSTERASE: // After an item has been erased // these are not handled now, but you might like to do so in the future *pResult = CDRF_DODEFAULT; break; case CDDS_ITEMPREPAINT: // Before an item is drawn. // This is where we perform our item-specific custom drawing switch ( itemSpec ) { case TBCD_CHANNEL: // channel that the trackbar control's thumb marker slides along // For the pre-item-paint of the channel, we simply tell the control // to draw the default and then tell us when it's done drawing // (i.e., item-post-paint) using CDRF_NOTIFYPOSTPAINT. // In post-item-paint of the channel, we draw a simple // colored highlight in the channel's recatngle *pResult = CDRF_DODEFAULT| CDRF_NOTIFYPOSTPAINT; break; case TBCD_TICS: // the increment tick marks that appear along the edge of the // trackbar control currently, there is no special drawing of the tics *pResult = CDRF_DODEFAULT; break; case TBCD_THUMB: // trackbar control's thumb marker. This is the portion of the control // that the user moves. For the pre-item-paint of the thumb, we draw // everything completely here, during item pre-paint, and then tell // the control to skip default painting and NOT to notify // us during post-paint. { CDC* pDC = CDC::FromHandle( nmcd.hdc ); int iSaveDC = pDC->SaveDC(); CBrush* pB = &m_normalBrush; CPen pen( PS_SOLID, 1, m_crShadow ); // if thumb is selected/focussed, switch brushes if ( nmcd.uItemState && CDIS_FOCUS ) { pB = &m_focusBrush; pDC->SetBrushOrg( nmcd.rc.right%8, nmcd.rc.top%8 ); pDC->SetBkColor( m_crPrimary ); pDC->SetTextColor( m_crHilite ); } pDC->SelectObject( pB ); pDC->SelectObject( &pen ); pDC->Ellipse( &(nmcd.rc) ); pDC->RestoreDC( iSaveDC ); pDC->Detach(); } // don't let control draw itself, or it will un-do our work *pResult = CDRF_SKIPDEFAULT; break; default: // all of a slider's items have been listed, so we shouldn't get here ASSERT( FALSE ); }; break; case CDDS_ITEMPOSTPAINT: // After an item has been drawn switch ( itemSpec ) { case TBCD_CHANNEL: // channel that the trackbar control's thumb marker slides along. // For the item-post-paint of the channel, we basically like // what the control has drawn, which is a four-line high rectangle // whose colors (in order) are white, mid-gray, black, and dark-gray. // However, to emphasize the control's color, we will replace the // middle two lines (i.e., the mid-gray and black lines) with hilite // and shadow colors of the control // using CDC::Draw3DRect. { CDC* pDC = CDC::FromHandle( nmcd.hdc ); RECT rrc = {nmcd.rc.left+1, nmcd.rc.top+1, nmcd.rc.right-1, nmcd.rc.bottom-1}; pDC->Draw3dRect( &rrc, m_crMidShadow, m_crHilite ); pDC->Detach(); } *pResult = CDRF_SKIPDEFAULT; break; case TBCD_TICS: // the increment tick marks that appear along the edge of the // trackbar control. Currently, there is no special post-item-paint // drawing of the tics *pResult = CDRF_DODEFAULT; break; case TBCD_THUMB: // trackbar control's thumb marker. This is the portion of the control // that the user moves. Currently, there is no special post-item-paint // drawing for the thumb // don't let control draw itself, or it will un-do our work *pResult = CDRF_DODEFAULT ; break; default: // all of a slider's items have been listed, so we shouldn't get here ASSERT( FALSE ); }; break; case CDDS_POSTPAINT: // After the paint cycle is complete. // This is the post-paint for the entire control, and it's possible to // add to whatever is now visible on the control. // To give an indication of directionality, we simply draw in two // colored dots at the extreme edges of the control { CDC* pDC = CDC::FromHandle( nmcd.hdc ); CBrush bWhite( RGB(255, 255, 255) ); // white brush CBrush bDark( m_crDarkerShadow ); // dark but still colored brush CPen p(PS_SOLID, 1, m_crPrimary); CRect rClient; GetClientRect( &rClient ); DWORD dwStyle = GetStyle(); int cx = 8; CRect rrcFirst;( 1, 1, cx, cx ); CRect rrcLast; // TBS_RIGHT, TBS_BOTTOM and TBS_HORZ are all defined as 0x0000 // so avoid testing on them if ( dwStyle & TBS_VERT ) { if ( dwStyle & TBS_LEFT ) { rrcFirst = CRect( rClient.right-cx, 1, rClient.right-1, cx ); rrcLast = CRect( rClient.right-cx, rClient.bottom-cx, rClient.right-1, rClient.bottom-1 ); } else { rrcFirst = CRect( 1, 1, cx, cx ); rrcLast = CRect( 1, rClient.bottom-cx, cx, rClient.bottom-1 ); } } else { if ( dwStyle & TBS_TOP ) { rrcFirst = CRect( 1, rClient.bottom-cx, cx, rClient.bottom-1 ); rrcLast = CRect( rClient.right-cx, rClient.bottom-cx, rClient.right-1, rClient.bottom-1 ); } else { rrcFirst = CRect( 1, 1, cx, cx ); rrcLast = CRect( rClient.right-cx, 1, rClient.right-1, cx ); } } int iSave = pDC->SaveDC(); pDC->SelectObject( &bWhite ); pDC->SelectObject( &p ); pDC->Ellipse( &rrcFirst ); pDC->SelectObject( &bDark ); pDC->Ellipse( &rrcLast ); pDC->RestoreDC( iSave ); pDC->Detach(); } *pResult = CDRF_SKIPDEFAULT; break; default: // all drawing stages are listed, so we shouldn't get here ASSERT( FALSE ); }; }
结果如文章开头所示,我在此重复。即使控件是用 TBS_ENABLESELRANGE
样式创建的,并且控件收到了 TBM_SETSEL
消息以将滑块的范围限制在整个控件的某个子范围内,控件看起来仍然很好并且运行正常。
- 下载颜色选择器演示项目 - 57.4 Kb (包含一个发布版可执行文件,无需编译即可运行)
- 仅 CCustomDrawSliderCtrl 源代码 - 4.99 Kb
我创建了这个辅助应用程序,帮助我在其他需要/想要一些浅色但可识别的颜色用于高亮显示目的的应用程序中选择颜色。要使用它,找到您喜欢的颜色,然后将亮度滑块向较浅的颜色拖动。上面描述的 RGB 到 HLS 函数(参见“Shell 调色板处理函数”)用于确保在更改为较浅亮度值时,色调和饱和度尽可能少地改变,以尽可能保留目标颜色。左下角的颜色补丁显示了当前选定的颜色,并显示了 139 个命名的 Web 颜色中最相似的一个的名称。最后,“复制值到剪贴板”按钮将 C 风格的 RGB
宏语句复制到剪贴板,可以粘贴到您的代码中。无论如何,这对我来说都非常有用 <g>。
如何在您的项目中使用该控件
这些说明适用于 VC++ 6.0 版本,但它们应该很容易用于 .NET 等其他版本。
要在您的项目中使用该控件,请将源代码和头文件(即 CustomDrawSliderCtrl.cpp 和 CustomDrawSliderCtrl.h)下载到方便的文件夹中,然后将它们都包含在您的项目中(“项目”->“添加到项目”->“文件…”)。
创建您的对话框资源模板,并使用控件工具栏添加一个标准滑块控件。
此时,您应该能够使用 ClassWizard 添加 CCustomDrawSliderCtrl
类型的变量。但是,我发现除非您强制 ClassWizard 重新构建类数据库,否则它不会注册新类,该类数据库存储在项目文件夹中扩展名为“.clw”的文件中。要强制 ClassWizard 重新构建类数据库,请使用资源管理器打开项目的工作区,然后找到扩展名为“.clw”的文件。删除它。(相信我,但如果您不相信,请将其重命名为“.clw~1”之类的扩展名。)现在,打开 ClassWizard,您将收到一条消息,说“.clw”文件不存在,并询问您是否希望从源文件重新构建它。当然,您应该选择“是”。在弹出的对话框中,选择“添加所有”,然后确保从您存储它们的任何文件夹中添加 CustomDrawSliderCtrl.cpp 和 CustomDrawSliderCtrl.h。
现在,打开 ClassWizard,添加一个 CCustomDrawSliderCtrl
的“控件”样式变量,如上面“编写自定义绘图 CSliderCtrl 的分步指南”一节所述。ClassWizard 会警告您在头文件中添加 #include
,您现在应该立即添加,以免忘记。
仅此而已。您的项目现在应该能正常编译了。
未来可能的工作
当然,还有更多可以做的事情,我肯定还没有穷尽自定义轨迹条控件外观的想法。
我们完全没有讨论刻度线。可以在这里做一些工作来改变这些标记的外观。例如,应该可以将刻度线显示为饱和度逐渐增加的彩色圆圈,或者直径缓慢增加以反映控件范围的圆圈。
在写这篇文章的时候,我还注意到一个新闻组的帖子提到,当轨迹条控件被禁用时,它的外观并没有真正改变。这很容易通过自定义绘图来改变。
可能性很多,我敦促您尝试一下并在此处发布您的发现。
参考文献
此处集中列出了文章中提到的所有文章和链接:
- Michael Dunn,《使用自定义绘图在列表控件中做些有趣的事情》。
- MSDN,《使用自定义绘图自定义控件的外观》。
- MSDN,《平台 SDK:简介》。
- MSDN,《TN062:Windows 控件的消息反射》。
- MSDN,《NMCUSTOMDRAW 结构》。
- ucc801,《带 C++ 源代码的酷 GDI 图案画刷工具》。
- MSDN,《Shell 调色板处理函数》。
脚注
- 实际上,这比这更复杂一点,因为消息处理程序实际上只获取一个
NMHDR
结构,但此处无需讨论这些复杂性;请阅读参考书目中提到的 MSDN 文章。 - 这是一个快速测验:如何将 RGB=(30, 144, 255),即明亮的“道奇蓝”变成更偏紫的颜色,如“深紫罗兰”?您可能期望必须增加红色。但您会期望也必须减少绿色吗?答案是 RGB=(148, 0, 211)(有趣的是,您还需要减少蓝色)。
- 另外,我认为这些功能让控件看起来更好,视觉上更平衡。当我们花时间自定义控件的外观时,通常是为了改善用户界面,让用户感觉更直观。但让我们面对现实:有时,我们仅仅是为了让控件看起来漂亮而进行表面修饰——这又有什么不好呢 <g>!