65.9K
CodeProject 正在变化。 阅读更多。
Home

CSliderCtrlEx - 一个带有背景颜色以指示范围的滑块

starIconstarIconstarIconstarIcon
emptyStarIcon
starIcon

4.92/5 (8投票s)

2002 年 11 月 25 日

CPOL

8分钟阅读

viewsIcon

154703

downloadIcon

8045

此滑块允许使用颜色/渐变来指示良好、差或一般的范围

Sample Image - CSliderCtrlEx.gif

目录

引言

在一个最近的项目中,我需要通过滑块或编辑框来征求用户输入。第一个滑块-编辑框中输入的值控制了第二个滑块-编辑框中允许输入的值。我的第一个尝试是在输入的值不允许(或不推荐)时显示错误消息。但这对用户来说很不方便。

然后我想到,滑块是模拟的,而错误消息是离散的;我需要的是一种同样具有模拟性质的反馈。用颜色填充滑块的各个范围(例如,green表示良好red表示)似乎是可行的方法。您可以在上面的截图中看到预期的效果。

CSliderCtrlEx 是从 CSliderCtrl 派生的,可以轻松地放入您的项目中。

致谢

部分代码,特别是用于在绘制过程中提取刻度标记和中心线的代码,是基于 Nic Wilson 在 miscctrl/transparentslider.asp 上的一篇精彩文章。

我还使用了动态加载msimg32.dll并获取 GradientFill 的技术,如 Irek Zielinski 在 staticctrl/gradient_static.asp 中所示。Irek 还提供了一个 GradientFill 的替代方案,但 CSliderCtrlEx 中的方案是我完全独立开发的(费力、痛苦,并且在我读 Irek 的文章之前)。

Ales Krajnc 写了一篇名为 gdi/colornames.asp 的文章,我通过复制ColorNames.h来利用它。阅读和理解具有 colOrange 之类内容的 Y代码比RGB(255,165,0).

要容易得多。一个在我开发此控件时发现很有用的函数是基于“gelbert”在 www.experts-exchange.com 上的一篇文章,地址是 Programming/Programming_Languages/MFC/Q_20193761.html。它只是一个简单的实用程序,可以将位图转储到文件中,以便以后在您喜欢的绘图程序中进行检查。对于那些还不太熟悉 GDI 操作的人来说,这真是太棒了。我在源代码中将其包含进来,名称是(不出所料!)SaveBitmap()

CSliderCtrlEx 的功能

此控件有两个主要功能:

  • 可以向控件背景添加颜色。颜色按照给定的顺序绘制(因此您可以将整个范围绘制成一种颜色,例如红色,然后将一个子范围绘制成绿色)。颜色的位置是根据滑块位置值指定的(而不是像素或其他非可移植的机制)。成员函数是:
    BOOL AddColor(int nLow, int nHigh, COLORREF color); BOOL AddColor(int nLow, int nHigh, COLORREF strColor, COLORREF endColor); 
  • 可以安装一个回调函数,在滑块值更改时调用。这是为了让我的生活更轻松(我需要在滑块更改时更新一个编辑框),而且这样做很容易,所以我把它包含在这里。反向操作(在更改编辑框时更新滑块位置)在封装框架中完成,我在这里没有例子,但它相当直接。

    回调函数如下所示:
    typedef void(*ptr2Func)(void *p2Object, LPARAM data1, int sValue, BOOL IsDragging); 
    这些参数的预期用途是,p2Object 是指向类实例(this)的指针,而 data1 将是滑块的控件 ID。这样您就可以有一个回调函数来知道它正在处理哪个滑块。sValue 只是滑块的位置(无需调用 GetPos()),而 IsDragging 仅指示左鼠标按钮是否按下。

它是如何工作的

控件以多个步骤绘制。幸运的是,有一种方法可以在重要步骤之间获得通知,方法是使用 OnCustomDraw 函数(有关文档可以在 NM_CUSTOMDRAW 中找到,而不是在 CSliderCtrl 的文档中)。此函数在控件绘制之前调用。如果请求,该函数还会在绘制过程的各个阶段被调用,例如在绘制刻度标记、通道和滑块之前和之后。因此,确保该函数为子控件调用是至关重要的:

void CSliderCtrlEx::OnCustomDraw(NMHDR* pNMHDR, LRESULT* pResult) { int loopMax = colorList.GetSize(); // 要处理的颜色范围数 LPNMCUSTOMDRAW lpCustDraw = (LPNMCUSTOMDRAW)pNMHDR; ////////////////////////////////////////////////////////////////////// // OnCustomDraw() 在控件绘制过程的许多不同阶段被调用。 // 我们只关心 PREPAINT 状态或 ITEMPREPAINT 状态,而且不总是这样。 // // 如果我们想在子控件绘制时收到通知,我们必须在收到 // 初始 PREPAINT 消息时告知。 ///////////////////////////////////////////////////////////////////// if(lpCustDraw->dwDrawStage == CDDS_PREPAINT) { int curVal = GetPos(); // 是否应该报告滑块的位置? if((m_Callback != NULL) && (curVal != m_oldPosition)) { m_oldPosition = curVal; m_Callback(m_p2Object, m_data1, curVal, m_IsDragging); } // 如果没有特殊的着色要做,则跳过所有 // 东西... if(loopMax <= 0) { *pResult = CDRF_DODEFAULT; } else { // 我们希望在控件的每个部分被  // 处理时收到通知,以便在绘制滑块之前插入颜色  *pResult = CDRF_NOTIFYITEMDRAW; // 为每个 // 部分发送消息 } return; } } 
背景的着色在绘制完除滑块外的所有内容之后进行,因此我们可以忽略其他所有内容:
if((lpCustDraw->dwDrawStage == CDDS_ITEMPREPAINT) && (lpCustDraw->dwItemSpec != TBCD_THUMB)) { *pResult = CDRF_DODEFAULT; return; } 

保存刻度线

现在开始涉及 GDI 内容(这是我的薄弱环节)。下面的代码用于保存刻度标记(Nic Wilson 作品的改编)。我从源代码中提取了以下显示并省略了注释,源代码中有大量注释,可能看起来很有趣:

// 获取控件窗口的坐标 CRect crect; GetClientRect(crect); CDC *pDC = CDC::FromHandle(lpCustDraw->hdc); CDC SaveCDC; CBitmap SaveCBmp; //设置单色蒙版位图的颜色 COLORREF crOldBack = pDC->SetBkColor(RGB(0,0,0)); // 设置为黑色 COLORREF crOldText = pDC->SetTextColor(RGB(255,255,255)); // 设置为白色 int iWidth = crect.Width(); // 通道宽度 int iHeight = crect.Height(); // 通道高度 SaveCDC.CreateCompatibleDC(pDC); SaveCBmp.CreateCompatibleBitmap(&SaveCDC, iWidth, iHeight); CBitmap* SaveCBmpOld = (CBitmap *)SaveCDC.SelectObject(SaveCBmp); SaveCDC.BitBlt(0, 0, iWidth, iHeight, pDC, crect.left, crect.top, SRCCOPY); if(m_dumpBitmaps) // 调试内容 { SaveBitmap("MonoTicsMask.bmp",SaveCBmp); } 
请注意对 SaveBitmap 的调用。我发现这个函数非常有用。事实上,这是生成的位图(已放大):

显示刻度线的单色位图

这个位图(包含在 SaveCDC 设备上下文中)稍后会在背景颜色绘制完成后被大量使用。

在内存空间中操作,而不是屏幕空间

这里涉及大量的操作,虽然我可以在屏幕上进行渐变、矩形、重叠颜色以及 AND、INVERT 等操作,但这样做会很慢,而且屏幕会闪烁很多。因此,我创建了一个内存 DC 来进行操作:

CDC memDC; memDC.CreateCompatibleDC(pDC); CBitmap memBM; memBM.CreateCompatibleBitmap(pDC,iWidth,iHeight); // 从 pDC 创建, // 不是 memDC CBitmap *oldbm = memDC.SelectObject(&memBM); memDC.BitBlt(0,0,iWidth,iHeight,pDC,0,0,SRCCOPY); 
请注意,即使我有一个与屏幕兼容的 DC(memDC),我也必须使用屏幕的 DC 来创建位图。否则,我会得到一个单色位图。(别问我花了多长时间才弄明白)。位图看起来像:

滑块控件的起始位图

实际上,这是控件第一次绘制时的样子。在后续更新中,您可以看到先前背景颜色的残余。这其实无关紧要,因为我将完全覆盖它。但是使用 SaveBitmap 确实让我能够确认我正朝着正确的方向前进。

绘制到哪里?

我第一次制作这个控件时,我绘制了整个客户窗口的长度。看起来不错。看起来是对的。但后来我注意到滑块的中心并不总是与报告的位置对应。我最终意识到问题在于滑块的范围不是整个客户矩形的宽度(想象一下!)。滑块值的范围由滑块中心的可移动范围表示。

滑块控件的组成部分

所以,我需要将颜色限制在滑块中心所覆盖的区域内,而不是客户矩形的整个宽度。嗯,有一个 GetChannelRect() 函数可以返回滑块在其中滑动的通道。还有一个 GetThumbRect() 函数。好的,我可以进行计算。但有一个小细节:

// 出于未知原因,GetChannelRect() 返回的矩形 // 好像它是水平放置的滑块,即使它不是! if(IsVertical) { CRect n; n.left = chanRect.top; n.right = chanRect.bottom; n.top = chanRect.left; n.bottom = chanRect.right; n.NormalizeRect(); chanRect.CopyRect(&n); } // 客户端矩形中颜色范围开始的偏移量 int Offset = chanRect.left + thmbRect.Width()/2; if(IsVertical) { Offset = chanRect.top + thmbRect.Height()/2; } // 滑块中心的范围 int ht = chanRect.Height() - thmbRect.Height(); int wd = chanRect.Width() - thmbRect.Width(); 
现在我可以获得滑块范围单位和像素之间的缩放因子。

用颜色绘制

颜色范围存储在一个结构数组中,其中包含开始和结束位置,以及开始和结束颜色。按顺序循环处理这些相对简单。我将位置缩放到像素值,从开始和结束颜色中提取红色、绿色和蓝色值,并设置一个调用 GradientFill 来进行绘制:

TRIVERTEX vert[2]; // 用于指定渐变填充的范围 GRADIENT_RECT gRect; vert[0].Red = sR<<8; // 需要 16 位颜色值! vert[0].Green = sG<<8; vert[0].Blue = sB<<8; vert[0].Alpha = 0; // 没有渐变/透明度 vert[1].Red = eR<<8; vert[1].Green = eG<<8; vert[1].Blue = eB<<8; vert[1].Alpha = 0; gRect.UpperLeft = 0; gRect.LowerRight = 1; BOOL retval; if(IsVertical) // 垂直方向? { vert[0].x = 0; vert[0].y = Offset + minVal; vert[1].x = iWidth; vert[1].y = Offset + minVal + widthVal; retval = GradientFill(memDC,vert,2,&gRect,1,GRADIENT_FILL_RECT_V); } else { vert[0].x = Offset + minVal; vert[0].y = 0; vert[1].x = Offset + minVal + widthVal; vert[1].y = iHeight; retval = GradientFill(memDC,vert,2,&gRect,1,GRADIENT_FILL_RECT_H); } 
一个让我困惑了一段时间的问题是,在使用 TRIVERTEX 结构时,RGB 值必须向上移一位。很长一段时间我只能得到黑色……

如果没有渐变(开始和结束颜色相同),那么 GradientFill 会做得合乎逻辑:进行实心填充。

如果我只留下这些,那么颜色将是正确的,但控件看起来会很难看:

控件的难看背景

我需要做的是填充两端:

背景颜色

if(IsVertical) { if(gotStartColor) { memDC.FillSolidRect(0, 0, iWidth, Offset, startColor); } if(gotEndColor) { memDC.FillSolidRect(0,iHeight - Offset - 1,iWidth, Offset, endColor); } } else { if(gotStartColor) { memDC.FillSolidRect(0, 0, Offset, iHeight, startColor); } if(gotEndColor) { memDC.FillSolidRect(iWidth - Offset - 1,0,Offset, iHeight, endColor); } } 
显然,我在着色循环中保存了颜色。如果颜色范围没有延伸到控件范围的末尾,我也没有理由进行任何扩展。

重新应用刻度线

这是 Nic Wilson 的作品再次帮我省去大量麻烦的地方。下面是步骤和中间结果。源代码中有更多注释,但没有位图可供查看。请记住,刻度标记保存在 SaveCDC 中:

memDC.SetBkColor(pDC->GetBkColor()); // RGB(0,0,0) memDC.SetTextColor(pDC->GetTextColor()); // RGB(255,255,255) memDC.BitBlt(0, 0, iWidth, iHeight, &SaveCDC, 0, 0, SRCINVERT); 
这会应用刻度线,但颜色是“反向”的。



现在修复刻度线:
memDC.BitBlt(0, 0, iWidth, iHeight, &SaveCDC, 0, 0, SRCAND); 


现在反转所有颜色以获得最终图像:
memDC.BitBlt(0, 0, iWidth, iHeight, &SaveCDC, 0, 0, SRCINVERT); 


将其 Blit 到屏幕并清理!控件的其他绘制由基类代码处理,主要是滑块和边框。
// 现在复制到屏幕 pDC->BitBlt(0,0,iWidth,iHeight,&memDC,0,0,SRCCOPY); 

使用代码

添加到项目中

要将 CSliderCtrlEx 添加到您的项目中,请将源文件 SliderCtrlEx.h 和 SliderCtrlEx.cpp 添加到您的项目(项目 -> 添加到项目 -> 文件...)并进行构建。然后,在您的对话框中添加普通滑块。然后使用类向导将成员变量类型设置为 CSliderCtrlEx。如果类向导未列出新类型,请仅使用 CSliderCtrl 并手动更改类型。

添加颜色范围

OnInitialUpdate()(或您喜欢的任何地方)中,添加类似以下的行:

// 普通 CSliderCtrl 初始化: m_Slider2.SetBuddy(&m_Edit2,FALSE); // 强制编辑控件“配对” m_Slider2.SetRange(0,1000); m_Slider2.SetTicFreq(100); // CSliderCtrlEx 特有内容: m_Slider2.AddColor(0,1000,RGB(255,0,0)); //纯红色 // 创建渐变 m_Slider2.AddColor(200,300,colRed,colOrange); // 用户应避开 // 此处 m_Slider2.AddColor(300,400,colOrange,colYellow);// 不是最佳值 m_Slider2.AddColor(400,500,colYellow,colGreen); // 最佳范围 m_Slider2.AddColor(500,600,colGreen,colYellow); m_Slider2.AddColor(600,750,colYellow,colOrange); m_Slider2.AddColor(750,maxRange,colOrange,colRed);// 对用户极其危险! m_Slider2.Refresh(); // 强制更新新配置滑块的屏幕显示 // sItemUpdate() 是一个静态函数,用于分派到 ItemUpdate() m_Slider2.setCallback(CSliderClrDemoView::sItemUpdate,this, (LPARAM)IDC_SLIDER2); 
OnInitialUpdate()Refresh() 实际上不是必需的,但如果您在程序运行期间更改颜色(例如,在我开始这一切的那个项目中),那么您将需要它。

添加通知回调

回调机制有点像 Windows 的工作方式。我有如下声明:

void ItemUpdate(LPARAM data1, int sValue, BOOL IsDragging); static void sItemUpdate(CSliderClrDemoView *obj, LPARAM data1, int sValue, BOOL IsDragging); 
sItemUpdate 只是为了转换为类的操作空间。实现非常简单:
void CSliderClrDemoView::sItemUpdate(CSliderClrDemoView *obj, LPARAM data1, int sValue, BOOL IsDragging) { CSliderClrDemoView *me = (CSliderClrDemoView *)obj; me->ItemUpdate(data1, sValue, IsDragging); } void CSliderClrDemoView::ItemUpdate(LPARAM data1, int sValue, BOOL /* IsDragging */) { double slope1 = 0.05; double intercept1 = -25.0; double slope2 = 0.08; double intercept2 = -15.0; CString val; switch(data1) { case IDC_SLIDER1: val.Format("%6.2lf", (slope1 * double(sValue)) + intercept1); m_Edit1.SetWindowText(val); break; case IDC_SLIDER2: val.Format("%6.2lf", (slope2 * double(sValue)) + intercept2); m_Edit2.SetWindowText(val); break; case IDC_SLIDER3: val.Format("%6.2lf", (slope2 * double(sValue)) + intercept2); m_Edit3.SetWindowText(val); break; } } 

历史记录

  • 2002 年 11 月 25 日 -- 首次发布到(不知情的)CodeProject
© . All rights reserved.