Introduction

Edit controls are often used to enter dates, times, currency values, or preformatted numeric data such as phone numbers. A control that allows its user to only enter a specific type of data is useful for several reasons:

The user may do less typing if the control automatically fills in characters such as dashes or slashes.

The entered data is much more likely to be valid when the user "submits" it.

The classes presented in this article provide these benefits for the most popular types of data that may be entered: dates, times, decimals, integers, currency amounts, formatted numerics, and restricted alphanumerics.

Background

A while back I worked on a project which required edit controls with very strict behavior. One was a date control which needed to accept dates in mm/dd/yyyy format only. The user would be allowed to enter just the numbers and the slashes would be filled in automatically (unlike Microsoft’s Date control). The control would also prevent the user from messing up the format, by, for example, deleting the second character. Another edit control was used for entering dollar amounts and would automatically insert commas for the thousand’s separator.

I went on the hunt for a set of controls that would give me this behavior and I took a look at Masked Edit Control from Dundas Software. Unfortunately it wasn’t specific enough, especially when it came to dates. I needed more than just a masked control; I needed to make sure the value would always be valid. I also didn’t feel like spending time modifying or extending Dundas’s code to make it work like I needed it, so I took a big gulp and decided to do it all from scratch. What you see now is the result of that effort, which I finally got around to publishing.

Classes

All classes derive from CEdit and are prefixed with CAMS (AMS are my company’s initials). The following table gives the class names and descriptions:

I decided to give all my CEdit-derived classes a common base class for any common methods they may need. The result is this class which only has minor enhancements to the CEdit class, but serves as a pseudo-namespace for the Behavior classes used by the CAMSEdit-derived classes.

Member

Description

voidSetText(const CString& strText)

Sets the text for the control. This text is validated against the controls proper format.

CString GetText() const

Retrieves the text in the control.

CString GetTrimmedText() const

Retrieves the text in the control without leading or trailing spaces.

voidSetBackgroundColor(COLORREF rgb)

Sets the control's background color.

COLORREF GetBackgroundColor() const

Retrieves the control's background color.

voidSetTextColor(COLORREF rgb)

Sets the control's text color.

COLORREF GetTextColor() const

Retrieves the control's text color.

boolIsReadOnly() const

Returns true if the control is read-only.

virtualboolShouldEnter(TCHAR c) const;

This protected member is called directly by all classes right before a typed-in character is to be shown. By default it always returns true. But you may override it (in a derived class) and make it return false if the user enters a character you don't want to allow.

This class is used for general alphanumeric input with the exception of whatever characters you specify to not be allowed. So if you need to prevent certain characters from being entered, this class is useful. It can also restrict the length of the text.

Member

Description

voidSetInvalidCharacters(const CString& strInvalidChars)

Sets each of characters contained in the given string to not be allowed for input. By default these characters are not allowed: %'*"+?><:\

const CString& GetInvalidCharacters() const

Retrieves the characters not allowed for input.

voidSetMaxCharacters(int nMaxChars)

Sets the maximum number of characters allowed. By default there is no limit (0).

intGetMaxCharacters() const

Retrieves the maximum number of characters allowed.

CAMSNumericEdit

This is the base class for all numeric entry classes. It ensures that the user enters a valid number and provides features such as automatic formatting. It also allows you to restrict what the number can look like, such as how many digits before and after the decimal point, and whether it can be negative or not.

Member

Description

voidSetDouble(double dText, bool bTrimTrailingZeros = true)

Sets the control's text to the given double value. If bTrimTrailingZeros is true, any insignificant zeros after the decimal point are removed.

doubleGetDouble() const

Returns the current text as a double value.

voidSetInt(int nText)

Sets the control's text to the given integer value.

intGetInt() const

Returns the current text as an integer value.

voidSetMaxWholeDigits(int nMaxWholeDigits)

Sets the maximum number of digits before the decimal point. This is 9 by default.

intGetMaxWholeDigits() const

Retrieves the maximum number of digits before the decimal point.

voidSetMaxDecimalPlaces(int nMaxDecimalPlaces)

Sets the maximum number of digits after the decimal point. This is 4 by default.

intGetMaxDecimalPlaces() const

Retrieves the maximum number of digits after the decimal point.

voidAllowNegative(bool bAllowNegative = true)

Sets a flag to allow a negative sign or not. By default negatives are allowed.

boolIsNegativeAllowed() const

Determines whether a negative sign can be entered or not.

voidSetDigitsInGroup(int nDigitsInGroup)

Sets the size of the groups of digits to be separated before the decimal point. This is 0 by default but is typically set to 3 to separate thousands.

intGetDigitsInGroup() const

Retrieves the number of digits per group shown before the decimal point.

voidSetSeparator(TCHAR cDecimal, TCHAR cGroup)

Sets the character to use for the decimal point and the group separator (thousands)

voidGetSeparator(TCHAR* pcDecimal, TCHAR* pcGroup) const

Retrieves the character being used for the decimal point and group separator (thousands).

voidSetPrefix(const CString& strPrefix)

Sets the characters to automatically display in front of the numeric value entered. This is useful for displaying currency signs. By default it is empty.

const CString& GetPrefix() const

Retrieves the characters to automatically display in front of the numeric value entered.

voidSetMask(const CString& strMask)

Parses a string containing '#', comma, and period characters to determine the format of the number that the user may enter. For example: #,###.# means: (1) allow up to 4 digits before the decimal point, (2) insert commas every 3 digits before the decimal point, (3) use a dot as the decimal point, and (4) allow up to one digit after the decimal point.

CString GetMask() const

Retrieves a string containing '#', comma, and period characters representing the format of the numeric value that the user may enter.

CAMSCurrencyEdit

This class is used for entering monetary values. It is also derived from CAMSNumericEdit but it sets the prefix to the currency sign specified in the locale (such as a '$'). It also separates thousands using the character specified in the locale (such as a comma). And it sets the maximum number of digits after the decimal point to two.

CAMSDateEdit

This class handles dates in a very specific format: mm/dd/yyyy or dd/mm/yyyy, depending on the locale. As the user enters the digits, the slashes are automatically filled in. The user may only remove characters from the right side of the value entered. This ensures that the value is kept in the proper format. As a bonus, the user may use the up/down arrow keys to increment/decrement the month, day, or year, depending on the location of the caret.

Member

Description

voidSetDate(int nYear, int nMonth, int nDay)

Sets the date value.

voidSetDate(const CTime& date)

Sets the date value.

voidSetDate(const COleDateTime& date)

Sets the date value.

voidSetDateToToday()

Sets the date value to today's date.

CTime GetDate() const

Retrieves the date (with zeros for the hour, minute, and second).

COleDateTime GetOleDate() const

Retrieves the date (with zeros for the hour, minute, and second).

intGetYear() const

Retrieves the year.

intGetMonth() const

Retrieves the month

intGetDay() const

Retrieves the day

voidSetYear(int nYear)

Sets the year.

voidSetMonth(int nMonth)

Sets the month

voidSetDay(int nDay)

Sets the day

boolIsValid() const

Returns true if the date value is valid.

boolCheckIfValid(bool bShowErrorIfNotValid = true)

Returns true if the date is valid and optionally shows an error message if not.

Sets the character used to separate the date's components. By default it's a slash ('/').

TCHAR GetSeparator() const

Retrieves the character used to separate the date components.

voidShowDayBeforeMonth (bool bDayBeforeMonth = true)

Overrides the locale's format and based on the flag sets the day to be shown before or after the month.

boolIsDayShownBeforeMonth () const

Returns true if the day will be shown before the month (dd/mm/yyyy).

voidModifyFlags(UINT uAdd, UINT uRemove)

Allows adding or removing flags from the control. This may used to set/clear the flags below to alter the behavior of the control when it loses focus:

OnKillFocus_Beep_IfInvalid

Beeps if the value is not a valid date.

OnKillFocus_Beep_IfEmpty

Beeps if the no value has been entered.

OnKillFocus_Beep

Beeps if the value is not valid or has not been entered.

OnKillFocus_SetValid_IfInvalid

Changes the value to a valid date if it isn't.

OnKillFocus_SetValid_IfEmpty

Fills in a valid date value (today's date if allowed) if the control is empty.

OnKillFocus_SetValid

Sets the value to a valid date if it's empty or not valid.

OnKillFocus_SetFocus_IfInvalid

Sets the focus back to the control if its value is not valid.

OnKillFocus_SetFocus_IfEmpty

Sets the focus back to the control if it doesn't contain a value.

OnKillFocus_SetFocus

Sets the focus back to the control if it's empty or not valid.

OnKillFocus_ShowMessage_IfInvalid

Shows an error message box if the value is not a valid date.

OnKillFocus_ShowMessage_IfEmpty

Shows an error message box if it doesn't contain a value.

OnKillFocus_ShowMessage

Shows an error message box if its empty of not valid.

CAMSTimeEdit

This class handles times in a very specific format: HH:mm or hh:mm AM, depending on the locale. Seconds are also supported, but not by default. As the user enters the digits, the colons are automatically filled in. The user may only remove characters from the right side of the value entered. This ensures that the value is kept in the proper format. As a bonus, the user may use the up/down arrow keys to increment/decrement the hour, minute, second, or AM/PM, depending on the location of the caret.

Member

Description

voidSetTime(int nHour, int nMinute, int nSecond = 0)

Sets the time value.

voidSetTime(const CTime& date)

Sets the time value using the time portion of the given date.

voidSetTime(const COleDateTime& date)

Sets the time value using the time portion of the given date

voidSetTimeToNow()

Sets the time value to the current time.

CTime GetTime() const

Retrieves the time (with Dec 30, 1899 for the date).

COleDateTime GetOleTime() const

Retrieves the time (with Dec 30, 1899 for the date).

intGetHour() const

Retrieves the hour.

intGetMinute() const

Retrieves the minute.

intGetSecond() const

Retrieves the second.

CString GetAMPM() const

Returns the AM/PM symbol currently shown on the control or an empty string.

voidSetHour(int nYear)

Sets the hour.

voidSetMinute(int nMonth)

Sets the minute.

voidSetSecond(int nDay)

Sets the second.

voidSetAMPM(bool bAM)

Sets the AM or PM symbol if not in 24 hour format.

boolIsValid(bool bCheckRangeAlso = true) const

Returns true if the time value is valid. If bCheckRangeAlso is true, the time value is also checked that it falls between the range of times established by the SetRange function.

boolCheckIfValid(bool bShowErrorIfNotValid = true)

Returns true if the time is valid and is in range, and optionally shows an error message if not.

voidSetRange(const CTime& dateMin, const CTime& dateMax)

Sets the range of valid time values allowed. By default it's 00:00:00 to 23:59:59.

Note: While the control has focus, the user will be allowed to input any value between 00:00:00 and 23:59:59, regardless of what values are passed to SetRange.

Sets the character used to separate the time's components. By default it's a colon (':').

TCHAR GetSeparator() const

Retrieves the character used to separate the date components.

voidShow24HourFormat (bool bShow24HourFormat = true)

Overrides the locale's format and based on the flag sets the hour to go from 00 to 23 (24 hour format) or from 01 to 12 with the AM/PM symbols.

boolIsShowing24HourFormat () const

Returns true if the hour will be shown in 24 hour format (instead of 12 hour format with the AM/PM symbols).

voidShowSeconds (bool bShowSeconds = true)

Sets whether the seconds will be shown or not.

boolIsShowingSeconds () const

Returns true if the seconds will be shown.

voidSetAMPMSymbols(const CString& strAM, const CString& strPM)

Overrides the locale's format and sets the symbols to display for AM and PM, which may be of any length but must both be of the same length.

voidGetAMPMSymbols(CString* pStrAM, CString* pStrPM) const

Retrieves the symbols to display for AM and PM into the given CString pointers.

voidModifyFlags(UINT uAdd, UINT uRemove)

Allows adding or removing flags from the control. This may used to set/clear the flags below to alter the behavior of the control when it loses focus:

OnKillFocus_Beep_IfInvalid

Beeps if the value is not a valid date.

OnKillFocus_Beep_IfEmpty

Beeps if the no value has been entered.

OnKillFocus_Beep

Beeps if the value is not valid or has not been entered.

OnKillFocus_SetValid_IfInvalid

Changes the value to a valid date if it isn't.

OnKillFocus_SetValid_IfEmpty

Fills in a valid date value (today's date if allowed) if the control is empty.

OnKillFocus_SetValid

Sets the value to a valid date if it's empty or not valid.

OnKillFocus_SetFocus_IfInvalid

Sets the focus back to the control if its value is not valid.

OnKillFocus_SetFocus_IfEmpty

Sets the focus back to the control if it doesn't contain a value.

OnKillFocus_SetFocus

Sets the focus back to the control if it's empty or not valid.

OnKillFocus_ShowMessage_IfInvalid

Shows an error message box if the value is not a valid date.

OnKillFocus_ShowMessage_IfEmpty

Shows an error message box if it doesn't contain a value.

OnKillFocus_ShowMessage

Shows an error message box if its empty of not valid.

CAMSDateTimeEdit

This class multiply inherits from the Date and Time behaviors to show a date and time value separated by a space. It contains all the functions of the CAMSDateEdit and CAMSTimeEdit classes, plus a few of its own. In addition, this class can dynamically be changed to only accept a date or a time value, using the ModifyFlags function.

Member

Description

voidSetDateTime(int nHour, int nMinute, int nSecond = 0)

Sets the date and time value.

voidSetDateTime(const CTime& date)

Sets the date and time value from the given CTime object.

voidSetDateTime(const COleDateTime& date)

Sets the date and time value from the given COleDateTime object.

voidSetToNow()

Sets the date and time value to the current date and time.

CTime GetDateTime() const

Retrieves the date and time into a CTime object.

COleDateTime GetOleDateTime() const

Retrieves the time and time into a CTime object.

boolIsValid() const

Returns true if the date and time value is valid.

boolCheckIfValid(bool bShowErrorIfNotValid = true)

Returns true if the date and time is valid and is in range, and optionally shows an error message if not.

Changes this class to only allow a date value, just like the CAMSDateEdit class.

TimeOnly

Changes this class to only allow a time value, just like the CAMSTimeEdit class.Note: If both of these flags are set, the TimeOnly flag is ignored.

CAMSMaskedEdit

This class is useful for values with a fixed numeric format, such as phone numbers, social security numbers, or zip codes.

Member

Description

voidSetMask(const CString& strMask)

Sets the format of the values to be entered. By default, each '#' symbol in the mask represents a digit. Any other characters in between the # symbols are automatically filled-in as the user types digits.

Additional characters may be added to be interpreted as special mask symbols. See GetSymbolArray below.

const CString& GetMask() const

Retrieves the mask used to format the value entered by the user.

CString GetNumericText() const

Retrieves the control's value without any non-numeric characters.

SymbolArray& GetSymbolArray ()

Retrieves a reference to the array of symbols that may be found on the mask. By default, this array will contain one element for the # symbol.

Use this function to add, edit, or delete symbols (see the CAMSEDit::MaskedBehavior::Symbol class in amsEdit.h). Here are some examples of how to use this function to add additional symbols:

CAMSMultiMaskedEdit

This class is capable of dynamically taking on the behavior of any of the above classes based on the mask assigned to it. See SetMask below for more details. It contains not only it's own member functions but also those of the Alphanumeric, Numeric, Masked, and DateTime classes above. With such high overhead, I recommend you only use this class for controls which must dynamically change from one behavior to another at run time. The default behavior is Alphanumeric.

Member

Description

voidSetMask(const CString& strMask)

Sets the format of the values to be entered.

##/##/#### ##:##:## = Date and time (with seconds). The location of the month and the hour format are retrieved from the locale.

##/##/#### ##:## = Date and time (without seconds). The location of the month and the hour format are retrieved from the locale.

##/##/#### = Date. The location of the month is retrieved from the locale.

##:##:## = Time (with seconds). The location of hour format is retrieved from the locale.

##:## = Time (without seconds). The location of hour format is retrieved from the locale.

If it looks like a numeric value, such as ### or #,###.### (without foreign characters after the first #) then it's treated as a number; otherwise it's treated as a masked value (e.g., ###-####).

const CString& GetMask() const

Retrieves the mask used to format the value entered by the user.

Usage

These classes are designed to be used inside CDialog-derived classes as replacements for the CEdit variables typically created with the ClassWizard. Here's what you need to use them inside your project:

Add amsEdit.cpp and amsEdit.h to your project.

Include amsEdit.h inside your sources. I recommend including it inside stdafx.h so that it's only done in one place.

Add edit controls to your dialog resource inside DevStudio.

Use the "Member Variables" tab of the ClassWizard to associate your edit controls to CEdit variables.

After closing ClassWizard, open the dialog's header file and change the type of the controls you need to validate from CEdit to one of the CAMSEdit-derived classes above.

If necessary, use the OnInitDialog handler to adjust any settings, such as initial values, ranges, etc. For example if you have a CAMSDateEdit class, you would probably want to set its date ranges using the SetRange member.

Enjoy!

Note: If you need to keep your executable as small as possible, you can change the AMSEDIT_COMPILED_CLASSES macro (defined in amsEdit.h) to compile just the classes you need to use. Also, if you want to build and export these classes in an MFC Extension DLL, look inside amsEdit.h for instructions.

History

Version 1.0

Apr 9, 2002

Fixed incorrect behavior of the up/down arrow keys in CAMSDateEdit whenever the date would appear in dd/mm/yyyy format. Thanks to spleen for reporting it.

Apr 21, 2002

Fixed incorrect handling of numerics when the locale is set to use a comma for the decimal point and a period for the thousand's separator. Thanks to Anonymous for "pointing" it out.

Version 2.0

Jan 28, 2003

Added the CAMSTimeEdit class to allow input of time values. Many thanks to Paul Runstedler for providing me with his version of the class.

Added a section to the demo's dialog box to show the new CAMSTimeEdit class in action.

Added the CAMSDateTimeEdit class to allow input of date and time values in the same edit box. This class multiply inherits from the DateBehavior and TimeBehavior classes.

Enhanced the MaskedBehavior class to allow any character (in addition to the '#') to be interpreted as a special mask symbol. Thanks to Paul Runstedler for providing me with his code, which gave me the idea to add this feature.

Added OnKillFocus flags to the NumericBehavior class to allow padding of the value with zeros before and/or after the decimal point. This is especially useful when entering monetary values, so I added it to the CAMSCurrencyEdit class. Thanks to Spiros Prantalos for giving me the idea.

Changed the NumericBehavior to automatically put a zero in front of values that start with a decimal symbol when the control loses focus (ie. ".69" becomes "0.69").

Added AddDecimalAfterMaxWholeDigits flag to allow the decimal point to be automatically inserted when the user enters a digit and all the whole numbers have already been entered. Thanks to Spiros Prantalos for giving me the idea.

Fixed a bug with the date validation code (IsValidMonth). Thanks to Hakon for reporting it.

Added GetTrimmedText to the CAMSEdit class. Thanks to Paul Runstedler for providing me with his code.

Replaced the GetTextAsLong and GetTextAsDouble functions with GetInt and GetDouble inside the CAMSNumericEdit class (which propagates to the CAMSIntegerEdit and CAMSCurrencyEdit classes). I also added SetInt and SetDouble to make it easy to set the text to an int or double value.

Added comments to the top of all the functions and shuffled some code around to make it cleaner and easier to maintain.

Feb 14, 2003

Got the project to build successfully in Visual Studio .NET, after fixing a few minor issues.

Added virtual destructors that were missing in two classes containing virtual functions.

Added IsReadOnly to CAMSEdit and fixed a bug that was allowing input even when the control was read-only. Thanks to solopido for reporting it.

Mar 4, 2003

Added passive range checking to the NumericBehavior class. This means that if a range has been set and any of the OnKillFocus flags are set, the value will be verified when the control loses focus. Thanks to Anatoly Sidorov for requesting it.

Added the ability to change and retrieve the separators for numeric values (NumericBehavior) -- the decimal symbol and the thousand's separator. Thanks to João Paulo Figueira for giving me the idea.

Fixed a bug that was causing the NumericBehavior's value to be refreshed unnecessarily and thus causing extra EN_CHANGE messages to be sent. Thanks to Anatoly Sidorov for reporting it.

Cleaned up the code even further and fixed minor issues with the TimeBehavior.

Sep 25, 2003

Fixed a problem related to adding masked symbols with overlapping character validations, reported by G. Steudtel. Thanks to G. Steudtel and Spiros for providing the solution to the problem.

Fixed a bug that would cause a crash when selecting text and pressing Shift+Delete, originally reported by Raffaele Romito. Thanks to Matteo (Teo77) for also reporting it and then providing me with the solution.

Version 3.0

Mar 18, 2004

Fixed bugs in NumericBehavior class related to decimal and thousand's separators for non-US locales. Thanks to Carlos Marsura for reporting and suggesting a fix the problem.

Added _tsetlocale(LC_ALL, _T("")); inside OnInitDialog of the Demo to make it use the current user's locale. This isn't done by the underlying code! Doing this fixes problems with conversions between string and double values in locales where the decimal point is not a dot. Thanks to JNKS for reporting this problem along with a workaround.

Added the PadWithZerosAfterDecimalWhenTextChanges and PadWithZerosAfterDecimalWhenTextIsSet flags to the NumericBehavior class to allow automatic padding with zeros. I then set the PadWithZerosAfterDecimalWhenTextIsSet flag into the CAMSCurrencyEdit class so that the ".00" would be automatically appended (if necessary).

Added a call to IsReadOnly to prevent the Up/Down arrows from changing the Date and Time controls when read-only.

Made minor adjustments to get the code to compile with no warnings on Warning Level 4. Thanks to Jim Chekerylla and Carlo Masura for their comments and suggestions.

Fixed minor bug with the MaskedBehavior class related to deletion occurring when using custom symbols.

Added AMSEDIT_EXPORT to each class declaration to make it easy to export the code from an MFC Extension DLL if desired. See amsEdit.h for more details.

Broke each class into #if blocks to allow them to be selectively compiled using the new AMSEDIT_COMPILED_CLASSES macro defined at the top of amsEdit.h. This helps to reduce the executable's size. Thanks to Michael Mann for the idea.

Share

About the Author

I've done extensive work with C++, MFC, COM, and ATL on the Windows side. On the Web side, I've worked with VB, ASP, JavaScript, and COM+. I've also been involved with server-side Java, which includes JSP, Servlets, and EJB, and more recently with ASP.NET/C#.

Comments and Discussions

It seems that there are certain issues when trying to port that class to VS 2005 beta 2.

1. All declarations done in for loops will not be accepted. So for example when declaring [for(int items = 0, moreItems = 0; moreItems < 5 && items < 3; etc...] it will not compile. You need to declare items and moreitems outside the for loop.
2. Class inheritance has some issues. For example:
CAMSDateEdit::CAMSDateEdit() :
DateBehavior(this),
Behavior(this) //
will not compile and will generate the error message:
Error 1 error C2385: ambiguous access of 'Behavior' c:\dev\VS2005\amsedit.cpp 4520

Hmmm, I actually don't understand why the VC++ compiler required me to explicitly call the Behavior constructor. I figured it had to do with the virtual multiple inheritance. Have you tried getting rid of Behavior(this)?

Thanks,
Alvaro

Victory means exit strategy, and it's important for the President to explain to us what the exit strategy is. -- GWB, 1999.

Yes but it did not work. The compiler gave another error message. I will try to experiment with some of the compilation switches like /vd0-2 or /vmv. I am not that optimistic about that approach but I will try it. Generally speaking it looks like that there is a different approach regarding virtual inheritance in VS 2005 because I tried to do something similar in a simpler project and I am still having issues.