Click here to Skip to main content
15,881,715 members
Articles / Desktop Programming / MFC
Article

A Validating Edit Control

Rate me:
Please Sign up or sign in to vote.
4.20/5 (5 votes)
3 Nov 2000 127K   1.6K   37   13
A very informative, user-oriented validation edit control.

The Problem

The DDV mechanism is far too primitive to be useful. It only validates on the OK button (or, more specifically, on the UpdateData that accompanies it), which delays the validation until far too late. It also issues error messages that are related to the representation in the program, not to the problem domain. I have thus avoided this toy mechanism and written my own validation code. I also find it particularly annoying that I am allowed to click the OK button when there is an error; this violates the basic principles of GUI design. So DDV is not only hard for the user to use, it actually violates the GUI design guidelines since normally you must click a button which is not actually valid to click before the DDV mechanism can be invoked!

Sure, it makes it easier to program. But the goal is not to make applications easy to program as much as it is to make applications easy to use. I believe my techniques contribute significantly to the latter. The fact that they are not easier to program is of little consequence.

This particular example is useful as it illustrates several useful techniques. These include:

  • Showing how to validate input on a character-by-character basis
  • Providing useful feedback immediately as to the correctness of the input
  • Showing how to use a ToolTip to indicate the reason that the OK button is disabled.

I have used all of these techniques in some form or other in building very informative, user-oriented validation methods.

The Solutions

The first thing I include is a validating edit control. This particular control solves a problem many users ask for: an edit control that validates floating-point input. However, you can replace the FSM with one that validates dates, times, Social Security Numbers, or any other textual form you can parse. The validation does not have to be limited to simple parsing, although it is clear from the fact that every change initiates a validation that you do not want to do some sort of database lookup on every character. In such a case, you would more likely do the validation on the WM_KILLFOCUS (OnKillFocus) event. But that's a different problem than the one this control addresses.

The way I handle this is to handle the reflected WM_COMMAND/EN_CHANGE message. When the contents change, I read the entire string and reparse it. This is one of the methodological changes from traditional getch-style input where you could simply run the FSM on each keystroke. In Windows, the keystrokes have nothing to do with the order of the content of the edit control, because the user can reposition the input caret anywhere in the string. So the entire string must be reparsed, from the start, each time.

Syntax Check

In this case, I parse the floating point number using (a subset of) the syntax specified for the atof function. (I don't accept D or d as exponent indicators).

[whitespace] [sign] [digits] [.digits] [{ e | E} [sign] digits]

(Actually, I don't let the user type the whitespace in, but we'll talk about that below). The parsing is done via a Finite State Machine (FSM) which takes the current state and the current character and decodes against a table indicating the next state. The table is encoded as a sequence of case statements inside a switch. In each match, I can do one or more of the following:

  • "Eat" the character, removing it from the input stream, or leaving it for the next state to process
  • Set the next state
  • Set an indicator as to whether the string is complete, incomplete, or erroneous.

This is encoded as shown (partially) below. To set the "indicator", I set a brush value to the pointer to a predefined brush. If the brush ever gets set to the "error" indicator, the loop ends.

int state = S0;
for(int i = 0; brush != &errorBrush && i < s.GetLength();)
   { /* scan string */
    TCHAR ch = s[i];
    switch(MAKELONG(state, ch))
       { /* states */
        case MAKELONG(S0, _T(' ')):
        case MAKELONG(S0, _T('\t')):
           i++;
          continue;
        case MAKELONG(S0, _T('+')):
        case MAKELONG(S0, _T('-')):
           i++;
           brush = &partialBrush;
           state = IPART;
           continue;
      case MAKELONG(S0, _T('0')):
           ¤
           ¤
           ¤
      case MAKELONG(S0, _T('9')):
           state = IPART;
           continue;
      case MAKELONG(S0, _T('.')):
           i++;
           state = FPART;
           brush = &partialBrush;
           continue;
      case MAKELONG(S0, _T('E')):
      case MAKELONG(S0, _T('e')):
           i++;
           state = ESIGN;
           brush = &partialBrush;
          continue;
      case MAKELONG(IPART, _T('0')):
           ¤
           ¤
           ¤
      case MAKELONG(IPART, _T('9')):
           i++;
           brush = &OKBrush;
           continue;
      case MAKELONG(IPART, _T('.')):
           i++;
           brush = &OKBrush;
           state = FPART;
           continue;
      case MAKELONG(IPART, _T('e')):
      case MAKELONG(IPART, _T('E')):
           i++;
           brush = &partialBrush;
           state = ESIGN;
           continue;
      case MAKELONG(FPART, _T('0')):
      case MAKELONG(FPART, _T('9')):
           i++;
           brush = &OKBrush;
           continue;
       case MAKELONG(FPART, _T('e')):
       case MAKELONG(FPART, _T('E')):
           i++;
           brush = &partialBrush;
           state = ESIGN;
           continue;
      case MAKELONG(ESIGN, _T('+')):
      case MAKELONG(ESIGN, _T('-')):
           i++;
           brush = &partialBrush;
           state = EPART;
           continue;
      case MAKELONG(ESIGN, _T('0')):
      case MAKELONG(ESIGN, _T('1')):
           ¤
           ¤
           ¤
      case MAKELONG(ESIGN, _T('9')):
           state = EPART;
           continue;
      case MAKELONG(EPART, _T('0')):
           ¤
           ¤
           ¤
      case MAKELONG(EPART, _T('9')):
           i++;
           brush = &OKBrush;
           continue;
      default:
           brush = &errorBrush;
           continue;
     } /* states */
   } /* scan string */

To absorb a character, I just increment the pointer (i++). You can create a similar table to parse a date, time, or any other field you can define.

Value Check

Values which are syntactically correct may not meet other criteria. For example, credit card numbers apply a validation algorithm in which one of the digits (usually the low-order one) is some function of the preceding digits. One common scheme years ago would add up the digits modulo 10, then subtract the resulting value from 9, and use the resulting digit as the low-order digit. You might put range checks in place, or validate that the day of the month does not exceed the valid range for the selected month (no February 31st, for example).

In my sample program, I limit the value to have an absolute value of greater than 1.0, a positive value of <= 8192.0f, and a negative value of >= -16384.0f. How do we couple the value range check into the basic validation? The answer is that any time I get a syntactically valid number, I send a message to the parent window requesting that it validate the control. It returns, from the SendMessage, a Boolean value of TRUE or FALSE to indicate if the value is valid.

To do this, I use a user-defined message, in fact, a Registered Window Message, to notify the parent. See my essay on Message Management for more details about this. In this case, I use a static class member variable, which I declare in the class as:

static UINT UWM_CHECK_VALUE;

I initialize this in the .cpp file by doing:

UINT CFloatingEdit::UWM_VALID_CHANGE = ::RegisterWindowMessage( 
  _T("UWM_VALID_CHANGE-{6FE8A4C1-AE33-11d4-A002-006067718D04}"));

I react to this message by placing the following line the the MESSAGE_MAP of the parent. Note that the message request follows the magic ClassWizard comments.

//}}AFX_MSG_MAP
ON_REGISTERED_MESSAGE(CFloatingEdit::UWM_CHECK_VALUE, OnCheckValue)

I have defined the parameters of this message to be as shown:

/***************************************************************************
*                                UWM_CHECK_VALUE
* Inputs:
*        WPARAM: MAKELONG(GetDlgCtrlID(), EN_CHANGE)
*        LPARAM: (LPARAM)(HWND): Window handle
* Result: BOOL
*        TRUE if value is acceptable
*        FALSE if value has an error
* Effect: 
*        If the value is FALSE, the window is marked as an invalid value
*        If the value is TRUE, the window is marked as a valid value
* Notes:
*        This message is sent to the parent of the control as a consequence
*        of the EN_CHANGE notification, but only if the value 
*        is syntactically correct. It may be sent at other times as well
***************************************************************************/

Display Change

In order to indicate the state and provide immediate feedback to the user, I modify the background color of the control. I selected white for an empty control, red for an invalid value, yellow for a value that is syntactically correct so far but is not yet completely valid, and green for values that meet all criteria. These are illustrated below.

Image 1The edit control is empty. It displays as the normal edit background, which on this machine is white.
Image 2The edit control has a syntactically valid value that matches any range constraints (for this example, range checks are not enabled).
Image 3The edit control has a value that has not yet been completed. It is not yet syntactically valid, but what is there so far is correct.
Image 4The edit control has a value that is not syntactically correct. No amount of addition to this value can make it correct.

Control Update

I need to update the controls in response to changes in the validation. This means that I need to enable or disable controls (such as the OK button) whenever the state changes. In order to do this, I send a notification to the parent window indicating that a change in the validation status has occurred. This is another Registered Window Message.

/**************************************************************************
*                                UWM_VALID_CHANGE
* Inputs:
*    WPARAM: (WPARAM)MAKELONG(GetDlgCtrlID(), BOOL) 
*                     Flag indicating new valid state
*    LPARAM: Window handle
* Result: LRESULT
*    Logically void, 0, always
* Effect: 
*    Notifies the parent that the validity of the input value has changed
**************************************************************************/

When I get this message, I invoke the following handler:

LRESULT CValidatorDlg::OnValidChange(WPARAM, LPARAM lParam)
   {
    CWnd::FromHandle((HWND)lParam)->InvalidateRect(NULL);
    updateControls();

    return 0;
   } // CValidatorDlg::OnValidChange

Note the initial InvalidateRect. I have to force a redraw of the entire control when the valid state changes, else the display ends up slightly skewed, with only the background behind the letters being redrawn. As you read the code, you will find several other full-control invalidations that guarantee the correct appearance is maintained. These are necessary because Windows is actually very good at minimizing the redrawing of the control, and if you type the value as shown below without this invalidation, the results are strange indeed. You may even note that some of the values which, if typed left-to-right, should have produced valid values appear in red indicating invalid values. This is because the InvalidateRect had not been done at all. Note also how parts of the background which are outside the character cells is invalid.

Image 5

ToolTips

I found that when there are many controls on the dialog, and several of them affect whether or not a control (such as the OK button) is enabled, it is often quite informative to use a ToolTip to specify the text explaining why the control is not enabled. Often, there are several causes. In this case, the problem is which one to display. ToolTips have a limited string length they can display, and characters beyond this will not be displayed. My strategy is to adopt a mechanism that involves a rule-based system that examines the conditions and displays the first condition that has disabled the control. Sometimes I arrange these rules so that the most common, or easiest-to-fix, condition is the one displayed first.

To enable ToolTips, you must call the function EnableToolTips(TRUE) in the OnInitDialog handler. In addition, I do the ToolTip by using a callback function. This means I must add the following message handler to my MESSAGE_MAP, outside the magic comments:

//}}AFX_MSG_MAP
ON_NOTIFY_EX(TTN_NEEDTEXT, 0, OnToolTipNotify)

The handler is defined as follows:

BOOL CValidatorDlg::OnToolTipNotify(UINT, NMHDR * pNMHDR, LRESULT *)
   {
    TOOLTIPTEXT * pTTT = (TOOLTIPTEXT *)pNMHDR;
    HWND ctl = (HWND)pNMHDR->idFrom;

    UINT msg = 0; // set to message ID of text to display

    if(pTTT->uFlags & TTF_IDISHWND)
       { /* display request */
        UINT id = ::GetDlgCtrlID(ctl);
        switch(id)
           { /* id */
            case IDC_SAMPLE:
                 // This is the 'OK' button
                 // We search for reasons it might be disabled
                 // Only the first reason counts.
                 // Our limit is the tooltip text length, so we
                 // present the errors in the order we think the
                 // user might most easily fix them. For example,
                 // the tab order

Within each case for each control I put the rules, or a call on a function that computes the rules, for that control. For example, to make the messages clearer, I have two internal state functions in the CFloatingEdit control, one of which tells me if the value is syntactically valid and one of which tells me if the value is semantically valid. The routine IsValid returns TRUE only if the value is both syntactically and semantically valid. This allows me to report in more detail why the value is not valid. For the semantic check, I call the same function that the child validity check used, which returns me a string ID for a string to display if there is an error, or 0 if there is not an error. Ultimately, this value is stored in the variable msg. If this value is 0, there is nothing to display, and I return FALSE from the handler; otherwise, I set up some fields in the structure passed in, and return TRUE.

if(msg == 0)
   return FALSE;
pTTT->lpszText = MAKEINTRESOURCE(msg);
pTTT->hinst = AfxGetResourceHandle();
return TRUE;

This causes the string designated by the string ID stored in msg to be displayed as the ToolTip.

Image 6

In a real application, the text would have been more informative, for example, "Temperature value is not in correct format", but the generalization to that should now be obvious.

Input Limitation

To reduce the chances of error even further, I lock out characters that are not actually valid. So for my floating-point control, I disallow all characters except the digits, plus and minus signs, decimal point, and the letters 'e' and 'E'. And backspace. Don't forget backspace!

A common piece of advice that appears is to "put this in the PreTranslateMessage handler of your dialog". This doesn't make any sense to me; it violates any number of issues of abstraction and object orientation. It makes a lot more sense to me to put this in the control that wants to filter the characters. To do this, I write a handler like the one shown below, which appears in my subclassed dialog. This gets created when I add a WM_CHAR handler using ClassWizard, and all I do is fill in the code shown.

void CFloatingEdit::OnChar(UINT nChar, UINT nRepCnt, UINT nFlags) 
   {
    switch(nChar)
       { /* validate */
    case _T('+'):
    case _T('-'):
    case _T('.'):
    case _T('E'):
    case _T('e'):
    case _T('0'):
    case _T('1'):
    case _T('2'):
    case _T('3'):
    case _T('4'):
    case _T('5'):
    case _T('6'):
    case _T('7'):
    case _T('8'):
    case _T('9'):
    case _T('\b'):
       break;
    default:
       MessageBeep(0);
       return;
       } /* validate */
    CEdit::OnChar(nChar, nRepCnt, nFlags);
}

All this does is accept the characters shown, and call the superclass handler, or simply issue a beep and return, thus discarding the character.

Summary

This article summarizes a set of techniques I use extensively in the applications I build. I've not seen this set of ideas documented elsewhere, so this seemed a good topic for an essay.


The views expressed in these essays are those of the author, and in no way represent, nor are they endorsed by, Microsoft.

Send mail to newcomer@flounder.com with questions or comments about this article.
Copyright © 1999 The Joseph M. Newcomer Co. All Rights Reserved.
www.flounder.com/mvp_tips.htm

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
Retired
United States United States
PhD, Computer Science, Carnegie Mellon University, 1975
Certificate in Forensic Science and the Law, Duquesne University, 2008

Co-Author, [i]Win32 Programming[/i]

Comments and Discussions

 
Generalquestion Pin
vivadot21-Jun-04 14:29
vivadot21-Jun-04 14:29 
GeneralRe: question Pin
Joseph M. Newcomer26-Jun-04 16:17
Joseph M. Newcomer26-Jun-04 16:17 
GeneralHelp! Pin
15-Dec-01 10:54
suss15-Dec-01 10:54 
GeneralRe: Help! Pin
Joseph M. Newcomer15-Dec-01 15:13
Joseph M. Newcomer15-Dec-01 15:13 
GeneralApproaches Pin
9-May-01 2:39
suss9-May-01 2:39 
General[Message Removed] Pin
immetoz1-Oct-08 8:49
immetoz1-Oct-08 8:49 
GeneralCopy & Paste problem Pin
9-Dec-00 15:02
suss9-Dec-00 15:02 
GeneralInternationalization Support Pin
Andrew Wirger8-Nov-00 8:49
Andrew Wirger8-Nov-00 8:49 
GeneralToo much for too little Pin
Joseph Dempsey5-Nov-00 5:02
Joseph Dempsey5-Nov-00 5:02 
GeneralRe: Too much for too little Pin
5-Nov-00 8:51
suss5-Nov-00 8:51 
GeneralSimplying your OnChar function Pin
5-Nov-00 2:46
suss5-Nov-00 2:46 
GeneralRe: Simplying your OnChar function Pin
7-Nov-00 2:51
suss7-Nov-00 2:51 
GeneralRe: Simplying your OnChar function Pin
10-Dec-00 8:39
suss10-Dec-00 8:39 

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.