Click here to Skip to main content
65,938 articles
CodeProject is changing. Read more.
Articles
(untagged)

Numeric Edit Control

0.00/5 (No votes)
2 Mar 2004 1  
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