Click here to Skip to main content
65,938 articles
CodeProject is changing. Read more.
Articles
(untagged)

Decimal number support for your controls

0.00/5 (No votes)
18 Nov 2007 1  
The WTL way of adding number support to your controls.

Screenshot - decimal_support.jpg

Introduction

In this article, I will describe how to extend the edit control for validating decimal numbers (like in the above picture), and the problems of converting a string to double and vice versa.

Instead of creating a new class that is derived from CEdit, I use multiple inheritance and templates to achieve the goal. This design allows you to combine the number support class with your favorite control, not just CEdit, and it is even possible to use the CDecimalSupport class for both WTL and MFC.

The problem

An edit control with the ES_NUMBER style allows only digits to be entered into the edit control. On Windows XP (and above), the edit control shows a balloon tip (if enabled) in an attempt to enter a non-digit.

Unfortunately, an edit control with the ES_NUMBER style set doesn't accept a decimal point or a negative sign. To get rid of this restriction, you have to subclass the edit control and handle the WM_CHAR message yourself.

The WTL solution

WTL and ATL use the curiously recurring template pattern (CRTP) extensively. This makes it possible to combine different extensions for a class via multiple inheritance.

The CDecimalSupport class, which handles the WM_CHAR message, is a class template without any base class.

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()
...

But, how is such a class, which isn't derived from CEdit or CWindow, able to handle the WM_CHAR message?

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;
}

This is a first try of the OnChar function. The OnChar function will replace the current selection with the decimal point, whenever the point key is pressed. Otherwise, it sets the bHandled flag to false, so the edit control can handle the WM_CHAR message itself. Since CDecimalSupport doesn't have a ReplaceSel member function and isn't derived from a base class with a ReplaceSel member, this code generates an error (C3861 identifier not found).

Fortunately, CDecimalSupport is a base class in the CRTP, so we are able to use the template parameter 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 uses this technique, called simulated dynamic binding, as a replacement for virtual functions.

Using the code (WTL)

First, create a new edit control class that is derived from CDecimalSupport, as shown below.

The CHAIN_MSG_MAP_ALT enables the CDecimalSupport class to handle the WM_CHAR message.

class CNumberEdit : public CWindowImpl<CNumberEdit, CEdit>
                  , public CDecimalSupport<CNumberEdit>
{
public:
    BEGIN_MSG_MAP(CNumberEdit)
        CHAIN_MSG_MAP_ALT(CDecimalSupport<CNumberEdit>, 8)
    END_MSG_MAP()

};

Now, you need to subclass the edit control in the dialog's OnInit function (and don't forget to set the ES_NUMBER style).

class CMainDlg : public ...
{
  ...
  CNumberEdit myNumberEdit;
  ...
};


LRESULT CMainDlg::OnInitDialog(UINT /*uMsg*/, WPARAM /*wParam*/, 
        LPARAM /*lParam*/, BOOL& /*bHandled*/)
{
  ...
  myNumberEdit.SublassWindow(GetDlgItem(IDC_NUMBER));
  ...
}

CDecimalSupport also has some member functions for converting from text to double and vice versa.

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);
  ...
}

Using the code (MFC)

The main difference between the WTL and the MFC usage is the message handling.

In the MFC code, you have to write your own OnChar function that calls 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);
}

Converting the control's text to double

A conversion function from string to double has to deal with two problems:

  • The string may contain invalid characters.
  • The string format could be locale-dependent.

The first problem is solved by using the strtod instead of the atof function (atof simply returns 0.0 if the input cannot be converted). The strtod function is affected by the program's locale settings, which are changeable via the setlocale function. The TextToDouble function changes the decimal separator and the negative sign before it calls _tcstod. I recommend that you leave the locale unchanged in your program.

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');
}

The GetDecimalValue function converts the controls text to a double value. The result is true, if the conversion was successful.

Display a decimal value in a control

If you wish to display a decimal value, you have to make some decisions first:

  • Is the string format locale-dependent? Yes.
  • Is the text length limited? Yes, call SetDecimalValue.
  • Are the digits after the decimal print limited? Yes, call SetFixedValue.
  • Is the result truncated or rounded? Rounded.
  • Display a leading zero (0.5 or .5)? A leading zero is always displayed.
  • Display trailing zeros (2.5 or 2.5000)? Trailing zeros aren't displayed.
  • Support scientific formatting (1.05e6 or 1050000)? Not supported.
  • Display a thousands separator (1,000,000)? Not supported.

I chose to use the _fcvt and the _ecvt functions instead of sprintf, because these functions are unaffected by locale settings.

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 has two member functions, SetDecimalValue and SetFixedValue, to display a double value in the control. The only difference is the interpretation of the count parameter.

  • SetFixedValue: Number of digits after decimal point.
  • SetDecimalValue: Number of digits stored.

The magic of templates

Maybe you wish to display a double value in a button control, is it possible to use the CDecimalSupport for this purpose? Yes, just create a new button class as shown below:

class CNumberButton : public CButton , public CDecimalSupport<CNumberButton>
{
};

Did you ask: "Is this code legal C++, the CNumberButton class doesn't have a ReplaceSel member function?"

In the template world, the code for the OnChar function is only generated when it is really needed. So, if nobody calls the OnChar function, no code is generated, and the compiler doesn't complain about a missing ReplaceSel function.

You can use the CNumberButton class to set a button's text.

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

History

  • 18 Nov 2007: The WM_PASTE message handler added.
  • 10 Nov 2007: Initial version.

License

This article has no explicit license attached to it but may contain usage terms in the article text or the download files themselves. If in doubt please contact the author via the discussion board below.

A list of licenses authors might use can be found here