Published on

A Numeric Edit Control in MFC

Authors

It feels a bit late in the day to be talking about MFC, but some of the software at my company is still written with it. It does not seem like a technology with much of a future, so I have never been eager to study it deeply, and I am still not very comfortable with serious GUI work. These are my notes from implementing input restrictions on an edit control last month.

Restricting input

Create a class derived from CEdit and override OnChar.

#ifndef NUMERIC_EDIT_H_
#define NUMERIC_EDIT_H_

class CNumericEdit : public CEdit
{
  DECLARE_DYNAMIC(CNumericEdit)

public:
  CNumericEdit();
  virtual ~CNumericEdit();

  void SetRoundPlaceValue(const int RoundPlaceValue)
  {
    m_iRoundPlaceValue = RoundPlaceValue;
  }

  int GetRoundPlaceValue() const
  {
    return m_iRoundPlaceValue;
  }

protected:
  afx_msg void OnChar(UINT nChar, UINT nRepCnt, UINT nFlags);

  CString m_strDelim;
  int m_iRoundPlaceValue;

  DECLARE_MESSAGE_MAP()
};

#endif // NUMERIC_EDIT_H_
#include "stdafx.h"
#include "NumericEdit.h"

IMPLEMENT_DYNAMIC(CNumericEdit, CEdit)

CNumericEdit::CNumericEdit()
{
  // determine the decimal delimiter buffer size
  const int nBuffLen = ::GetLocaleInfo( LOCALE_USER_DEFAULT, LOCALE_SDECIMAL, NULL, 0 );
  _ASSERT( nBuffLen > 0 );

  // get the decimal number delimiter
  const int nResult = ::GetLocaleInfo( LOCALE_USER_DEFAULT, LOCALE_SDECIMAL,
      m_strDelim.GetBuffer(nBuffLen), nBuffLen );
  _ASSERT(nResult != 0);
  m_strDelim.ReleaseBuffer();
}

CNumericEdit::~CNumericEdit()
{
}

BEGIN_MESSAGE_MAP(CNumericEdit, CEdit)
  ON_WM_CHAR()
END_MESSAGE_MAP()

void CNumericEdit::OnChar(UINT nChar, UINT nRepCnt, UINT nFlags)
{
  CString strText;
  GetWindowText(strText);

  int iStart, iEnd;
  GetSel(iStart, iEnd);

  // If the decimal point was entered
  if (nChar == m_strDelim)
  {
    // Do not allow a decimal point if the field is empty
    if (strText.IsEmpty())
    {
      return;
    }
    else
    {
      // Do not allow another decimal point if one already exists
      if (strText.Find(m_strDelim) >= 0)
        return;
    }
  }

  // Minus can only be entered when the field is empty
  if (nChar == '-')
  {
    if (!strText.IsEmpty())
      return;
  }

  // Limit the number of digits after the decimal point
  if (nChar >= '0' && nChar <= '9' &&
      strText.Find(m_strDelim) != -1)
  {
    int iLength = strText.GetLength();
    int iRoundPlaceValue = strText.Find(m_strDelim);

    if (iStart == iEnd)
    {
      if (iStart > iRoundPlaceValue &&
          (iLength - iRoundPlaceValue) > m_iRoundPlaceValue)
        return;
    }
  }

  if ((nChar >= '0' && nChar <= '9') ||
      (nChar == m_strDelim) ||
      (nChar == '-') ||
      (nChar == '\b'))
  {
    CEdit::OnChar(nChar, nRepCnt, nFlags);
  }
}

Retrieving the value from a dialog

You need to be careful when launching a dialog and retrieving the entered value as shown below. Once DoModal() returns IDOK, the controls inside the dialog have already been destroyed, so you can no longer access the value from the control directly. In that case, define a CString member variable and update it with DDX_Text.

CMyDialog dialog;
if (dialog.DoModal() == IDOK) {
  // At this point, the controls inside CMyDialog no longer exist
  CString strText;
  dialog.m_editValue.GetWindowText(strText); // assertion error
}

Applying both input restriction and value retrieval

Define both a CEdit variable and a CString variable, then add DDX_Control and DDX_TEXT inside DoDataExchange.

void CMyDialog::DoDataExchange(CDataExchange* pDX)
{
  CDialog::DoDataExchange(pDX);

  // For input restriction: the class that overrides OnChar
  DDX_Control(pDX, IDC_EDIT_VALUE, m_editValue);

  // For retrieving the value after the dialog is gone
  DDX_Text(pDX, IDC_EDIT_VALUE, m_strValue);
}

Checking the allowed range of the input

Use dialog data validation. Add the following call inside DoDataExchange.

void CMyDialog::DoDataExchange(CDataExchange* pDX)
{
  CDialog::DoDataExchange(pDX);

  // For input restriction: the class that overrides OnChar
  DDX_Control(pDX, IDC_EDIT_VALUE, m_editValue);

  // For retrieving the value after the dialog is gone
  DDX_Text(pDX, IDC_EDIT_VALUE, m_strValue);

  // Check the valid range of the input value
  // Standard DDV functions for other types also exist
  DDV_MinMaxDouble(pDX, atof(m_strValue), 0.0, 1.0);
}

Validating multiple edit controls together

Sometimes the validity of the data depends on the relationship between two edit controls. For example, if the user enters an upper bound and a lower bound, then the upper bound has to be greater than the lower bound. In that case, create a custom validation function.

void DDV_CheckLowerUpper(CDataExchange* pDX, double lower, double upper)
{
  if (lower > upper) {
    AfxMessageBox(_T("Please enter a lower bound smaller than the upper bound."), MB_ICONWARNING);
    pDX->Fail();
  }
}

Add that function to DoDataExchange.

void CMyDialog::DoDataExchange(CDataExchange* pDX)
{
  CDialog::DoDataExchange(pDX);

  // For input restriction: the class that overrides OnChar
  DDX_Control(pDX, IDC_EDIT_LOWER_VALUE, m_editLower);
  DDX_Control(pDX, IDC_EDIT_UPPER_VALUE, m_editUpper);

  // For retrieving the values after the dialog is gone
  DDX_Text(pDX, IDC_EDIT_LOWER_VALUE, m_strLower);
  DDX_Text(pDX, IDC_EDIT_UPPER_VALUE, m_strUpper);

  // Check the valid range of the input values
  // Standard DDV functions for other types also exist
  DDV_MinMaxDouble(pDX, atof(m_strLower), 0.0, 1.0);
  DDV_MinMaxDouble(pDX, atof(m_strUpper), 0.0, 1.0);

  // If both values are present, check that lower < upper
  if (!m_strLower.IsEmpty() && !m_strUpper.IsEmpty()) {
    DDV_CheckLowerUpper(pDX, atof(m_strLower), atof(m_strUpper));
  }
}

Reference