Click here to Skip to main content
15,867,330 members
Articles / Desktop Programming / MFC
Article

Numeric Edit Control

Rate me:
Please Sign up or sign in to vote.
5.00/5 (18 votes)
2 Mar 20043 min read 148.4K   3.4K   27   26
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:

  1. The NumericEdit.h file that declares the CNumericEdit class
  2. 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

  1. 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.
  2. 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)

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


Written By
Canada Canada
This member has not yet provided a Biography. Assume it's interesting and varied, and probably something to do with programming.

Comments and Discussions

 
QuestionCannot use ctrl+v on numeric edit control Pin
Member 1050741910-Jan-14 17:00
Member 1050741910-Jan-14 17:00 
Questionit doesnt work at this case Pin
wan.rui@qq.com27-Feb-12 23:36
wan.rui@qq.com27-Feb-12 23:36 
AnswerRe: it doesnt work at this case Pin
wan.rui@qq.com28-Feb-12 0:07
wan.rui@qq.com28-Feb-12 0:07 
GeneralDecimal Separator and negative Sign Problem Pin
basti271121-Aug-07 22:35
basti271121-Aug-07 22:35 
GeneralRe: Decimal Separator and negative Sign Problem Pin
wan.rui@qq.com28-Feb-12 0:09
wan.rui@qq.com28-Feb-12 0:09 
Generalstill something missing Pin
mazen4423-Jun-06 12:14
mazen4423-Jun-06 12:14 
GeneralMassive design flaw... Pin
zlatnik20-Oct-05 14:18
zlatnik20-Oct-05 14:18 
GeneralProblems and Ideas Pin
luniv04047-Oct-05 8:00
luniv04047-Oct-05 8:00 
GeneralDDV method to validate data after DoDataExchange Pin
Scott M.G. Su17-Aug-05 20:44
Scott M.G. Su17-Aug-05 20:44 
GeneralModify code to check if the decimal symbol obey m_chDigitsAfterDecimal Pin
Scott M.G. Su17-Aug-05 2:46
Scott M.G. Su17-Aug-05 2:46 
Generalvertical alignment Pin
vivadot21-Jun-04 14:23
vivadot21-Jun-04 14:23 
GeneralRe: It only works properly on the numeric keypad. Pin
gUrM33T28-Apr-04 23:21
gUrM33T28-Apr-04 23:21 
GeneralRe: It only works properly on the numeric keypad. Pin
Miszou29-Apr-04 11:04
Miszou29-Apr-04 11:04 
GeneralRe: It only works properly on the numeric keypad. Pin
gUrM33T30-Apr-04 2:30
gUrM33T30-Apr-04 2:30 
GeneralRe: It only works properly on the numeric keypad.(Another Problem) Pin
flydream10-Jul-04 16:46
flydream10-Jul-04 16:46 
GeneralRe: It only works properly on the numeric keypad.(Another Problem) Pin
mamaqueso31-May-05 11:17
mamaqueso31-May-05 11:17 
GeneralIt only works properly on the numeric keypad. Pin
Miszou30-Mar-04 12:49
Miszou30-Mar-04 12:49 
GeneralRe: It only works properly on the numeric keypad. Pin
gUrM33T30-Mar-04 16:33
gUrM33T30-Mar-04 16:33 
GeneralArticle Updated Pin
gUrM33T5-Mar-04 14:58
gUrM33T5-Mar-04 14:58 
GeneralRe: Article Updated Pin
clayno26-Aug-04 3:55
clayno26-Aug-04 3:55 
GeneralCTRL+V Pin
snakeware3-Mar-04 5:51
snakeware3-Mar-04 5:51 
GeneralRe: CTRL+V Pin
gUrM33T3-Mar-04 15:59
gUrM33T3-Mar-04 15:59 
GeneralRe: CTRL+V Pin
snakeware4-Mar-04 13:40
snakeware4-Mar-04 13:40 
GeneralRe: CTRL+V Pin
gUrM33T4-Mar-04 16:02
gUrM33T4-Mar-04 16:02 
GeneralVery good example Pin
bishbosh023-Mar-04 3:05
bishbosh023-Mar-04 3:05 
One small point not all contrys use the decimal point so this should come from system configuration.

using GetLocaleInfo() Api


General General    News News    Suggestion Suggestion    Question Question    Bug Bug    Answer Answer    Joke Joke    Praise Praise    Rant Rant    Admin Admin   

Use Ctrl+Left/Right to switch messages, Ctrl+Up/Down to switch threads, Ctrl+Shift+Left/Right to switch pages.