Numeric Edit Control





5.00/5 (15 votes)
Mar 3, 2004
3 min read

153529

3444
How to subclass an edit control in MFC so that it accepts numeric values only
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; // character used for decimal separator TCHAR chNegationSymbol; // character used for negative sign _LocaleInfo() { // // you MAY initialize these variables to // any other value you like, for e.g., zero // chDecimalSymbol = _T('.'); // reasonable default! chNegationSymbol = _T('-'); // reasonable default! CString strT; // temporary string /** * retrieve the symbol used as the decimal separator */ int iResult = ::GetLocaleInfo(LOCALE_USER_DEFAULT, LOCALE_SDECIMAL, strT.GetBufferSetLength(2), 2); strT.ReleaseBuffer(); if (iResult) chDecimalSymbol = strT[0]; /** * retrieve the symbol used as the negative sign */ iResult = ::GetLocaleInfo(LOCALE_USER_DEFAULT, LOCALE_SNEGATIVESIGN, strT.GetBufferSetLength(2), 2); strT.ReleaseBuffer(); if (iResult) chNegationSymbol = strT[0]; } }; // Constructors public: CNumericEdit() : CEdit() { m_bAllowNegativeValues = true; m_chDigitsAfterDecimal = 6; // change this to any value between 0 and 9!!! } // Overridables public: virtual BOOL PreTranslateMessage(MSG *pMSG); // Message Handler(s) private: afx_msg LRESULT OnPaste(WPARAM wParam, LPARAM lParam); // Helpers private: CString GetClipboardText(void) const; // Attributes public: bool m_bAllowNegativeValues; // determines whether negative values are allowed TCHAR m_chDigitsAfterDecimal; // number of digits to allow after decimal 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() { // usual stuff goes here CDialog::OnInitDialog(); . . . m_editNumeric.m_bAllowNegativeValues = false; //where, m_editNumeric is an instance of CNumericEdit 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; // edit control's text buffer DWORD dwSelStart = 0, // selection starting position dwSelEnd = 0; // selection ending position int iPos = 0; // to hold the return value of CString::Find UINT nKeyCode = pMSG->wParam; // virtual key code of the key pressed // not a WM_KEYDOWN message? if (pMSG->message != WM_KEYDOWN) // forward for default processing! goto LForwardMsg; // CTRL+C, CTRL+X, or, CTRL+V? 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; // check to see if clipboard contains textual data? if (::IsClipboardFormatAvailable(CF_TEXT)) { // open the clipboard to get clipboard 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:
// part of CNumericEdit::GetClipboardText . . . /** * parse a number out of the retrieved text */ bool bHasDecimal = false; // decimal symbol flag int iStart = 0, iCount = 0; LPTSTR lpsz = strClipBrdText.LockBuffer(); // skip leading whitespaces (including tabs) for (; *lpsz == _T(' ') || *lpsz == _T('\t'); iStart++) lpsz = ::_tcsinc(lpsz); // is the first character a negative symbol? if (*lpsz == DefUserLocale.chNegationSymbol) { // negative values are not allowed? if (!m_bAllowNegativeValues) return ((LPCTSTR)NULL); ++iCount; lpsz = ::_tcsinc(lpsz); } while (*lpsz != _T('\0')) { if (!::_istdigit(*lpsz)) { if ( (m_chDigitsAfterDecimal) && (*lpsz != DefUserLocale.chDecimalSymbol) ) break; // a decimal symbol is already there? 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, // text available on clipboard strCtrlText; // text in the edit control DWORD dwSelStart = 0, // selection range starting position dwSelEnd = 0; // selection range ending position int iDecimalPos[2]; // 0 = in control, 1 = in clipboard text strClipBrdText = GetClipboardText(); // no (valid) clipboard data available? if (strClipBrdText.IsEmpty()) { ::MessageBeep(MB_ICONEXCLAMATION); return (0L); } // get control's current text and selection range GetWindowText(strCtrlText); SendMessage(EM_GETSEL, (WPARAM)&dwSelStart, (LPARAM)&dwSelEnd); // pasting a negative value somewhere in between the current text? 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); // both control and clipboard text contain a decimal symbol and // decimal symbol doesn't fall within the current selection range if ( (iDecimalPos[0] >= 0 && iDecimalPos[1] >= 0) && (iDecimalPos[0] < dwSelStart || iDecimalPos[0] > dwSelEnd) ) { // extract only that much data from clipboard text // that comes before the decimal symbol position strClipBrdText = strClipBrdText.Left(iDecimalPos[1]); // set iDecimalPos[1] to -1 to indicate that now // there is no decimal symbol in the clipboard text! iDecimalPos[1] = -1; } } strCtrlText = strCtrlText.Left(dwSelStart) + strClipBrdText + strCtrlText.Mid(dwSelEnd); dwSelEnd = dwSelStart + strClipBrdText.GetLength(); // if count of digits after decimal symbol exceeds the // value specified by m_chDigitsAfterDecimal, then, // truncate the rest of the string 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); // position caret, scroll if necessary! // Do not call CEdit::Paste from within this function!!! 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 ofCEdit
class and to subclass the control, simply useDDX_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)