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:
TCHAR m_DecimalSeparator[5];
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 , WPARAM wParam, LPARAM , BOOL& bHandled)
{
if (wParam == m_DecimalSeparator[0])
{
this->ReplaceSel(m_DecimalSeparator, true);
}
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 , WPARAM wParam, LPARAM , BOOL& bHandled)
{
if (wParam == m_DecimalSeparator[0])
{
T* pT = static_cast<T*>(this);
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 , WPARAM ,
LPARAM , BOOL& )
{
...
myNumberEdit.SublassWindow(GetDlgItem(IDC_NUMBER));
...
}
CDecimalSupport
also has some member functions for converting from text to double and vice versa.
LRESULT CMainDlg::OnInitDialog(UINT , WPARAM ,
LPARAM , BOOL& )
{
...
myNumberEdit.SublassWindow(GetDlgItem(IDC_NUMBER));
myNumberEdit.LimitText(12);
myNumberEdit.SetDecimalValue(3.14159265358979323846);
...
}
LRESULT CMainDlg::OnOK(WORD , WORD wID, HWND , BOOL& )
{
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()
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
{
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));
}
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);
btn.SetDecimalValue(2.75);
History
- 18 Nov 2007: The
WM_PASTE
message handler added.
- 10 Nov 2007: Initial version.