为您的控件提供十进制数支持






4.43/5 (9投票s)
WTL 为您的控件添加数字支持的方式。
引言
在本文中,我将介绍如何扩展编辑控件以验证十进制数字(如上图所示),以及将字符串转换为双精度浮点数和反之的难题。
我没有创建从 CEdit
派生的新类,而是使用多重继承和模板来实现目标。这种设计允许您将数字支持类与您喜欢的控件结合使用,而不仅仅是 CEdit
,甚至可以使用 CDecimalSupport
类来支持 WTL 和 MFC。
问题所在
带有 ES_NUMBER
样式的编辑控件只允许在编辑控件中输入数字。在 Windows XP(及更高版本)上,当尝试输入非数字字符时,编辑控件会显示一个气球提示(如果已启用)。
不幸的是,设置了 ES_NUMBER
样式的编辑控件不接受小数点或负号。要摆脱这个限制,您必须进行子类化编辑控件并自行处理 WM_CHAR
消息。
WTL 解决方案
WTL 和 ATL 广泛使用“奇偶递归模板模式”(CRTP)。这使得可以通过多重继承将不同的扩展组合到一个类中。
CDecimalSupport
类处理 WM_CHAR
消息,它是一个类模板,没有任何基类。
template <class T> class CDecimalSupport { public: //stores the decimal point returned by GetLocaleInfo TCHAR m_DecimalSeparator[5]; //stores the negative sign returned by GetLocaleInfo TCHAR m_NegativeSign[6]; BEGIN_MSG_MAP(CDecimalSupport) ALT_MSG_MAP(8) MESSAGE_HANDLER(WM_CHAR, OnChar) END_MSG_MAP() ...
但是,这样一个不派生自 CEdit
或 CWindow
的类是如何处理 WM_CHAR
消息的呢?
LRESULT OnChar(UINT /*uMsg*/, WPARAM wParam, LPARAM /*lParam*/, BOOL& bHandled) { if (wParam == m_DecimalSeparator[0]) //The '.' key was pressed { this->ReplaceSel(m_DecimalSeparator, true); //error C3861 } else { bHandled = false; } return 0; }
这是 OnChar
函数的第一次尝试。当按下小数点键时,OnChar
函数将用小数点替换当前选定的内容。否则,它会将 bHandled
标志设置为 false
,以便编辑控件可以自行处理 WM_CHAR
消息。由于 CDecimalSupport
没有 ReplaceSel
成员函数,也不是从具有 ReplaceSel
成员的基类派生的,因此此代码会生成一个错误(C3861 标识符未找到)。
幸运的是,CDecimalSupport
是 CRTP 中的一个基类,因此我们可以使用模板参数 T
。
LRESULT OnChar(UINT /*uMsg*/, WPARAM wParam, LPARAM /*lParam*/, BOOL& bHandled) { if (wParam == m_DecimalSeparator[0]) //The '.' key was pressed { //works if CDecimalSupport<T> is a base class of T T* pT = static_cast<T*>(this); //compiles if T has a RelpaceSel function pT->ReplaceSel(m_DecimalSeparator, true); } else { bHandled = false; } return 0; }
WTL 使用这种称为模拟动态绑定的技术来替代虚函数。
使用代码(WTL)
首先,创建一个新的编辑控件类,该类派生自 CDecimalSupport
,如下所示。
CHAIN_MSG_MAP_ALT
使 CDecimalSupport
类能够处理 WM_CHAR
消息。
class CNumberEdit : public CWindowImpl<CNumberEdit, CEdit> , public CDecimalSupport<CNumberEdit> { public: BEGIN_MSG_MAP(CNumberEdit) CHAIN_MSG_MAP_ALT(CDecimalSupport<CNumberEdit>, 8) END_MSG_MAP() };
现在,您需要在对话框的 OnInit
函数中对编辑控件进行子类化(别忘了设置 ES_NUMBER
样式)。
class CMainDlg : public ... { ... CNumberEdit myNumberEdit; ... }; LRESULT CMainDlg::OnInitDialog(UINT /*uMsg*/, WPARAM /*wParam*/, LPARAM /*lParam*/, BOOL& /*bHandled*/) { ... myNumberEdit.SublassWindow(GetDlgItem(IDC_NUMBER)); ... }
CDecimalSupport
还提供了一些成员函数,用于从文本转换为双精度浮点数以及反之。
LRESULT CMainDlg::OnInitDialog(UINT /*uMsg*/, WPARAM /*wParam*/, LPARAM /*lParam*/, BOOL& /*bHandled*/) { ... myNumberEdit.SublassWindow(GetDlgItem(IDC_NUMBER)); myNumberEdit.LimitText(12); myNumberEdit.SetDecimalValue(3.14159265358979323846); ... } LRESULT CMainDlg::OnOK(WORD /*wNotifyCode*/, WORD wID, HWND /*hWndCtl*/, BOOL& /*bHandled*/) { double d; bool ok = myNumberEdit.GetDecimalValue(d); ... }
使用代码(MFC)
WTL 和 MFC 用法的区别主要在于消息处理。
在 MFC 代码中,您必须编写自己的 OnChar
函数,该函数调用 CDecimalSupport::OnChar
。
class CNumberEdit : public CEdit , public CDecimalSupport<CNumberEdit> { protected: afx_msg void OnChar( UINT nChar, UINT nRepCnt, UINT nFlags ); DECLARE_MESSAGE_MAP() }; BEGIN_MESSAGE_MAP(CNumberEdit, CEdit) ON_WM_CHAR() //}}AFX_MSG_MAP END_MESSAGE_MAP() afx_msg void CNumberEdit::OnChar( UINT nChar, UINT nRepCnt, UINT nFlags ) { BOOL bHandled = false; CDecimalSupport<CNumberEdit>::OnChar(0, nChar, 0, bHandled); if (!bHandled) CEdit::OnChar(nChar , nRepCnt, nFlags); }
将控件文本转换为双精度浮点数
从字符串到双精度浮点数的转换函数必须处理两个问题:
- 字符串可能包含无效字符。
- 字符串格式可能因区域设置而异。
第一个问题是通过使用 strtod
而不是 atof
函数来解决的(atof
在输入无法转换时仅返回 0.0)。strtod
函数受程序区域设置的影响,可以通过 setlocale
函数进行更改。TextToDouble
函数在调用 _tcstod
之前会更改十进制分隔符和负号。我建议您在程序中保持区域设置不变。
bool GetDecimalValue(double& d) const { TCHAR szBuff[limit]; static_cast<const T*>(this)->GetWindowText(szBuff, limit); return TextToDouble(szBuff , d); } bool TextToDouble(TCHAR* szBuff , double& d) const { //replace the decimal separator with . TCHAR* point = _tcschr(szBuff , m_DecimalSeparator[0]); if (point) { *point = localeconv()->decimal_point[0]; if (_tcslen(m_DecimalSeparator) > 1) _tcscpy(point + 1, point + _tcslen(m_DecimalSeparator)); } //replace the negative sign with - if (szBuff[0] == m_NegativeSign[0]) { szBuff[0] = _T('-'); if (_tcslen(m_NegativeSign) > 1) _tcscpy(szBuff + 1, szBuff + _tcslen(m_NegativeSign)); } TCHAR* endPtr; d = _tcstod(szBuff, &endPtr); return *endPtr == _T('\0'); }
GetDecimalValue
函数将控件的文本转换为双精度浮点数值。如果转换成功,则结果为 true
。
在控件中显示十进制值
如果您希望显示十进制值,则必须先做一些决定:
- 字符串格式是否因区域设置而异?是。
- 文本长度是否有限制?是,调用
SetDecimalValue
。 - 小数点后的位数是否有限制?是,调用
SetFixedValue
。 - 结果是截断还是四舍五入?四舍五入。
- 显示前导零(0.5 或 .5)?始终显示前导零。
- 显示尾随零(2.5 或 2.5000)?不显示尾随零。
- 支持科学计数法(1.05e6 或 1050000)?不支持。
- 显示千位分隔符(1,000,000)?不支持。
我选择使用 _fcvt
和 _ecvt
函数而不是 sprintf
,因为这些函数不受区域设置的影响。
int SetFixedValue(double d, int count) { int decimal_pos; int sign; char* digits = _fcvt(d,count,&decimal_pos,&sign); TCHAR szBuff[limit]; DigitsToText(szBuff, limit , digits, decimal_pos, sign); return static_cast<T*>(this)->SetWindowText(szBuff); } int SetDecimalValue(double d, int count) { int decimal_pos; int sign; char* digits = _ecvt(d,count,&decimal_pos,&sign); TCHAR szBuff[limit]; DigitsToText(szBuff, limit , digits, decimal_pos, sign); return static_cast<T*>(this)->SetWindowText(szBuff); } int SetDecimalValue(double d) { return SetDecimalValue(d , min(limit , static_cast<const />(this)->GetLimitText()) - 2); }
CDecimalSupport
有两个成员函数 SetDecimalValue
和 SetFixedValue
,用于在控件中显示双精度浮点值。唯一的区别是 count
参数的解释。
SetFixedValue
:小数点后的位数。SetDecimalValue
:存储的位数。
模板的魔力
也许您想在按钮控件中显示双精度浮点值,是否可以使用 CDecimalSupport
来实现此目的?是的,只需创建一个新的按钮类,如下所示:
class CNumberButton : public CButton , public CDecimalSupport<CNumberButton> { };
您是否问过:“这段代码是否合法的 C++,CNumberButton
类没有 ReplaceSel
成员函数?”
在模板世界中,OnChar
函数的代码仅在真正需要时才生成。因此,如果没有人调用 OnChar
函数,就不会生成任何代码,编译器也不会抱怨缺少 ReplaceSel
函数。
您可以使用 CNumberButton
类来设置按钮的文本。
CNumberButton btn;
btn.Attach(GetDlgItem(ID_APP_ABOUT));
btn.SetDecimalValue(2.75, 3); //compiles fine
btn.SetDecimalValue(2.75);
//error C2039: GetLimitText is not a member of CNumberButton
历史
- 2007 年 11 月 18 日:添加了
WM_PASTE
消息处理程序。 - 2007 年 11 月 10 日:初始版本。