SliderGdiCtrl:又一个滑块控件 - 接受(几乎)所有 POD 类型并使用 GDI+
一个 MFC 滑块控件,支持大多数 POD 类型并具有增强的外观。
引言
如果你想让你的应用程序用户从某个选择范围中选择一些整数值,你很可能会使用 MFC 滑块控件。你指定选择范围和增量,滑块会通知你的应用程序滑块滑块位置的变化。
MFC 滑块控件只适用于整数。如果我们需要处理浮点值(如双精度浮点数),该怎么办呢?我需要一个控件来从 0...1.0 的范围中选择一个双精度浮点数,增量为 0.001。
标准的处理方法是放大你的范围数字,将它们转换为整数,然后将其提供给 MFC 滑块控件。收到用户选择后,你将它转换为双精度浮点数并缩小。我不想这样做。此外,滑块控件的外观会相当平淡。
所以我决定编写我自己的滑块控件,它接受双精度浮点数并具有更好(在我看来)的外观。我将其命名为 SliderGdiCtrl
。
我过去两年一直在使用我的 SliderGdiCtrl
。当我决定将其提交给 CodeProject 并开始撰写本文时,我发现只需相对较小的更改,我的滑块就可以接受并处理几乎所有 POD 数字类型。实际上,这些更改并不那么小,但无论如何,它就是 SliderGdiCtrl
控件。这个控件接受所有可以转换为 double
而不损失精度的 POD 类型。这不包括 long double
和 long long
类型。
致谢
我借鉴了 Andrei Alexandrescu 的“Modern C++ Design”第 3 章中模板 TypeList
和 IndexOf
的定义。我还使用了 Andrei 的 Loki 库的 static_check.h 中的编译时断言 STATIC_CHECK
。
我与 CodeProject 论坛上的 Code-o-mat 就 MFC 中的 Kill Focus 进行了非常有益的讨论。
我在 CP 上看到了一个外观非常相似的滑块控件,但它与我的控件不同。而且,据我记忆,它是用 C# 编写的。抱歉,我现在无法在 CP 上找到它。
一般描述
在两个源文件中有三个 C++ 类可用:SliderGdiCtrl.h 和 SliderGdiCtrl.cpp
class CSliderGdiCtrl : public CWnd;
template <typename T> class CSliderGdiCtrlT : public CSliderGdiCtrl;
class CTipWnd : public CWnd
这些类位于命名空间 SliderGdi
中。
类 CSliderGdiCtrlT
是 SliderGdiCtrlT
接口函数的模板包装器;所有功能都在 CSliderGdiCtrl
中。CTipWnd
是一个辅助类,用于在用户请求时在单独的(提示)窗口中显示控件名称和当前位置值。
所有内部计算都以 double
形式执行,结果存储在 CSliderGdiCtrl
数据成员中。用户界面函数的所有数据成员和输出值都四舍五入到滑块的增量。
滑块向其父级发送两个通知消息:当滑块位置正在改变时发送 TB_THUMBTRACK
,当滑块位置已改变时发送 TB_THUMBPOSITION
。
滑块图形界面使用 GDI+。它使用线性渐变和渐变画刷以及 alpha 通道(以获得半透明颜色)。
MFC 的 CSliderCtrl
使用两个额外的控件:一个静态控件用于显示滑块名称,一个编辑控件(又称伙伴控件)用于显示与当前滑块位置对应的值。
SliderGdiCtrl
控件在同一个窗口中显示滑块名称和当前值。
SliderGdiCtrl
在默认颜色下的典型布局如下图所示。你可以看到圆角矩形的条形、滑块、控件名称、带有当前值的数据标签区域和提示窗口。提示窗口在鼠标右键单击时出现,在第二次右键单击或鼠标离开条形矩形时隐藏。此窗口与工具提示窗口相似但不相同:可见性没有时间限制。
滑块的背景颜色可以通过编程方式更改。
如果当前值过大,无法在数据区域中显示,软件将尝试将名称和值显示为一个组合字符串。滑块颜色是半透明的,因此无论滑块位置在哪里,我们都能够读取名称和值。
如果组合字符串没有足够的空间,我们将只尝试显示名称。滑块值将在用户请求时显示在提示窗口中。
最后,如果甚至连名称都没有足够的空间,名称和值都将显示在提示窗口中。
只实现了水平方向。
如何使用它
前置条件
此项目中的类是 MFC 派生类,使用 GDI+ 渲染图形。所有代码都在 MS VC++ 2008 (VC9) 下编译和链接。请确保您的项目中包含了适当的 MFC 和 GDI+ 库。
您必须在应用程序中添加一些 GDI+ 代码,以便在启动时初始化并在退出时释放。为此,您必须:
- 在应用程序中包含一个私有数据成员
ULONG_PTR m_nGdiplusToken
class CMyApp : public CWinAppEx
{
.........
private:
ULONG_PTR m_nGdiplusToken;
.........
}
CMyApp::InitInstance
中,添加这两行BOOL CMyApp::InitInstance ()
{
......
Gdiplus::GdiplusStartupInput gdiplusStartupInput;
Gdiplus::GdiplusStartup(&m_nGdiplusToken, &gdiplusStartupInput, NULL);
......
}
CMyApp
中尚未包含 ExitInstance()
函数,请添加该函数,并在其主体中插入以下行int CMyAppApp::ExitInstance()
{
...........
Gdiplus::GdiplusShutdown(m_nGdiplusToken);
...........
}
您还需要在项目中包含头文件 SliderGdiCtrl.h
你可以将 SliderGdiCtrl
控件作为一个库使用,或者直接将源文件包含在你的项目中。
如果你打算将其作为库使用,请将 SliderGdiCtrlLib.lib 文件添加到你的项目可以访问的目录中。在项目属性对话框的 Linker\Input\Additional Dependency 中包含此库名称。
如果你只打算使用源文件,请额外在你的项目中包含源文件 SliderGdiCtrl.cpp。
如果您的项目正在使用预编译头文件,请在 stdafx.h 文件中包含以下头文件:<afxext.h>、<sstream>、<iomanip>、<cmath>、<limits> 和 <gdiplus.h>。如果不是,请将它们包含在 SliderGdiCtrl.cpp 中,并注释掉 '#include "stdafx.h"
行。
现在,您就可以开始在项目中使用 CSliderGdiCtrl
控件了。
将 SliderGdiCtrl 控件包含到您的应用程序中
- 如果您的应用程序是基于对话框的,请在资源编辑器中,从工具箱中选择滑块控件并将其拖动到对话框中的位置。调整控件的大小和位置。在控件属性窗口中,输入一个控件 ID(例如
IDC_SLLONG
或IDC_SLFL
)。 - 将数据成员添加到您的
CDialog
类定义中,例如 - 将函数
DDX_Control
添加到对话框函数DoDataExchange(CDataExchange* pDX)
以子类化控件,例如 - 或者,如果您的应用程序是文档视图应用程序,请在视图类中声明
CSliderGdiCtrl<T>
数据成员,例如 - 使用
CSliderGdiCtrlT
和CSliderGdiCtrl
接口成员来初始化和操作滑块控件。如果应用程序是基于对话框的,在InitDialog
中初始化滑块;如果不是,则在滑块的父CView
的OnInitialUpdate
中进行。
CSliderGdiCtrl<long> m_slGdiLong;
或
CSliderGdiCtrl<float>.m_slGdiFl;
DDX_Control(pDX, IDC_SLLONG, m_slGdiLong);
CSliderGdiCtrl<long> m_slGdiLong;
然后调用
m_slGdiLong.CreateSliderGdiCtrl(DWORD dwStyle, const CRect slRect,
CWnd* pParent, UINT slID)
以创建滑块窗口。
SliderGdiCtrl 控件接口
要初始化和操作 SliderGdiCtrl
,程序员应该使用 CSliderGdiCtrlT
和 CSliderGdiCtrl
类的接口成员函数。
获取当前值(滑块位置)
template <typename T>
T CSliderGdiCtrlT<T>::GetCurrValue(void) const;
获取选择范围的最小值和最大值
template <typename T>
T CSliderGdiCtrlT<T>::GetMinVal(void) const;
template <typename T>
T CSliderGdiCtrlT<T>::GetMaxVal(void) const;
在 CSliderGdiCtrlT
的模板成员函数中,用于设置 CSliderGdiCtrl
数据成员的所有函数参数必须与声明和实例化滑块时使用的类型 T
相同。此约束是为了类型安全。违反此规则将引发编译时断言。如果你确实需要将其他数据类型传递给这些函数,请显式将其转换为 T
。如果你想将字面量作为参数传递给这些函数,请使用字面量后缀来正确定义字面量类型,例如,short
为 23i16,float
为 6.98f 等。更多内容请参见兴趣点部分。
函数 SetCurrValue
设置滑块滑块的当前位置。此函数总是将超出滑块范围的值裁剪到范围的最大值或最小值,如果值被裁剪则返回 false。标志 bRedraw
定义是否应立即重绘滑块。
template <typename T>
template <typename T1>
bool CSliderGdiCtrlT<T>::SetCurrValue(T1 value, bool bRedraw = false);
下一个函数是设置选择范围的最小值和最大值。如果违反了最小值和最大值的约束,或者参数 bAdjustCurrVal == false
且当前值超出新的选择范围,则该函数不进行任何更改并返回 false
。如果 bAdjustCurrVal == true
,当当前值超出选择范围时,它将被裁剪到 minVal
或 maxVal
。
template <typename T>
template <typename T1, typename T2>
bool CSliderGdiCtrlT<T>::SetMinMaxVal(T1 minVal, T2 maxVal,
bool bAdjustCurrVal = false, bool bRedraw = false);
下一个函数设置选择范围的边界、精度(增量)和当前值(滑块位置)。如果精度值为正,则它定义小数点后的十进制位数。负精度定义小数点前非有效零的位数。如果精度 = P,则增量为 10-P。滑块与应用程序交换并显示给用户的所有值都四舍五入到 10-P。精度 = 3 表示小数点右侧三位数字;精度 = -2 表示小数点前最后两位数字不重要且始终为零。
此函数是初始化首选的,因为它检查范围、精度和当前值的所有关系和限制。
如果其任何参数的约束被违反,则该函数不进行任何更改并返回 false
。
template <typename T>
template <typename T1, typename T2, typename T3>
bool CSliderGdiCtrlT<T>::SetInitVals(T1 minVal, T2 maxVal,
int precision, T3 currVal, bool bRedraw = false);
要设置控件的背景颜色或恢复默认颜色,您将需要这些接口函数
void BarColor(Gdiplus::Color barFirstCol, bool bRedraw = false);
void SetBarColorDefault(bool bRedraw = false);
要获取当前条形颜色,请使用
Gdiplus::Color BarColor(void) const
两个重载函数将设置 SliderGdiCtrl
名称
void SetCaption(std::wstring& caption, bool bRedraw = false);
void SetCaption(WCHAR* caption, bool bRedraw = false);
要获取名称,请使用
std::wstring GetCaption();
要设置精度,请使用
bool SetPrecision(int precision, bool bRedraw = false);
如果精度值超出限制,函数将返回 false
。
要获取精度和增量值,请调用
int Precision(void) const;
和
double GetKeyStep (void) const;
您可以通过向滑块控件发送或发布 Windows 消息 WM_SIZE
和 WM_MOVE
来以编程方式移动滑块控件和/或更改其大小。
下面是文档/视图架构中滑块控件的声明和初始化示例
void CSliderGdiCtrlWView::OnInitialUpdate()
{
CView::OnInitialUpdate();
m_slCtrlPtr = new CSliderGdiCtrlT<long>;
CRect cRect;
GetClientRect(&cRect);
cRect.DeflateRect(250, 250);
m_slCtrlPtr->CreateSliderGdiCtrl(WS_CHILD|WS_VISIBLE, &cRect, this, 32789);
// Min range -100, max range 1000 no digits after
// decimal point, current value 50
m_slTest.SetInitVals(-100L, 100L, 0, 50L);
// Set control name "LONG VALUES"
m_slTest.SetCaption(L"LONG VALUES");
// Set background color red
m_slTest.BarColor(Color(255, 255, 0, 0));
}
看起来是这样的
Notifications
滑块向其父级发送两个通知消息:在鼠标左键按下、鼠标移动和键盘按下时发送 TB_THUMBTRACK
,在鼠标左键释放或键盘释放时发送 TB_THUMBPOSITION
。
如果 SliderGdiCtrl
的父级要处理这些通知,请在父级的消息映射中包含类似如下的条目
ON_NOTIFY(TB_THUMBTRACK, IDC_SLID, OnSlidePosChanging)
ON_NOTIFY(TB_THUMBPOSITION, IDC_SLID, OnSlidePosChanged)
其中处理函数声明为
afx_msg void OnSlidePosChanging(NMHDR *pNMHDR, LRESULT *pResult);
afx_msg void OnSlidePosChanged(NMHDR *pNMHDR, LRESULT *pResult);
并且 NMHDR
是来自 Windows API 的标准结构。
用户手册
要移动滑块,用户应左键单击滑块并拖动鼠标,保持左键按下。在滑块移动区域内但滑块外部的左键单击会将滑块移动到鼠标 X 坐标。在滑块条中的任何位置左键单击都会将焦点设置到滑块。
要以增量移动滑块,当滑块获得焦点时,用户应使用箭头键
- 左箭头键将滑块位置减少一个增量;
- 右箭头键将滑块位置增加一个增量;
- Page Up 键将滑块位置减少十个增量;
- Page Down 键将滑块位置增加十个增量;
- HOME 键将滑块设置在范围的最小(左)边界;
- END 键将滑块设置在范围的最大(右)边界。
要使提示窗口可见,用户应在滑块内的任何位置右键单击。当鼠标光标在滑块区域内时,提示窗口将保持可见。在第二次右键单击时关闭,或者当光标离开滑块条区域时自动隐藏。
关注点
类
如前所述,命名空间 SliderGdi
中有三个 C++ 类
class CSliderGdiCtrl : public CWnd;
template <typename T> class CSliderGdiCtrlT : public CSliderGdiCtrl;
class CTipWnd : public CWnd
所有滑块控件的数据成员和成员函数都在 CSliderGdiCtrl
类中。该类的数据成员存储滑块范围的最大值和最小值、增量值以及与当前滑块位置对应的当前值。这些数据成员的类型为 double
。您可以说 CSliderGdiCtrl
中这些值的内部表示是 double
。在我看来,这是最好的选择。除 long long
和 long double
之外的所有 POD 类型都可以转换为 double
类型而不会丢失精度。如果遵循 double
值的一些约束,从 double
到其他 POD 类型的转换也可以精确完成。
当然,如果你选择浮点类型,你就会招致浮点算术的所有陷阱,但你会获得更多的通用性,我稍后会展示。
要使用 POD 类型,有一个模板类
template <typename T> class CSliderGdiCtrlT: public CSliderGdiCtrl
本质上,它是一个围绕 CSliderGdiCtrl
接口函数的模板包装器。
CSliderGdiCtrlT
类负责从 T
到 double
以及从 double
到 T
的安全转换。从 CSliderGdiCtrl
继承可以减少代码膨胀,因为所有与 T
无关的函数都是非模板类 CSliderGdiCtrl
的成员。
CSliderGdiCtrl
使用第三个类 CTipWnd
来调用提示窗口。
限制和约束
让我们讨论一下 SliderGdiCtrl
范围边界、当前值和精度的限制和约束。这些值存储在 CSliderGdiCtrl
的数据成员中:m_fMinVal
、m_fMaxVal
、m_fCurrValue
和 m_precision
。
有些限制是显而易见的:范围的最小值不能大于或等于范围的最大值;增量不能大于范围或等于零。增量不能小于该类型的最小值,例如 double
的 DBL_MIN
,float
的 FLT_MIN
,以及整数类型的 1。
此外,滑块范围的最大值和最小值(或右边界和左边界)间接受到范围值限制。显然,范围不能大于该类型的最大值。对于 double
类型
范围 = 右边界 - 左边界 DBL_MAX = 1.7976931348623158e+e308
(当然,在应用程序中不能真正期望如此大的范围。不过,这个值可能会因为错误的计算而出现。)
幸运的是,double
类型不是模数类型。如果一个双精度数字超出类型范围,结果是正或负无穷大,而不是一些漂亮地包装在类型限制之一的数字。因此,必须对范围进行无穷大测试(或测试是否存在无穷大)
if (!_finite(dbRange))
return false;
但仅凭无限测试是不够的。
浮点数具有有限的精度,因为它们在内存中占用有限的位数。这意味着,如果我们增加一个太大的数,而增量太小,那么这个大数不会改变。因此,范围的最小和最大边界与增量值之间存在关系约束。
精度和增量
要获得滑块新位置的值,我们应该向/从该值添加或减去一个或多个增量。我们知道,将浮点数增加或减少一个太小的增量**不会改变**该数字。因此,增量应该有一个下限。
表征 double
类型精度和准确性的常数是
std::numeric_limits<double>:: digits10 = DBL_DIG = 15
C++ 参考将 std::numeric_limits<T>::digits10
定义为“可以不变地表示的数字位数(以十进制为底)”。这令人困惑。什么变化?增加一个过大的浮点数,但增量过小,并**不会改变**这个过大的数。
正如 [1] 中所解释的,std::numeric_limits<T>::digits10
“是**保证**正确的十进制位数”。它定义了数字的有效位数。
应用于声明为 double
类型的滑块,这意味着为了使当前值准确,范围边界的较大绝对值与增量之间的差值必须不大于十五个数量级。这是增量的下限。
十五个数量级是一个数字值的万亿分之一,因此这是一个相当理论上的限制,但同样,可能会发生错误的计算……
这个差异的实际数量级定义了精度,或者说是增量的十进制指数。那尾数呢?让我们看看我们那里有什么问题。
假设你的范围边界是 -1.0,+1.0,增量是 0.005。你想要将滑块移动到值为 0.345 的位置。你点击滑块的条形,得到的值是 0.344。下一次鼠标移动或点击恰好得到 0.345 的概率非常低。箭头键会将值增加一个增量,到 0.349。**因此,为了方便导航,你应该将增量的尾数设置为 1,并按增量对范围边界和当前值进行四舍五入**。
在我们的例子中,增量应该是 0.001。如果当前值是 0.345,右箭头键单击一次会将当前值增加到 0.345。
以相对于小数点位置的数字位数来衡量精度更为方便。在此滑块控件中,正精度等于小数点右侧的十进制位数;负精度表示小数点左侧的非有效零的位数。增量的值为
D = 10-P 其中 P 是精度
以上所有内容都是关于滑块类 CSliderGdiCtrlT<double>
的限制和约束。
其他 POD 类型的滑块选择范围不受类型最大值的限制,因为所有计算都以 double
值执行,并且 CSliderGdiCtrl
类中相关数据成员的类型为 double
。当然,范围的边界和当前滑块值不能超出类型的范围。
float
类型的最小增量受限于
std::numeric_limits<float>::digits10 = FLT_DIG = 6
整数类型的最小增量为1。
因此,类 CSliderGdiCtrl
必须知道其 double
数据成员的实际类型。
CSliderGdiCtrl
数据成员的实际类型信息存储在数据成员中
int CSliderGdiCtrl::m_typeID; // Index of the slider data
//type: -1 if not supported,
// 0 -6 for int types, 7 for
// float, 8 for double
代码中任何地方都看不到这些数字。那么类是如何初始化 m_typeID
并使用它的呢?
答案是:在 CSliderGdiCtrlT<T>
实例化时,通过基类 CSliderGdiCtrl
的初始化
template <typename T>
CSliderGdiCtrlT<T>::CSliderGdiCtrlT(void):
CSliderGdiCtrl(IndexOf<SL_TYPELIST, T>::value);
基类的构造函数是
CSliderGdiCtrl::CSliderGdiCtrl (int typeID = IndexOf<SL_TYPELIST, double>::value)
{
.......................
m_typeID = typeID;
.......................
}
在 CSliderGdiCtrlT<T>
实例化时,编译器会计算 typeID
的值并相应地初始化 m_typeID
。
每当我们想要做一些取决于实际数据类型的事情时,我们都会编写一段元代码
if (m_typeID == IndexOf<SL_TYPELIST, my_type>::value) do_something();
同样,条件在编译时解析。
此机制使用带有 TYPELIST
的元编程。我稍后会回到 TYPELIST
,但在此之前,我想展示一下函数 GetMaxPrecision
中最大精度的计算
int CSliderGdiCtrl::GetMaxPrecision(double value) const
{
// For integer types (located before float in TYPELIST)
if (m_typeID < IndexOf<SL_TYPELIST, float>::value)
return 0;
// For floating-point types float and double
double absVal = fabs(value);
double lgAbsVal = log10(absVal);
double maxDig;
modf(lgAbsVal, &maxDig); // Can't use ceil(lgAbsVal)
//because ceil(log10(1.0)) =
// 0 (we need 1)
int nMaxDig = static_cast<int>(maxDig);
if (lgAbsVal >= 0)
//Greatest significant digit
nMaxDig += 1; //has a position log+1
int maxPrec = m_digitNmb - nMaxDig;
// Check for minimum positive value of the type (has
// exponent < 0)
int minExp = (m_typeID == IndexOf<SL_TYPELIST, double>::value) ?
-numeric_limits<double>::min_exponent10 :
-numeric_limits<float>::min_exponent10;
return min(maxPrec, minExp);
}
CSliderGdiCtrl
的这个成员函数设置精度和增量
bool CSliderGdiCtrl::SetPrecision(int precision,bool bRedraw /*=false*/)
{
// m_fMaxVal and m_fMinVal are the range bounddaries
int maxPrecision = GetMaxPrecision(max(abs(m_fMinVal), abs(m_fMaxVal)));
if (precision > maxPrecision return false;
double step = pow(10.0, -precision); // Increment
// Calculate range
double fRange = m_fMaxVal - m_fMinVal;
if (step > fRange)
return false;
m_precision = precision; // Set data member
// Update the string to display current value
m_strValue = SetValStr(m_fCurrValue);
// Set flag to indicate layout must be recalculated
m_slStat = UNINIT;
if (bRedraw) // Redraw immediately
RedrawWindow(NULL, NULL, RDW_INVALIDATE | RDW_UPDATENOW | RDW_NOERASE);
return true;
}
类型列表和其他元编程技巧
如前所述,CSliderGdiCtrl
类必须知道其 double
数据成员的实际类型。在 CSliderGdiCtrlT<T>
实例化时,编译器会计算 typeID
的值并初始化 CSliderGdiCtrl::m_typeID
。
为此,编译器需要手头有一个允许的类型集合,并知道如何从集合中获取该类型的索引。
我使用了 TypeList
作为类型集合,并使用元函数 IndexOf
在集合中搜索类型的索引。下面的代码借用自 [2] 第 3 章
class NullType;
template <class T, class U>
struct TypeList
{
typedef T Head;
typedef U Tail;
};
#define TYPELIST_1(T1) TypeList<T1, NullType>
#define TYPELIST_2(T1, T2) TypeList<T1, TYPELIST_1(T2)>
#define TYPELIST_3(T1, T2, T3) TypeList<T1, \
TYPELIST_2(T2, T3)>
..........................................................
#define TYPELIST_9(T1, T2, T3, T4, T5, T6, T7, T8, T9) \
TypeList<T1, TYPELIST_8(T2, T3, T4, T5, T6, T7,\
T8, T9)>
template <class TList, typename T> struct IndexOf;
template <typename T>
struct IndexOf<NullType, T> { enum { value = -1};};
template <class Tail, typename T>
struct IndexOf<TypeList<T, Tail>, T>
{
enum {value = 0};
};
template <class Head, class Tail, typename T>
struct IndexOf<TypeList<Head, Tail>, T>
{
private:
enum {temp = IndexOf<Tail, T>::value};
public:
enum {value = temp == -1 ? -1 : 1 +temp};
};
现在,让我们定义滑块可以使用的类型
typedef TYPELIST_9(byte, short, unsigned short, int,
unsigned int, long, unsigned long, float,
double) SL_TYPELIST;
在编译时,编译器将计算类型的索引
index<T> = IndexOf<SL_TYPELIST, T>::value;
以确保 CSliderGdiCtrlT<T>
接口函数的类型安全。
现在让我们谈谈类型安全。
以函数 CSliderGdiCtrlT<T>::SetInitVals
为例。乍一看,将其定义为以下形式似乎就足够了:
bool CSliderGdiCtrlT<T>::SetInitVals(T minVal, T maxVal,
int precision, T currVal, bool bRedraw = false)
{
return CSliderGdiCtrl::SetInitVals(minVal, maxVal,
precision, currVal, bRedraw);
}
这行不通。
假设我们实例化了一个滑块,如 CSliderGdiCtrlT<short> slShort
,并尝试用以下方式对其进行初始化
slShort.SetInitVals(250000, 260000, 0, 255000);
编译器知道 slShort
的类型是 CSliderGdiCtrlT<short>
,所以参数 T minVal
、T minVal
、T currVal
的类型都是 short
。然而,它会将参数列表中的字面量视为 int
,并对这些参数应用隐式转换为 short
。结果是 minVal
= -12,144,minVal
= -7144,currVal
= -2144。
如果我们幸运的话,CSliderGdiCtrl::SetInitVals
会捕获错误并返回 false
,但很有可能这些数字是合理的,我们将得到一个完全错误的范围、当前值和精度,并且没有任何错误指示。
解决方案:由于所有类型 T
在编译时都已知,我将相关接口函数声明为模板成员函数,仅当参数类型与 T
不同时才抛出编译时断言。例如
template <typename T>
template <typename T1, typename T2, typename T3>
bool CSliderGdiCtrlT<T>::SetInitVals(T1 minVal, T2 maxVal,
int precision, T3 currVal, bool bRedraw = false)
{
STATIC_CHECK(((IndexOf<SL_TYPELIST, T>::value == IndexOf<SL_TYPELIST, T1>::value)&&
(IndexOf<SL_TYPELIST, T>::value == IndexOf<SL_TYPELIST, T2>::value) &&
(IndexOf<SL_TYPELIST, T>::value == IndexOf<SL_TYPELIST, T3>::value)),
Wrong_Types_In_SetInitVals);
return CSliderGdiCtrl::SetInitVals(minVal, maxVal, precision, currVal, bRedraw);
}
如果条件评估为 false
,编译时断言会向编译器提交一个未定义的构造(例如,结构或函数)。它有许多实现,包括 BOOST_STATIC_ASSERT
和 C++0x 的 C++ static_assert
。
我从 Andrei Alexandrescu 的 Loki 库文件 static_check.h 中选择了 STATIC_CHECK
的代码(我将模板参数从 int
更改为 bool
)
template<bool> struct CompileTimeError;
template<> struct CompileTimeError<true> {};
#define STATIC_CHECK(expr, msg) \
{ CompileTimeError<((expr) != 0)> ERROR_##msg; \
(void)ERROR_##msg; }
这里,expr
是要评估的表达式,msg
是编译器错误消息中函数的名称。例如,如果 msg = SetInitVals
,错误消息是
error C2079: 'ERROR_ Wrong_Types_In_SetInitVals' uses
undefined struct 'SliderGdi::CompileTimeError<__formal>'
...(place for the source file name and line number): see reference
to function template instantiation
'bool SliderGdi::CSliderGdiCtrlT<T>::SetInitVals<short,int,short>
T1,T2,int,T3,bool)' being compiled
with
[
T=short,
T1=short,
T2=int,
T3=short
]
Must be T2=short.
运行时异常
除了向 CSliderGdiCtrl<T>
的成员函数提供错误的类型外,还有其他方法可以搞乱参数。例如,你可能会向函数 CSliderGdiCtrlT<T>::SetInitVals(T1 minVal, T2 maxVal, int precision, T3 currVal, bool bRedraw = false)
传递 minVal > maxVal
、超出范围 (minVal
, maxVal
) 的 currVal
或过大的精度值。在这种情况下,函数将返回 false
。
我决定不对这类错误抛出异常。尽管如此,你可以捕获返回值并抛出异常(invalid_argument
或 out_of_range
)。
图形界面和外观
我使用 GDI+ 作为图形界面,因为我需要线性渐变和渐变画刷以及半透明颜色。此外,GDI+ 允许浮点坐标,这可能会提高当前值 (double
) 到滑块滑块屏幕坐标以及反向转换的准确性。SliderGdiCtrl
的布局和颜色已针对分辨率为 1680x1050 像素的 LCD 宽屏显示器进行了调整。
如前所述,滑块名称和滑块当前值集成在滑块窗口中。
滑块控件的典型布局如下图所示,采用默认颜色
函数 CSliderGdiCtrl::InitSliderCtl(Gdiplus::Graphics& gr)
计算滑块布局并将其存储在 CSliderGdiCtrl
数据成员中(矩形、字体等)。
如果滑块的范围、精度、当前值、大小或位置发生更改,则从 CSliderGdiCtrl::OnPaint()
调用此函数。如果用户或应用程序更改了滑块位置,也会调用此函数。技术上,如果数据成员 CSliderGdiCtrl::m_slStat = NOINIT
,则会重新计算布局。
为了消除闪烁,所有绘图都在内存位图上执行。在 GDI+ 中,它的样子是这样的:
void CSliderGdiCtrl::OnPaint()
{
CPaintDC dc(this); // Device context for painting
Graphics gr(dc.m_hDC); // GDI+ graphics class to paint
Rect rGdi;
gr.GetVisibleClipBounds(&rGdi); // The same as the client rect
if (m_slStat == UNINIT)
{
InitSliderCtl(gr); // Calculate layout, set rectangles
// Prepare to highlite the thumb and to catch WM_MOUSELEAVE
// if mouse is hovering over moving area
}
Bitmap clBmp(rGdi.Width, rGdi.Height); // Memory bitmap
Graphics* grPtr = Graphics::FromImage(&clBmp); // As memDC
grPtr->SetSmoothingMode(SmoothingModeAntiAlias);
GraphicsPath grPath; // To draw rounded rectangles
DrawBar(grPtr, grPath);
if (!m_rValLabelF.IsEmptyArea())// If data label visible
DrawValLabel(grPtr, grPath);
DrawMoveArea(grPtr, grPath);
DrawThumb(grPtr, grPath);
gr.DrawImage(&clBmp, rGdi); // Transfer onto screen
delete grPtr;
}
提示窗口在鼠标右键单击滑块时创建。它是一个分层窗口,创建时带有样式
wS_EX_LAYERED|WS_EX_TRANSPARENT|WS_EX_TOPMOST|WS_EX_NOACTIVATE
通过调用函数 SetLayeredWindowAttributes(...)
设置了一个虚假的透明颜色。
有一个函数可以创建 tipWnd
BOOL CTipWnd::CreateTipWnd(const CRect parentRect, const std::wstring& maxStr, int strNmb)
{
BOOL bRes = FALSE;
bRes = CreateEx(
WS_EX_LAYERED|WS_EX_TRANSPARENT|WS_EX_TOPMOST|
WS_EX_NOACTIVATE,
AfxRegisterWndClass(
CS_HREDRAW|CS_VREDRAW|CS_SAVEBITS),
NULL, // Wnd Title (Caption string ?)
WS_POPUP,
0, 0, 0, 0, // Zero Rectangle
NULL,
NULL,
0);
if (bRes) // Calculate the tipWnd layout and set a
{
// transparent color
Graphics* grPtr = Graphics::FromHWND(m_hWnd);
CRect tipWndRect = SetTipWndRects(grPtr, parentRect, maxStr, strNmb);
Color bkgndCol(Color::Azure);
bRes = SetLayeredWindowAttributes(
bkgndCol.ToCOLORREF(), 0, LWA_COLORKEY);
// Important: use only SetWindowPos
SetWindowPos(&wndBottom, tipWndRect.left,
tipWndRect.top, tipWndRect.Width(),
tipWndRect.Height(), SWP_NOACTIVATE);
delete grPtr;
}
return bRes;
}
最顶层窗口会从启动其创建的滑块中窃取焦点。由滑块来重置焦点。
颜色
为了绘制滑块,我们需要相当多的颜色以及线性渐变和梯度画刷。选择和调整所有颜色和画刷需要很长时间。因此,我决定不允许用户选择所有颜色,尽管我承认有这种需求。这里只是没有实现。
然而,需要通过颜色标记不同的滑块。想想颜色选择器中的红色、绿色和蓝色滑块。因此,用户可能会更改滑块条线性画笔的第一种颜色。
处理用户输入
此枚举描述滑块状态
enum CSliderGdiCtrl::SLIDE_STATUS
{
UNINIT, // Not initialized yet
IDLE, // Out of the slider's bar
LBTNDOWN,// Set after left mouse's click on the
// slider's until LBUTTONUP received
HOVERING,// If left button is up
};
对鼠标事件的响应取决于滑块的状态。
当鼠标位于滑块条上时,滑块状态变为 HOWERING
,滑块颜色变为绿色;此外,滑块准备捕获 WM_MOUSELEAVE
。准备跟踪的代码如下所示:
void CSliderGdiCtrl::OnMouseMove(UINT nFlags, CPoint point)
{
...................
// Prepare to catch WM_MOUSELEAVE; if the mouse is
// too quick, without this hovering colors will persist
if (!m_bMouseTracking)
{
TRACKMOUSEEVENT trMouseEvent;
trMouseEvent.cbSize = sizeof(TRACKMOUSEEVENT);
trMouseEvent.dwFlags = TME_LEAVE;
trMouseEvent.hwndTrack = this->m_hWnd;
m_bMouseTracking = TrackMouseEvent(&trMouseEvent);
}
.......................
}
在 HOWERING
状态下,在条形上单击左键会将焦点设置到滑块。如果光标在滑块移动区域内,则鼠标被捕获,并且光标被限制在此区域内。如果光标在滑块矩形内,它将移动到滑块中心;反之,滑块移动到光标 X 坐标,并计算并显示新的滑块当前值。状态变为 LBTNDOWN
。请注意,在此状态下,左键单击总是将光标定位在滑块中心。
因此,在 LBTNDOWN WM_MOUSEMOVE
中,总是拖动滑块以改变滑块的当前值。鼠标不能离开滑块移动区域:它被捕获了。
WM_LBUTTONUP
释放鼠标。
如前所述,鼠标右键单击显示/销毁提示窗口。此外,提示窗口在响应 WM_MOUSELEAVE
时销毁。
在鼠标事件处理程序中,滑块的当前值从鼠标坐标(滑块位置)计算。相反,为了响应箭头键(WM_KEYDOWN
),我们首先计算当前值,然后计算滑块和光标位置。为什么?因为响应按键事件的光标移动可能非常小。例如,对于范围 10000,增量 1,以及滑块移动区域 100,一个增量等于 0.01 像素的滑块移动。
如果其中一个箭头键按下,滑块不会对鼠标消息做出反应。
查阅源代码并运行演示应用程序以获取更多关于处理用户输入的想法。
源代码和演示项目
SliderGdiCtrlSource.zip 包含三个文件:stdafx.h、SliderGDICtrl.h 和 SliderGDICtrl.cpp。
此存档中的文件 stdafx.h 是一个纯头文件的模拟。如果您在项目中不使用预编译头文件并已包含 SliderGDICtrl.cpp,请也添加 stdafx.h。您无需更改 SliderGDICtrl.cpp 文件中的第 54 行
54 #include "stdafx.h"
我还在此存档中包含了 Doc 文件夹。它包含使用 Doxygen 生成的 SliderGdiCtrl
文档。要开始,您应该在浏览器中打开 index.html 文件。请注意,要使用源代码文件的链接,您必须首先将它们解压到 C:/VS2008/Projects/SliderGdiCtrlLib/SliderGdiCtrlLib/ 文件夹中。
我包含了三个演示项目,以展示如何使用滑块控件。所有项目都以警告级别 4 编译。
第一个演示是一个基于对话框的 MFC 应用程序,SlierGdiT。该对话框有四个滑块控件,一个接受短整数,两个处理双精度浮点数,一个处理长整数。你可以像这样玩它们:
- 启用和禁用滑块以查看外观差异。
- 改变滑块精度。
- 更改滑块大小和位置以查看滑块布局如何变化。
- 更改滑块背景颜色。单击“滑块颜色”按钮后,将显示“颜色”对话框。要恢复默认颜色,请单击“取消”;要更改颜色,请选择颜色并单击“确定”。
- “测试选项”按钮为您提供了一个选项菜单,供您测试和查看。由于鼠标离开滑块窗口时提示窗口会消失,因此这些选项会在两秒延迟后生效。您可以在更改之前返回到滑块并通过鼠标右键单击激活提示窗口。
- 它还展示了当“set”函数返回 false 时如何抛出异常。
我还包含了可执行文件 SliderGdiT.exe。此文件是发布版本,使用静态库中的 MFC。它是一个大文件,但您可以直接运行它。
第二个项目是 SlierGdiCtrlW,一个文档视图应用程序,展示了如何在不使用资源编辑器的情况下创建滑块。
第三个项目 SliderGdiCtrlLib 展示了如何将 CSlierGdiCtrlT 作为库使用。
为了编译和链接代码,我使用了带有 SP1 和 Visual Studio 2008 Feature Pack 的 MS Visual Studio 2008 Standard。您可以从 Microsoft 免费下载此 Feature Pack。
文学
- “N2005: A Maximum Significant Decimal Digits Value for the C++0x Standard Library Numeric Limits”,作者:Paul A Bristow,2006 年。
- “现代 C++ 设计:泛型编程与设计模式应用”,作者:Andrei Alexandrescu,Addison-Wesley。
历史
- 初始版本:2011年4月11日。