Introduction
Hi friends. This is my first article on CodeProject in which I am going to describe you an easy way to subclass an edit control so that it accepts only numeric values and ignore any other keys.
To allow you to customize the control accordingly, two public variables, m_nDigitsAfterDecimalPoint
and m_bAllowNegativeValues
have been declared in the sub-class. Using m_nDigitsAfterDecimalPoint
and m_bAllowNegativeValues
, you can specify: first, how many digits you want the control to accept after the decimal point and second, whether you want to allow a user to enter negative values in the control or not.
So, here we go...
The Source Code
The source code included with this article contains only two files:
- The NumericEdit.h file that declares the
CNumericEdit
class
- The NumericEdit.cpp file that contains the implementation of the
CNumericEdit
class.
The NumericEdit.h file contains the usual stuff needed to create a subclass of an existing class as shown below:
class CNumericEdit : public CEdit
{
private:
struct _LocaleInfo
{
TCHAR chDecimalSymbol;
TCHAR chNegationSymbol;
_LocaleInfo()
{
chDecimalSymbol = _T('.');
chNegationSymbol = _T('-');
CString strT;
int iResult = ::GetLocaleInfo(LOCALE_USER_DEFAULT,
LOCALE_SDECIMAL,
strT.GetBufferSetLength(2), 2);
strT.ReleaseBuffer();
if (iResult)
chDecimalSymbol = strT[0];
iResult = ::GetLocaleInfo(LOCALE_USER_DEFAULT,
LOCALE_SNEGATIVESIGN,
strT.GetBufferSetLength(2), 2);
strT.ReleaseBuffer();
if (iResult)
chNegationSymbol = strT[0];
}
};
public:
CNumericEdit() : CEdit() {
m_bAllowNegativeValues = true;
m_chDigitsAfterDecimal = 6;
}
public:
virtual BOOL PreTranslateMessage(MSG *pMSG);
private:
afx_msg LRESULT OnPaste(WPARAM wParam, LPARAM lParam);
private:
CString GetClipboardText(void) const;
public:
bool m_bAllowNegativeValues;
TCHAR m_chDigitsAfterDecimal;
private:
static const _LocaleInfo DefUserLocale;
DECLARE_MESSAGE_MAP()
};
Note that the two variables declared in the class are accessible publicly. So, it is very simple to customize the behaviour of the class as per your needs. What you can do, for example, is that in your OnInitDialog
, you can set these variables appropriately and the CNumericEdit
class will act accordingly. For instance, if you set m_bAllowNegativeValues
to false, the CNumericEdit
class will not allow the user to enter negative values, i.e., it will not accept the negative symbol key.
EXAMPLE:
BOOL CMyDialog::OnInitDialog()
{
CDialog::OnInitDialog();
.
.
.
m_editNumeric.m_bAllowNegativeValues = false;
return (TRUE);
}
Detailed Analysis
Now lets take a look into NumericEdit.cpp file and understand it step-by-step:
The CNumericEdit
class overrides the default implementation of PreTranslateMessage
, as shown below, to process WM_KEYDOWN
message before it is translated and dispatched. Take a look:
BOOL CNumericEdit::PreTranslateMessage(MSG *pMSG)
{
CString strBuffer;
DWORD dwSelStart = 0,
dwSelEnd = 0;
int iPos = 0;
UINT nKeyCode = pMSG->wParam;
if (pMSG->message != WM_KEYDOWN)
goto LForwardMsg;
if ( (nKeyCode == _T('C') || nKeyCode == _T('X')
|| nKeyCode == _T('V')) &&
(::GetKeyState(VK_CONTROL) & 0x8000) )
goto LForwardMsg;
.
.
.
Before processing any other key, we look for the key combination of CTRL+X, CTRL+C, or CTRL+V. These keys trigger the cut, copy, and paste operation in an edit control. In case the user is performing a paste operation on the edit control, our WM_PASTE
handler will be executed, whose purpose is to collect the clipboard data, if CF_TEXT
clipboard format is available, and parse a number out of the extracted text. For this, another member function GetClipboardText
has been defined as shown below:
CString CNumericEdit::GetClipboardText(void) const
{
CString strClipBrdText;
if (::IsClipboardFormatAvailable(CF_TEXT))
{
if (::OpenClipboard(m_hWnd))
{
HANDLE hClipBrdData = NULL;
if ((hClipBrdData = ::GetClipboardData(CF_TEXT)) != NULL)
{
LPTSTR lpClipBrdText = (LPTSTR)::GlobalLock(hClipBrdData);
if (lpClipBrdText)
{
strClipBrdText = lpClipBrdText;
::GlobalUnlock(hClipBrdData);
}
}
VERIFY(::CloseClipboard());
.
.
.
The code shown above extract the textual data from the clipboard, if available. As soon as the data is retrieved, the next step is to parse numeric data out of the retrieved text:
.
.
.
bool bHasDecimal = false;
int iStart = 0, iCount = 0;
LPTSTR lpsz = strClipBrdText.LockBuffer();
for (; *lpsz == _T(' ') || *lpsz == _T('\t'); iStart++)
lpsz = ::_tcsinc(lpsz);
if (*lpsz == DefUserLocale.chNegationSymbol)
{
if (!m_bAllowNegativeValues)
return ((LPCTSTR)NULL);
++iCount;
lpsz = ::_tcsinc(lpsz);
}
while (*lpsz != _T('\0'))
{
if (!::_istdigit(*lpsz))
{
if ( (m_chDigitsAfterDecimal) &&
(*lpsz != DefUserLocale.chDecimalSymbol) )
break;
if (bHasDecimal)
break;
bHasDecimal = true;
}
++iCount;
lpsz = ::_tcsinc(lpsz);
}
strClipBrdText.UnlockBuffer();
if ( (!iStart) && (!iCount) )
strClipBrdText.Empty();
else
strClipBrdText = strClipBrdText.Mid(iStart, iCount);
As shown above, first of all, leading spaces and tabs are skipped, and while parsing the numeric value out of the clipboard data, only digits, decimal symbol, and a negative sign is considered valid. Moreover, a negative symbol is allowed only on the first character position and that too only if m_bAllowNegativeValues
is set to a value of true. The parsing process stops as soons as it encounters anything but the above specified symbols and digits. (Yes, that includes CR-LFs too)
Once parsing is complete, our WM_PASTE
handler OnPaste
handles the rest of the process to paste/replace the numeric text appropriately. The complete implementation of OnPaste
is shown below:
LRESULT CNumericEdit::OnPaste(WPARAM wParam, LPARAM lParam)
{
UNUSED_ALWAYS(wParam);
UNUSED_ALWAYS(lParam);
CString strClipBrdText,
strCtrlText;
DWORD dwSelStart = 0,
dwSelEnd = 0;
int iDecimalPos[2];
strClipBrdText = GetClipboardText();
if (strClipBrdText.IsEmpty())
{
::MessageBeep(MB_ICONEXCLAMATION);
return (0L);
}
GetWindowText(strCtrlText);
SendMessage(EM_GETSEL, (WPARAM)&dwSelStart, (LPARAM)&dwSelEnd);
if ( (strClipBrdText[0] == DefUserLocale.chNegationSymbol)
&& (dwSelStart) )
{
::MessageBeep(MB_ICONEXCLAMATION);
return (0L);
}
if (strCtrlText.GetLength() &&
strCtrlText[0] == DefUserLocale.chNegationSymbol)
{
if (dwSelEnd == dwSelStart)
{
::MessageBeep(MB_ICONEXCLAMATION);
return (0L);
}
}
if (m_chDigitsAfterDecimal)
{
iDecimalPos[0] = strCtrlText.Find(DefUserLocale.chDecimalSymbol);
iDecimalPos[1] = strClipBrdText.Find(DefUserLocale.chDecimalSymbol);
if ( (iDecimalPos[0] >= 0 && iDecimalPos[1] >= 0) &&
(iDecimalPos[0] < dwSelStart || iDecimalPos[0] > dwSelEnd) )
{
strClipBrdText = strClipBrdText.Left(iDecimalPos[1]);
iDecimalPos[1] = -1;
}
}
strCtrlText = strCtrlText.Left(dwSelStart) +
strClipBrdText +
strCtrlText.Mid(dwSelEnd);
dwSelEnd = dwSelStart + strClipBrdText.GetLength();
iDecimalPos[0] = strCtrlText.Find(DefUserLocale.chDecimalSymbol);
if (iDecimalPos[0] >= 0)
{
if (strCtrlText.Mid(iDecimalPos[0] + 1).GetLength()
> m_chDigitsAfterDecimal)
strCtrlText = strCtrlText.Left(iDecimalPos[0]
+ m_chDigitsAfterDecimal + 1);
}
SetWindowText(strCtrlText);
SetSel(dwSelEnd, dwSelEnd);
return (0L);
}
Rest everything is very clearly described in the code itself. Just download the source code and you will definitely find the code very easy to understand and use. If you need to know more about it, or want to report any bug, suggestion, etc., feel free to mail me. I'll try to reply as soon as possible.
Important Notes
- In order to use this code in a dialog-based application, don't forget to use
CNumericEdit
instead of CEdit
class and to subclass the control, simply use DDX_Control
in your dialog class' DoDataExchange
function.
- If you get the
signed/unsigned mismatch
warning, simply wrap the function definition(s) with #pragma
as shown below: #pragma warning(disable: 4018)
BOOL CNumericEdit::PreTranslateMessage(MSG *pMSG)
{
.
.
.
}
#pragma warning(default: 4018)