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

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

starIconstarIconstarIconstarIcon
emptyStarIcon
starIcon

4.43/5 (9投票s)

2007年11月10日

CPOL

4分钟阅读

viewsIcon

48480

downloadIcon

1843

WTL 为您的控件添加数字支持的方式。

Screenshot - decimal_support.jpg

引言

在本文中,我将介绍如何扩展编辑控件以验证十进制数字(如上图所示),以及将字符串转换为双精度浮点数和反之的难题。

我没有创建从 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()
...

但是,这样一个不派生自 CEditCWindow 的类是如何处理 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 有两个成员函数 SetDecimalValueSetFixedValue,用于在控件中显示双精度浮点值。唯一的区别是 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 日:初始版本。
为您的控件提供十进制数支持 - CodeProject - 代码之家
© . All rights reserved.