
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 fromCEdit
, 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 justCEdit
, and it is even possible to use theCDecimalSupport
class for both WTL and MFC.
The problem
An edit control with theES_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 theES_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 theWM_CHAR
message yourself.
The WTL solution
WTL and ATL use thecuriously recurring template pattern (CRTP) extensively. This makes it possible to combine different extensions for a class via multiple inheritance.
TheCDecimalSupport
class, which handles theWM_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 fromCEdit
orCWindow
, able to handle theWM_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; }return0;}
This is a first try of theOnChar
function. TheOnChar
function will replace the current selection with the decimal point, whenever the point key is pressed. Otherwise, it sets thebHandled
flag tofalse
, so the edit control can handle theWM_CHAR
message itself. SinceCDecimalSupport
doesn't have aReplaceSel
member function and isn't derived from a base class with aReplaceSel
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 parameterT
.
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; }return0;}
WTL uses this technique, calledsimulated dynamic binding, as a replacement for virtual functions.
Using the code (WTL)
First, create a new edit control class that is derived fromCDecimalSupport
, as shown below.
TheCHAIN_MSG_MAP_ALT
enables theCDecimalSupport
class to handle theWM_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'sOnInit
function (and don't forget to set theES_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 ownOnChar
function that callsCDecimalSupport::OnChar
.
class CNumberEdit :public CEdit ,public CDecimalSupport<CNumberEdit>{protected: afx_msgvoid OnChar( UINT nChar, UINT nRepCnt, UINT nFlags ); DECLARE_MESSAGE_MAP()};BEGIN_MESSAGE_MAP(CNumberEdit, CEdit) ON_WM_CHAR()//}}AFX_MSG_MAPEND_MESSAGE_MAP()afx_msgvoid 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 thestrtod
instead of theatof
function (atof
simply returns 0.0 if the input cannot be converted). Thestrtod
function is affected by the program's locale settings, which are changeable via thesetlocale
function. TheTextToDouble
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');}
TheGetDecimalValue
function converts the controls text to a double value. The result istrue
, 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 ofsprintf
, 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);returnstatic_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);returnstatic_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
andSetFixedValue
, to display a double value in the control. The only difference is the interpretation of thecount
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 theCDecimalSupport
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++, theCNumberButton
class doesn't have aReplaceSel
member function?"
In the template world, the code for theOnChar
function is only generated when it is really needed. So, if nobody calls theOnChar
function, no code is generated, and the compiler doesn't complain about a missingReplaceSel
function.
You can use theCNumberButton
class to set a button's text.
CNumberButton btn;btn.Attach(GetDlgItem(ID_APP_ABOUT));btn.SetDecimalValue(2.75,3);//compiles finebtn.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.