Click here to Skip to main content
15,885,216 members
Articles / Desktop Programming / MFC
Article

Validating Edit Controls

Rate me:
Please Sign up or sign in to vote.
4.86/5 (68 votes)
21 Mar 2004CPOL24 min read 692.4K   10K   163   217
CEdit-derived classes which validate the most popular types of data input.

Sample Image

Introduction
Background
Classes
Usage

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:

  1. The user may do less typing if the control automatically fills in characters such as dashes or slashes.
  2. The user gets immediate feedback if he/she enters an invalid character.
  3. 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:

Class NameDescription
CAMSEdit

Base class for the classes below. It has some basic functionality.

CAMSAlphanumericEdit

Prohibits input of one or more characters and restricts length.

CAMSNumericEdit

Used to input a decimal number with a maximum number of digits before and/or after the decimal point.

CAMSIntegerEdit

Only allows a whole number to be entered.

CAMSCurrencyEdit

Inserts a monetary symbol in front of the value and a separator for the thousands.

CAMSDateEdit

Allows input of a date in the mm/dd/yyyy or dd/mm/yyyy format, depending on the locale.

CAMSTimeEdit

Allows input of a time with or without seconds and in 12 or 24 hour format, depending on the locale.

CAMSDateTimeEditAllows input of a date and time by combining the above two classes.
CAMSMaskedEdit

Takes a mask containing '#' symbols, each one corresponding to a digit. Any characters between the #s are automatically inserted as the user types. It may be customized to accept additional symbols.

CAMSMultiMaskedEdit

Takes a mask and adopts its behavior to any of the above classes.

What follows is a more detailed description of these classes and their usage. All member functions are public unless otherwise specified.

CAMSEdit

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.

MemberDescription

void SetText(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.

void SetBackgroundColor(COLORREF rgb)

Sets the control's background color.

COLORREF GetBackgroundColor() const

Retrieves the control's background color.

void SetTextColor(COLORREF rgb)

Sets the control's text color.

COLORREF GetTextColor() const

Retrieves the control's text color.

bool IsReadOnly() const

Returns true if the control is read-only.
virtual bool ShouldEnter(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.

CAMSAlphanumericEdit

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.

MemberDescription

void SetInvalidCharacters(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.

void SetMaxCharacters(int nMaxChars)

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

int GetMaxCharacters() 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.

MemberDescription

void SetDouble(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.

double GetDouble() const

Returns the current text as a double value.

void SetInt(int nText)

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

int GetInt() const

Returns the current text as an integer value.

void SetMaxWholeDigits(int nMaxWholeDigits)

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

int GetMaxWholeDigits() const

Retrieves the maximum number of digits before the decimal point.

void SetMaxDecimalPlaces(int nMaxDecimalPlaces)

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

int GetMaxDecimalPlaces() const

Retrieves the maximum number of digits after the decimal point.

void AllowNegative(bool bAllowNegative = true)

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

bool IsNegativeAllowed() const

Determines whether a negative sign can be entered or not.

void SetDigitsInGroup(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.

int GetDigitsInGroup() const

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

void SetSeparator(TCHAR cDecimal, TCHAR cGroup)

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

void GetSeparator(TCHAR* pcDecimal, TCHAR* pcGroup) const

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

void SetPrefix(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.

void SetMask(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.

void SetRange(double dMin, double dMax)

Sets the range of valid numbers values allowed. By default it's AMS_MIN_NUMBER to AMS_MAX_NUMBER.

void GetRange(double* pdMin, double* pdMax) const

Retrieves the range of valid numbers allowed.

bool IsValid() const

Returns true if the number is valid and falls within the allowed range.

bool CheckIfValid(bool bShowErrorIfNotValid = true)

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

void ModifyFlags(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:

AddDecimalAfterMaxWholeDigits

Inserts the decimal symbol automatically when the user enters a digit and all the whole numbers have already been entered.

PadWithZerosAfterDecimalWhenTextChanges

Automatically pads the number with zeros after the decimal point any time the text is changed (using SetWindowText or a key stroke).

PadWithZerosAfterDecimalWhenTextIsSet

Automatically pads the number with zeros after the decimal point any time the text is set (using SetWindowText).

OnKillFocus_PadWithZerosBeforeDecimal

Pads the number with zeros before the decimal symbol up to the max allowed (set by SetMaxWholeDigits).

OnKillFocus_PadWithZerosAfterDecimal

Pads the number with zeros after the decimal symbol up to the max allowed (set by SetMaxDecimalPlaces).

OnKillFocus_DontPadWithZerosIfEmpty

When combined with any of the above two "Pad" flags, the value is not padded if it's empty.

OnKillFocus_Beep_IfInvalid

Beeps if the value is not a valid number.

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 number if it isn't.

OnKillFocus_SetValid_IfEmpty

Fills in a valid number value if the control is empty.

OnKillFocus_SetValid

Sets the value to a valid number 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 number.

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.

CAMSIntegerEdit

This class is used to only allow integer values to be entered. It is derived from

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.

MemberDescription

void SetDate(int nYear, int nMonth, int nDay)

Sets the date value.

void SetDate(const CTime& date)

Sets the date value.

void SetDate(const COleDateTime& date)

Sets the date value.

void SetDateToToday()

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

int GetYear() const

Retrieves the year.

int GetMonth() const

Retrieves the month

int GetDay() const

Retrieves the day

void SetYear(int nYear)

Sets the year.

void SetMonth(int nMonth)

Sets the month

void SetDay(int nDay)

Sets the day

bool IsValid() const

Returns true if the date value is valid.

bool CheckIfValid(bool bShowErrorIfNotValid = true)

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

void SetRange(const CTime& dateMin, const CTime& dateMax)

Sets the range of valid date values allowed. By default it's 01/01/1900 to 12/31/9999.

void SetRange(const COleDateTime& dateMin, const COleDateTime& dateMax)

Sets the range of valid date values allowed.

void GetRange(CTime* pDateMin, CTime* pDateMax) const

Retrieves the range of valid dates allowed.

void GetRange(COleDateTime* pDateMin, COleDateTime* pDateMax) const

Retrieves the range of valid dates allowed.

void SetSeparator(TCHAR cSep)

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.

void ShowDayBeforeMonth (bool bDayBeforeMonth = true)

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

bool IsDayShownBeforeMonth () const

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

void ModifyFlags(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.

MemberDescription

void SetTime(int nHour, int nMinute, int nSecond = 0)

Sets the time value.

void SetTime(const CTime& date)

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

void SetTime(const COleDateTime& date)

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

void SetTimeToNow()

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

int GetHour() const

Retrieves the hour.

int GetMinute() const

Retrieves the minute.

int GetSecond() const

Retrieves the second.

CString GetAMPM() const

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

void SetHour(int nYear)

Sets the hour.

void SetMinute(int nMonth)

Sets the minute.

void SetSecond(int nDay)

Sets the second.

void SetAMPM(bool bAM)

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

bool IsValid(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.

bool CheckIfValid(bool bShowErrorIfNotValid = true)

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

void SetRange(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.

void SetRange(const COleDateTime& dateMin, const COleDateTime& dateMax)

Sets the range of valid time values allowed.

void GetRange(CTime* pDateMin, CTime* pDateMax) const

Retrieves the range of valid times allowed.

void GetRange(COleDateTime* pDateMin, COleDateTime* pDateMax) const

Retrieves the range of valid times allowed.

void SetSeparator(TCHAR cSep)

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.

void Show24HourFormat (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.

bool IsShowing24HourFormat () const

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

void ShowSeconds (bool bShowSeconds = true)

Sets whether the seconds will be shown or not.

bool IsShowingSeconds () const

Returns true if the seconds will be shown.

void SetAMPMSymbols(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.

void GetAMPMSymbols(CString* pStrAM, CString* pStrPM) const

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

void ModifyFlags(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.

MemberDescription

void SetDateTime(int nHour, int nMinute, int nSecond = 0)

Sets the date and time value.

void SetDateTime(const CTime& date)

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

void SetDateTime(const COleDateTime& date)

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

void SetToNow()

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.

bool IsValid() const

Returns true if the date and time value is valid.

bool CheckIfValid(bool bShowErrorIfNotValid = true)

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

void SetRange(const CTime& dateMin, const CTime& dateMax)

Sets the range of valid date values allowed. By default it's 01/01/1900 00:00:00 to 12/31/9999 23:59:59.

void SetRange(const COleDateTime& dateMin, const COleDateTime& dateMax)

Sets the range of valid time values allowed.

void GetRange(CTime* pDateMin, CTime* pDateMax) const

Retrieves the range of valid times allowed.

void GetRange(COleDateTime* pDateMin, COleDateTime* pDateMax) const

Retrieves the range of valid times allowed.

void SetSeparator(TCHAR cSep, bool bDate)

Sets the character used to separate the date or the time components. By default it's a slash ('/') for the date, or the colon (':') for the time.

TCHAR GetSeparator(bool bDate) const

Retrieves the character used to separate the date or the time components.

void ModifyFlags(UINT uAdd, UINT uRemove)

Allows adding or removing flags from the control. This may used to set/clear the same flags in the <a href="#CAMSDateEdit">CAMSDateEdit</a> and <a href="#CAMSTimeEdit">CAMSTimeEdit</a> classes, plus these:

DateOnly

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.

MemberDescription

void SetMask(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:

// Add the '?' symbol to allow alphabetic
// characters.
m_ctlMasked.GetSymbolArray().Add(
  CAMSMaskedEdit::Symbol('?', _istalpha));

// Add the '{' symbol to allow alphabetic
// characters and then convert them to uppercase.
m_ctlMasked.GetSymbolArray().Add(
  CAMSMaskedEdit::Symbol('{', _istalpha,
  _totupper));

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.

MemberDescription

void SetMask(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:

  1. Add amsEdit.cpp and amsEdit.h to your project.
  2. Include amsEdit.h inside your sources. I recommend including it inside stdafx.h so that it's only done in one place.
  3. Add edit controls to your dialog resource inside DevStudio.
  4. Use the "Member Variables" tab of the ClassWizard to associate your edit controls to CEdit variables.
  5. 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.
  6. 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.
  7. 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.
<a href="#CAMSNumericEdit">CAMSNumericEdit</a> so it contains all of its member, except SetDouble, GetDouble, and SetMaxDecimalPlaces which don't make sense (thus are private).

License

This article, along with any associated source code and files, is licensed under The Code Project Open License (CPOL)


Written By
Web Developer
United States United States
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

 
GeneralRe: Cool Pin
alex.barylski14-May-04 14:08
alex.barylski14-May-04 14:08 
QuestionHow to use it in VB Pin
muehlhoe12-May-04 5:25
muehlhoe12-May-04 5:25 
AnswerRe: How to use it in VB Pin
Alvaro Mendez12-May-04 8:23
Alvaro Mendez12-May-04 8:23 
GeneralRe: How to use it in VB Pin
muehlhoe12-May-04 22:43
muehlhoe12-May-04 22:43 
GeneralCAMSDateEdit Derived Class Problem Pin
John Papas27-Apr-04 22:42
John Papas27-Apr-04 22:42 
GeneralRe: CAMSDateEdit Derived Class Problem Pin
mace20-Jul-04 3:36
mace20-Jul-04 3:36 
GeneralRe: CAMSDateEdit Derived Class Problem Pin
Alvaro Mendez20-Aug-04 14:03
Alvaro Mendez20-Aug-04 14:03 
GeneralCAMSNumericEdit/CAMSCurrencyEdit bug &amp; fix Pin
cmarsura7-Apr-04 2:38
cmarsura7-Apr-04 2:38 
GeneralLocale bug &amp; other minor things Pin
cmarsura30-Mar-04 20:04
cmarsura30-Mar-04 20:04 
Hi Alvaro, I am using CAMSCurrencyEdit in an OCX and during test phase discovered some little bugs, one of
them regarding locale (anew!).
Briefly, I will describe them:

First - environment:
- Windows International Language Settings set to Italian, Decimal Separator is ',', Group Separator is '.'
- Using AMS* classes from a DLL (OCX)
- Cannot force applications to call _tsetlocale()

Second - problems:
a) Applications does not call _tsetlocale() (it's a VB app), NumericBehavior.GetDouble() returns values without decimal part.
b) Changing separators at runtime with NumericBehavior.SetSeparators() when value is set change value afterwards
c) Minor inconsistencies in code using directly '.' and '-' instead of m_cDecimalPoint and m_cNegativeSign
d) Compiler issues an error when not defined both _UNICODE and _MBCS

Third - comments:
I noted that CString.Format(), _tcstod(), _ttoi() functions are using settings retrieved from _tsetlocale().
If you does not call _tsetlocale(), the default is '.' for Decimal Separator; this is independent of values returned from
::GetLocaleInfo(LOCALE_USER_DEFAULT, LOCALE_SDECIMAL, ...
Bugs can be reproduced also changing your demo app accordingly.

Fourth - solutions:
Problem a): Reviewed NumericBehavior class. Added the NumericBehavior::GetLocaleCharacters(), using it in all places
where code works with strings directly involved with locale-affected runtime functions (CString.Format(),
_tcstod(), ...), reviewed last parameter passed to GetNumericText(). This change permits to class work well whether
_tsetlocale() is called or not by the app.
Problem b): Added the CAMSEdit::MultiCharReplace() and using it in NumericBehavior::SetSeparators().
Problem c): Changed use of '.' and '-' in favor of m_cDecimalPoint and m_cNegativeSign.
Problem d): Adding compiler preprocessor directive in amsEdit.h to check if _MBCS is defined inside _UNICODE

Follows here the code changes marked with comments containing "Carlo" (remove them).
The source is relative to Version 3 you posted.
Keep up with this good work.

Regards,
Carlo.

#if !defined(AFX_AMS_EDIT_H__AC5ACB94_4363_11D3_9123_00105A6E5DE4__INCLUDED_)
#define AFX_AMS_EDIT_H__AC5ACB94_4363_11D3_9123_00105A6E5DE4__INCLUDED_

#if _MSC_VER >= 1000
#pragma once
#endif // _MSC_VER >= 1000
// Edit.h : header file
// Created by: Alvaro Mendez - 07/17/2000
//
// Source from http://www.codeproject.com/editctrl/validatingedit.asp
//

#include <afxwin.h>
#include <afxtempl.h>

// To export this code from an MFC Extension DLL uncomment the following line and then define _AMSEDIT_EXPORT inside the DLL's Project Settings.
// #define AMSEDIT_IN_DLL

// Import/Export macro for the classes below
#if defined(AMSEDIT_IN_DLL)
#if defined(_AMSEDIT_EXPORT)
#define AMSEDIT_EXPORT _declspec(dllexport)
#else
#define AMSEDIT_EXPORT _declspec(dllimport)
#endif
#else
#define AMSEDIT_EXPORT
#endif

// The following IDs are assigned for each class below to allow us to set which ones we need compiled
#define AMSEDIT_ALPHANUMERIC_CLASS 0x01
#define AMSEDIT_MASKED_CLASS 0x02
#define AMSEDIT_NUMERIC_CLASS 0x04
#define AMSEDIT_INTEGER_CLASS (AMSEDIT_NUMERIC_CLASS | 0x08)
#define AMSEDIT_CURRENCY_CLASS (AMSEDIT_NUMERIC_CLASS | 0x10)
#define AMSEDIT_DATE_CLASS 0x20
#define AMSEDIT_TIME_CLASS 0x40
#define AMSEDIT_DATETIME_CLASS (AMSEDIT_DATE_CLASS | AMSEDIT_TIME_CLASS)
#define AMSEDIT_ALL_CLASSES (AMSEDIT_ALPHANUMERIC_CLASS | AMSEDIT_MASKED_CLASS | AMSEDIT_INTEGER_CLASS | AMSEDIT_CURRENCY_CLASS | AMSEDIT_DATETIME_CLASS)

// If your program does not need all the CAMSEdit classes below, you can reduce
// the size of your executable by selecting just the classes you want to be compiled
// via the following macro. Use the IDs defined above and "OR" together the classes you need.
#define AMSEDIT_COMPILED_CLASSES AMSEDIT_ALL_CLASSES

#if !defined ELEMENTS_OF // (Added) Carlo
#define ELEMENTS_OF(v) (sizeof(v)/sizeof(v[0]))
#endif

/////////////////////////////////////////////////////////////////////////////
// CAMSEdit window

// Class CAMSEdit is the base class for all the other AMS CEdit classes.
// It provides some base functionality to set and get the text and change
// its text and background color.
//
class AMSEDIT_EXPORT CAMSEdit : public CEdit
{
public:
// Construction/destruction
CAMSEdit();
virtual ~CAMSEdit();

// Operations
void SetText(const CString& strText);
CString GetText() const;
CString GetTrimmedText() const;

void SetBackgroundColor(COLORREF rgb);
COLORREF GetBackgroundColor() const;

void SetTextColor(COLORREF rgb);
COLORREF GetTextColor() const;

bool IsReadOnly() const;

protected:
virtual void Redraw();
virtual CString GetValidText() const;
virtual BOOL OnChildNotify(UINT message, WPARAM wParam, LPARAM lParam, LRESULT* pLResult);
virtual bool ShouldEnter(TCHAR c) const;

protected:
CBrush m_brushBackground;
COLORREF m_rgbText;

private:
enum InternalFlags
{
None = 0x0000,
TextColorHasBeenSet = 0x0001
};
UINT m_uInternalFlags;

public:

static CString MultiCharReplace(const CString & sText, int nElementsOfChars, const PTCHAR pcOldChars, const PTCHAR pcNewChars); // (Added) Carlo

// Class SelectionSaver is used to save an edit box's current
// selection and then restore it on destruction.
class AMSEDIT_EXPORT SelectionSaver
{
public:
SelectionSaver(CEdit* pEdit);
SelectionSaver(CEdit* pEdit, int nStart, int nEnd);
~SelectionSaver();

void MoveTo(int nStart, int nEnd);
void MoveBy(int nStart, int nEnd);
void MoveBy(int nPos);
void operator+=(int nPos);

int GetStart() const;
int GetEnd() const;

void Update();
void Disable();

protected:
CEdit* m_pEdit;
int m_nStart, m_nEnd;
};


// Class Behavior is an abstract base class used to define how an edit
// box will behave when it is used. Note that its virtual member functions start
// with an underscore; this avoids naming conflicts when multiply inheriting.
class AMSEDIT_EXPORT Behavior
{
protected:
Behavior(CAMSEdit* pEdit);
virtual ~Behavior();

public:
bool ModifyFlags(UINT uAdd, UINT uRemove);
UINT GetFlags() const;

public:
virtual CString _GetValidText() const = 0;

virtual void _OnChar(UINT uChar, UINT nRepCnt, UINT nFlags) = 0;
virtual void _OnKeyDown(UINT uChar, UINT nRepCnt, UINT nFlags);
virtual void _OnKillFocus(CWnd* pNewWnd);
virtual LONG _OnPaste(UINT wParam, LONG lParam);

protected:
// Wrappers to allow access to protected members of CAMSEdit
virtual LRESULT _Default();
virtual void _Redraw();
virtual bool _ShouldEnter(TCHAR c) const;

protected:
CAMSEdit* m_pEdit;
UINT m_uFlags;
};
friend class Behavior;


#if (AMSEDIT_COMPILED_CLASSES & AMSEDIT_ALPHANUMERIC_CLASS)

// The AlphanumericBehavior class is used to allow entry of alphanumeric
// characters. It can be restricted in terms of what characters cannot
// be inputed as well as how many are allowed altogether.
class AMSEDIT_EXPORT AlphanumericBehavior : public Behavior
{
public:
AlphanumericBehavior(CAMSEdit* pEdit, int nMaxChars = 0, const CString& strInvalidChars = _T("%'*\"+?><:\\"));

// Operations
void SetInvalidCharacters(const CString& strInvalidChars);
const CString& GetInvalidCharacters() const;

void SetMaxCharacters(int nMaxChars);
int GetMaxCharacters() const;

protected:
virtual CString _GetValidText() const;
virtual void _OnChar(UINT uChar, UINT nRepCnt, UINT nFlags);

protected:
int m_nMaxChars;
CString m_strInvalidChars;
};

#endif // (AMSEDIT_COMPILED_CLASSES & AMSEDIT_ALPHANUMERIC_CLASS)


#if (AMSEDIT_COMPILED_CLASSES & AMSEDIT_MASKED_CLASS)

// The MaskedBehavior class is used to allow entry of numeric characters
// based on a given mask containing '#' characters to hold digits.
class AMSEDIT_EXPORT MaskedBehavior : public Behavior
{
public:
// Construction
MaskedBehavior(CAMSEdit* pEdit, const CString& strMask = _T(""));

public:
// Operations
void SetMask(const CString& strMask);
const CString& GetMask() const;

CString GetNumericText() const;

// The Symbol class represents a character which may be added to the mask and then interpreted by the
// MaskedBehavior class to validate the input from the user and possibly convert it to something else.
class AMSEDIT_EXPORT Symbol
{
public:
#ifndef _UNICODE
#ifdef _MBCS // (Added) Carlo
typedef int (*ValidationFunction)(UINT); // designed for functions such as _istdigit, _istalpha
typedef UINT (*ConversionFunction)(UINT); // designed for functions such as _totupper, _totlower
#else
typedef int (*ValidationFunction)(INT); // designed for functions such as _istdigit, _istalpha
typedef UINT (*ConversionFunction)(INT); // designed for functions such as _totupper, _totlower
#endif
#else
typedef int (*ValidationFunction)(WCHAR);
typedef WCHAR (*ConversionFunction)(WCHAR);
#endif

Symbol();
Symbol(TCHAR cSymbol, ValidationFunction fnValidation, ConversionFunction fnConversion = NULL);
virtual ~Symbol();

virtual bool Validate(TCHAR c) const;
virtual TCHAR Convert(TCHAR c) const;

void Set(TCHAR cSymbol);
TCHAR Get() const;
operator TCHAR() const;

protected:
TCHAR m_cSymbol;
ValidationFunction m_fnValidation;
ConversionFunction m_fnConversion;
};

typedef CArray<Symbol, Symbol const&> SymbolArray;

SymbolArray& GetSymbolArray();

protected:
virtual CString _GetValidText() const;
virtual void _OnChar(UINT uChar, UINT nRepCnt, UINT nFlags);
virtual void _OnKeyDown(UINT uChar, UINT nRepCnt, UINT nFlags);

protected:
// Attributes
CString m_strMask;
SymbolArray m_arraySymbols;
};

#endif // (AMSEDIT_COMPILED_CLASSES & AMSEDIT_MASKED_CLASS)


#if (AMSEDIT_COMPILED_CLASSES & AMSEDIT_NUMERIC_CLASS)

// The NumericBehavior class is used to allow the entry of an actual numeric
// value into the edit control. It may be restricted by the number of digits
// before or after the decimal point (if any). If can also be set to use
// commas to separate and group thousands.
class AMSEDIT_EXPORT NumericBehavior : public Behavior
{
public:
// Construction
NumericBehavior(CAMSEdit* pEdit, int nMaxWholeDigits = 9, int nMaxDecimalPlaces = 4);

public:
// Operations
void SetDouble(double dText, bool bTrimTrailingZeros = true);
double GetDouble() const;

void SetInt(int nText);
int GetInt() const;

void SetMaxWholeDigits(int nMaxWholeDigits);
int GetMaxWholeDigits() const;

void SetMaxDecimalPlaces(int nMaxDecimalPlaces);
int GetMaxDecimalPlaces() const;

void AllowNegative(bool bAllowNegative = true);
bool IsNegativeAllowed() const;

void SetDigitsInGroup(int nDigitsInGroup);
int GetDigitsInGroup() const;

void SetSeparators(TCHAR cDecimal, TCHAR cGroup);
void GetSeparators(TCHAR* pcDecimal, TCHAR* pcGroup) const;

void SetPrefix(const CString& strPrefix);
const CString& GetPrefix() const;

void SetMask(const CString& strMask);
CString GetMask() const;

void SetRange(double dMin, double dMax);
void GetRange(double* pdMin, double* pdMax) const;

virtual bool IsValid() const;
bool CheckIfValid(bool bShowErrorIfNotValid = true);

static TCHAR GetLocaleCharacters(PTCHAR pcGroupSeparator = NULL, PTCHAR pcNegativeSign = NULL); // Carlo

enum Flags
{
.
.
.
(follows untouched source code)





// amsEdit.cpp : implementation file for CEdit-derived classes
// Created by: Alvaro Mendez - 07/17/2000
//
// Source from http://www.codeproject.com/editctrl/validatingedit.asp
//

#include "stdafx.h"
#include <locale.h> // (Added) Carlo
#include "amsEdit.h"

#ifdef _DEBUG
#define new DEBUG_NEW
#undef THIS_FILE
static char THIS_FILE[] = __FILE__;
#endif

#pragma warning (disable:4355) // disables: 'this': used in base member initializer list

/////////////////////////////////////////////////////////////////////////////
// CAMSEdit

// Constructs the object with the default attributes
CAMSEdit::CAMSEdit() :
m_rgbText(0),
m_uInternalFlags(None)
{
}

// Destroys the object (virtual).
CAMSEdit::~CAMSEdit()
{
}

BEGIN_MESSAGE_MAP(CAMSEdit, CEdit)
//{{AFX_MSG_MAP(CAMSEdit)
ON_WM_KEYDOWN()
//}}AFX_MSG_MAP
ON_MESSAGE(WM_CUT, OnCut)
ON_MESSAGE(WM_PASTE, OnPaste)
ON_MESSAGE(WM_CLEAR, OnClear)
ON_MESSAGE(WM_SETTEXT, OnSetText)
END_MESSAGE_MAP()

// Returns the control's text.
CString CAMSEdit::GetText() const
{
CString strText;
if (::IsWindow(m_hWnd)) GetWindowText(strText); // (Changed) Carlo
return strText;
}

// Returns the control's text without leading or trailing blanks.
CString CAMSEdit::GetTrimmedText() const
{
CString strText = GetText();
strText.TrimLeft();
strText.TrimRight();
return strText;
}

// Sets the control's text to the given string value.
void CAMSEdit::SetText(const CString& strText)
{
if (::IsWindow(m_hWnd)) SetWindowText(strText); // (Changed) Carlo
}

// Sets the background color to the given rgb.
void CAMSEdit::SetBackgroundColor(COLORREF rgb)
{
m_brushBackground.DeleteObject();
m_brushBackground.CreateSolidBrush(rgb);
Invalidate();
}

// Returns the RGB for the background color.
COLORREF CAMSEdit::GetBackgroundColor() const
{
CAMSEdit* pThis = const_cast<CAMSEdit*>(this);

if (!m_brushBackground.GetSafeHandle())
{
COLORREF rgb = pThis->GetDC()->GetBkColor();
pThis->m_brushBackground.CreateSolidBrush(rgb);
return rgb;
}

LOGBRUSH lb;
pThis->m_brushBackground.GetLogBrush(&lb);
return lb.lbColor;
}

// Sets the text color to the given rgb.
void CAMSEdit::SetTextColor(COLORREF rgb)
{
m_rgbText = rgb;
m_uInternalFlags |= TextColorHasBeenSet;
Invalidate();
}

// Returns the RGB for the text color.
COLORREF CAMSEdit::GetTextColor() const
{
if (!(m_uInternalFlags & TextColorHasBeenSet))
{
CAMSEdit* pThis = const_cast<CAMSEdit*>(this);
pThis->m_rgbText = pThis->GetDC()->GetTextColor();
pThis->m_uInternalFlags |= TextColorHasBeenSet;
}
return m_rgbText;
}

// Returns true if the control is read only
bool CAMSEdit::IsReadOnly() const
{
return !!(GetStyle() & ES_READONLY);
}

// Returns the control's value in a valid format.
CString CAMSEdit::GetValidText() const
{
return GetText();
}

// Redraws the window's text.
void CAMSEdit::Redraw()
{
if (!::IsWindow(m_hWnd))
return;

CString strText = GetValidText();
if (strText != GetText())
SetWindowText(strText);
}

// Returns true if the given character should be entered into the control.
bool CAMSEdit::ShouldEnter(TCHAR) const
{
return true;
}

// Cuts the current selection into the clipboard.
LONG CAMSEdit::OnCut(UINT, LONG)
{
int nStart, nEnd;
GetSel(nStart, nEnd);

if (nStart < nEnd)
{
SendMessage(WM_COPY); // copy the selection and...
ReplaceSel(_T("")); // delete it
}

return 0;
}

// Clears the current selection.
LONG CAMSEdit::OnClear(UINT, LONG)
{
int nStart, nEnd;
GetSel(nStart, nEnd);

if (nStart < nEnd)
SendMessage(WM_KEYDOWN, VK_DELETE); // delete the selection

return 0;
}

// Pastes the text from the clipboard onto the current selection.
LONG CAMSEdit::OnPaste(UINT, LONG)
{
int nStart, nEnd;
GetSel(nStart, nEnd);

CEdit::Default();
CString strText = GetValidText();

if (strText != GetText())
{
SetWindowText(strText);
SetSel(nStart, nEnd);
}

return 0;
}

// Handles drawing the text and background using the designated colors
BOOL CAMSEdit::OnChildNotify(UINT message, WPARAM wParam, LPARAM lParam, LRESULT* pLResult)
{
if ((message == WM_CTLCOLOREDIT || message == WM_CTLCOLORSTATIC) && (m_brushBackground.GetSafeHandle() || m_uInternalFlags & TextColorHasBeenSet))
{
CDC* pDC = CDC::FromHandle((HDC)wParam);

if (m_rgbText)
pDC->SetTextColor(m_rgbText);

// Set the text background to the requested background color
pDC->SetBkColor(GetBackgroundColor());

*pLResult = (LRESULT)m_brushBackground.GetSafeHandle();
return TRUE;
}

return CEdit::OnChildNotify(message, wParam, lParam, pLResult);
}

// Handles the WM_SETTEXT message to ensure that text (set via SetWindowText) is valid.
LONG CAMSEdit::OnSetText(UINT, LONG lParam)
{
LONG nResult = CEdit::Default();

CString strText = GetValidText();
if (strText != (LPCTSTR)lParam)
SetWindowText(strText);

return nResult;
}


// Search and replace in string all chars specified
CString CAMSEdit::MultiCharReplace(const CString & sText, int nElementsOfChars, const PTCHAR pcOldChars, const PTCHAR pcNewChars) // (Added) Carlo
{
CString sTextNew(sText);

// For each char to replace
for (int i = 0; i < nElementsOfChars; i++)
{
// For each occurrence of that char in source string
for (int pos = 0; (pos = sText.Find(pcOldChars[i], pos)) >= 0; pos++)
{
// Change relative char in destination string
sTextNew.SetAt(pos, pcNewChars[i]);
}
}

return sTextNew;
}

/////////////////////////////////////////////////////////////////////////////
// CAMSEdit::SelectionSaver

// Constructs the selection saver object for the given edit control.
// It then saves the edit control's current selection.
CAMSEdit::SelectionSaver::SelectionSaver(CEdit* pEdit) :
m_pEdit(pEdit)
{
ASSERT(pEdit);
pEdit->GetSel(m_nStart, m_nEnd);
}

// Constructs the selection saver object for the given edit control.
// It then saves the given nStart and nEnd values.
CAMSEdit::SelectionSaver::SelectionSaver(CEdit* pEdit, int nStart, int nEnd) :
m_pEdit(pEdit),
m_nStart(nStart),
m_nEnd(nEnd)
{
ASSERT(pEdit);
ASSERT(nStart <= nEnd);
}

// Destroys the object and restores the selection to the saved start and end values.
CAMSEdit::SelectionSaver::~SelectionSaver()
{
if (m_pEdit)
m_pEdit->SetSel(m_nStart, m_nEnd, TRUE);
}

// Changes the start and end values to nStart and nEnd respectively.
void CAMSEdit::SelectionSaver::MoveTo(int nStart, int nEnd)
{
ASSERT(nStart <= nEnd);

m_nStart = nStart;
m_nEnd = nEnd;
}

// Changes the start and end values by nStart and nEnd respectively.
void CAMSEdit::SelectionSaver::MoveBy(int nStart, int nEnd)
{
m_nStart += nStart;
m_nEnd += nEnd;

ASSERT(m_nStart <= m_nEnd);
}

// Changes both the start and end values by nPos.
void CAMSEdit::SelectionSaver::MoveBy(int nPos)
{
m_nStart += nPos;
m_nEnd += nPos;
}

// Changes both the start and end values by nPos.
void CAMSEdit::SelectionSaver::operator+=(int nPos)
{
MoveBy(nPos);
}

// Returns the value for the selection's start.
int CAMSEdit::SelectionSaver::GetStart() const
{
return m_nStart;
}

// Returns the value for the selection's end.
int CAMSEdit::SelectionSaver::GetEnd() const
{
return m_nEnd;
}

// Updates the selection's start and end with the current selection.
void CAMSEdit::SelectionSaver::Update()
{
if (m_pEdit)
m_pEdit->GetSel(m_nStart, m_nEnd);
}

// Disables resetting the selection in the destructor
void CAMSEdit::SelectionSaver::Disable()
{
m_pEdit = NULL;
}


/////////////////////////////////////////////////////////////////////////////
// CAMSEdit::Behavior

// Constructs the object from the given control.
CAMSEdit::Behavior::Behavior(CAMSEdit* pEdit) :
m_pEdit(pEdit),
m_uFlags(0)
{
ASSERT(m_pEdit);
}

// Destroys the object (virtual).
CAMSEdit::Behavior::~Behavior()
{
}

// Adds and removes flags from the behavior and then redraws the control
bool CAMSEdit::Behavior::ModifyFlags(UINT uAdd, UINT uRemove)
{
UINT uFlags = (m_uFlags & ~uRemove) | uAdd;

if (m_uFlags == uFlags)
return false;

m_uFlags = uFlags;
_Redraw();
return true;
}

// Returns the flags
UINT CAMSEdit::Behavior::GetFlags() const
{
return m_uFlags;
}

// Handles the WM_CHAR message by passing it to the control.
void CAMSEdit::Behavior::_OnChar(UINT uChar, UINT nRepCnt, UINT nFlags)
{
m_pEdit->OnChar(uChar, nRepCnt, nFlags);
}

// Handles the WM_KEYDOWN message by passing it to the control.
void CAMSEdit::Behavior::_OnKeyDown(UINT uChar, UINT nRepCnt, UINT nFlags)
{
m_pEdit->OnKeyDown(uChar, nRepCnt, nFlags);
}

// Handles the WM_KILLFOCUS message by passing it to the control.
void CAMSEdit::Behavior::_OnKillFocus(CWnd* pNewWnd)
{
m_pEdit->OnKillFocus(pNewWnd);
}

// Handles the WM_PASTE message by passing it to the control.
LONG CAMSEdit::Behavior::_OnPaste(UINT wParam, LONG lParam)
{
return m_pEdit->OnPaste(wParam, lParam);
}

// Calls the default handler for the current message
LRESULT CAMSEdit::Behavior::_Default()
{
return m_pEdit->Default();
}

// Redraws the control so that its value is valid
void CAMSEdit::Behavior::_Redraw()
{
m_pEdit->Redraw();
}

// Returns true if the given character should be entered into the control.
bool CAMSEdit::Behavior::_ShouldEnter(TCHAR c) const
{
return m_pEdit->ShouldEnter(c);
}


#if (AMSEDIT_COMPILED_CLASSES & AMSEDIT_ALPHANUMERIC_CLASS)

/////////////////////////////////////////////////////////////////////////////
// CAMSEdit::AlphanumericBehavior

// Constructs the object using the given set of strInvalidChars
CAMSEdit::AlphanumericBehavior::AlphanumericBehavior(CAMSEdit* pEdit, int nMaxChars /*= 0*/, const CString& strInvalidChars /*= _T("%'*\"+?><:\\")*/) :
Behavior(pEdit),
m_nMaxChars(nMaxChars),
m_strInvalidChars(strInvalidChars)
{
ASSERT(m_nMaxChars >= 0);
}

// Sets the characters to be considered invalid for text input.
void CAMSEdit::AlphanumericBehavior::SetInvalidCharacters(const CString& strInvalidChars)
{
if (m_strInvalidChars == strInvalidChars)
return;

m_strInvalidChars = strInvalidChars;
_Redraw();
}

// Returns the characters considered invalid for text input.
const CString& CAMSEdit::AlphanumericBehavior::GetInvalidCharacters() const
{
return m_strInvalidChars;
}

// Sets the maximum number of characters to allow for input.
void CAMSEdit::AlphanumericBehavior::SetMaxCharacters(int nMaxChars)
{
if (m_nMaxChars == nMaxChars)
return;

m_nMaxChars = nMaxChars;
_Redraw();
}

// Returns the characters considered invalid for input.
int CAMSEdit::AlphanumericBehavior::GetMaxCharacters() const
{
return m_nMaxChars;
}

// Returns the control's value in a valid format.
CString CAMSEdit::AlphanumericBehavior::_GetValidText() const
{
CString strText = m_pEdit->GetText();
CString strNewText = strText.Left(m_nMaxChars ? m_nMaxChars : strText.GetLength());

// Remove any invalid characters from the control's text
for (int iPos = strNewText.GetLength() - 1; iPos >= 0; iPos--)
{
if (m_strInvalidChars.Find(strNewText[iPos]) >= 0)
strNewText = strNewText.Left(iPos) + strNewText.Mid(iPos + 1);
}

return strNewText;
}

// Handles the WM_CHAR message, which is called when the user enters a regular character or Backspace
void CAMSEdit::AlphanumericBehavior::_OnChar(UINT uChar, UINT nRepCnt, UINT nFlags)
{
// Check to see if it's read only
if (m_pEdit->IsReadOnly())
return;

if (!m_strInvalidChars.IsEmpty())
{
// Check if the character is invalid
if (m_strInvalidChars.Find((TCHAR)uChar) >= 0)
{
MessageBeep(MB_ICONEXCLAMATION);
return;
}
}

TCHAR c = static_cast<TCHAR>(uChar);

// If the number of characters is already at Max, overwrite
CString strText = m_pEdit->GetText();
if (strText.GetLength() == m_nMaxChars && m_nMaxChars && _istprint(c))
{
int nStart, nEnd;
m_pEdit->GetSel(nStart, nEnd);

if (nStart < m_nMaxChars && _ShouldEnter(c))
{
m_pEdit->SetSel(nStart, nStart + 1);
m_pEdit->ReplaceSel(CString(c), TRUE);
}
return;
}

if (_ShouldEnter(c))
Behavior::_OnChar(uChar, nRepCnt, nFlags);
}

#endif // (AMSEDIT_COMPILED_CLASSES & AMSEDIT_ALPHANUMERIC_CLASS)


#if (AMSEDIT_COMPILED_CLASSES & AMSEDIT_MASKED_CLASS)

/////////////////////////////////////////////////////////////////////////////
// CAMSEdit::MaskedBehavior

// Constructs the object using the given mask.
CAMSEdit::MaskedBehavior::MaskedBehavior(CAMSEdit* pEdit, const CString& strMask /*= _T("")*/) :
Behavior(pEdit),
m_strMask(strMask)
{
m_arraySymbols.Add(Symbol('#', _istdigit)); // default mask symbol
}

// Returns the mask
const CString& CAMSEdit::MaskedBehavior::GetMask() const
{
return m_strMask;
}

// Sets the mask and redraws the control to accomodate it
void CAMSEdit::MaskedBehavior::SetMask(const CString& strMask)
{
if (m_strMask == strMask)
return;

m_strMask = strMask;
_Redraw();
}

// Returns the numeric portion of the control's value as a string
CString CAMSEdit::MaskedBehavior::GetNumericText() const
{
CString strText = m_pEdit->GetText();
CString strResult;

for (int iPos = 0, nLen = strText.GetLength(); iPos < nLen; iPos++)
{
TCHAR c = strText[iPos];
if (_istdigit(c))
strResult += c;
}

return strResult;
}

// Returns the control's value in a valid format.
CString CAMSEdit::MaskedBehavior::_GetValidText() const
{
CString strText = m_pEdit->GetText();
int nMaskLen = m_strMask.GetLength();

// If the mask is empty, allow anything
if (!nMaskLen)
return strText;

CString strNewText;

// Accomodate the text to the mask as much as possible
for (int iPos = 0, iMaskPos = 0, nLen = strText.GetLength(); iPos < nLen; iPos++, iMaskPos++)
{
TCHAR c = strText[iPos];
TCHAR cMask = static_cast<TCHAR>(iMaskPos < nMaskLen ? m_strMask[iMaskPos] : 0);

// If we've reached the end of the mask, break
if (!cMask)
break;

// Match the character to any of the symbols
for (int iSymbol = 0, nSymbolCount = m_arraySymbols.GetSize(); iSymbol < nSymbolCount; iSymbol++)
{
const Symbol& symbol = m_arraySymbols[iSymbol];

// Find the symbol that applies for the given character
if (cMask != symbol || !symbol.Validate(c))
continue;

// Try to add matching characters in the mask until a different symbol is reached
for (; iMaskPos < nMaskLen; iMaskPos++)
{
cMask = m_strMask[iMaskPos];
if (cMask == symbol)
{
strNewText += symbol.Convert(c);
break;
}
else
{
for (int iSymbol2 = 0; iSymbol2 < nSymbolCount; iSymbol2++)
{
if (cMask == m_arraySymbols[iSymbol2])
{
strNewText += symbol.Convert(c);
break;
}
}

if (iSymbol2 < nSymbolCount)
break;

strNewText += cMask;
}
}

break;
}

// If the character was not matched to a symbol, stop
if (iSymbol == nSymbolCount)
{
if (c == cMask)
{
// Match the character to any of the symbols
for (iSymbol = 0; iSymbol < nSymbolCount; iSymbol++)
{
if (cMask == m_arraySymbols[iSymbol])
break;
}

if (iSymbol == nSymbolCount)
{
strNewText += c;
continue;
}
}

break;
}
}

return strNewText;
}

// Handles the WM_CHAR message, which is called when the user enters a regular character or Backspace
void CAMSEdit::MaskedBehavior::_OnChar(UINT uChar, UINT nRepCnt, UINT nFlags)
{
// Check to see if it's read only
if (m_pEdit->IsReadOnly())
return;

TCHAR c = static_cast<TCHAR>(uChar);

// If the mask is empty, allow anything
int nMaskLen = m_strMask.GetLength();
if (!nMaskLen)
{
if (_ShouldEnter(c))
Behavior::_OnChar(uChar, nRepCnt, nFlags);
return;
}

// Check that we haven't gone past the mask's length
int nStart, nEnd;
m_pEdit->GetSel(nStart, nEnd);
if (nStart >= nMaskLen && c != VK_BACK)
return;

CString strText = m_pEdit->GetText();
int nLen = strText.GetLength();

// Check for a non-printable character (such as Ctrl+C)
if (!_istprint(c))
{
if (c == VK_BACK && nStart != nLen)
{
m_pEdit->SendMessage(WM_KEYDOWN, VK_LEFT); // move the cursor left
return;
}

// Allow backspace only if the cursor is all the way to the right
if (_ShouldEnter(c))
Behavior::_OnChar(uChar, nRepCnt, nFlags);
return;
}

TCHAR cMask = m_strMask[nStart];

// Check if the mask's character matches with any of the symbols in the array.
for (int iSymbol = 0, nSymbolCount = m_arraySymbols.GetSize(); iSymbol < nSymbolCount; iSymbol++)
{
const Symbol& symbol = m_arraySymbols[iSymbol];
if (cMask == symbol)
{
if (symbol.Validate(c) && _ShouldEnter(c))
{
nEnd = (nEnd == nLen ? nEnd : (nStart + 1));
m_pEdit->SetSel(nStart, nEnd);
m_pEdit->ReplaceSel(CString(symbol.Convert(c)), TRUE);
}
return;
}
}

// Check if it's the same character as the mask.
if (cMask == c && _ShouldEnter(c))
{
nEnd = (nEnd == nLen ? nEnd : (nStart + 1));
m_pEdit->SetSel(nStart, nEnd);
m_pEdit->ReplaceSel(CString(c), TRUE);
return;
}

// Concatenate all the mask symbols
CString strSymbols;
for (iSymbol = 0; iSymbol < nSymbolCount; iSymbol++)
strSymbols += m_arraySymbols[iSymbol];

// If it's a valid character, find the next symbol on the mask and add any non-mask characters in between.
for (iSymbol = 0; iSymbol < nSymbolCount; iSymbol++)
{
const Symbol& symbol = m_arraySymbols[iSymbol];

// See if the character is valid for any other symbols
if (!symbol.Validate(c))
continue;

// Find the position of the next symbol
CString strMaskPortion = m_strMask.Mid(nStart);
int nMaskPos = strMaskPortion.FindOneOf(strSymbols);

// Enter the character if there isn't another symbol before it
if (nMaskPos >= 0 && strMaskPortion[nMaskPos] == symbol && _ShouldEnter(c))
{
m_pEdit->SetSel(nStart, nStart + nMaskPos);
m_pEdit->ReplaceSel(strMaskPortion.Left(nMaskPos), TRUE);

_OnChar(uChar, nRepCnt, nFlags);
return;
}
}
}

// Handles the WM_KEYDOWN message, which is called when the user enters a special character such as Delete or the arrow keys.
void CAMSEdit::MaskedBehavior::_OnKeyDown(UINT uChar, UINT nRepCnt, UINT nFlags)
{
switch (uChar)
{
case VK_DELETE:
{
// If deleting make sure it's the last character or that
// the selection goes all the way to the end of the text

int nStart, nEnd;
m_pEdit->GetSel(nStart, nEnd);

CString strText = m_pEdit->GetText();
int nLen = strText.GetLength();

if (nEnd != nLen)
{
if (!(nEnd == nStart && nEnd == nLen - 1))
return;
}
break;
}
}

Behavior::_OnKeyDown(uChar, nRepCnt, nFlags);
}

// Returns a reference to the array of symbols that may be found on the mask.
// This allows adding, editing, or deleting symbols for the mask.
CAMSEdit::MaskedBehavior::SymbolArray& CAMSEdit::MaskedBehavior::GetSymbolArray()
{
return m_arraySymbols;
}


/////////////////////////////////////////////////////////////////////////////
// CAMSEdit::MaskedBehavior::Symbol

// Constructs the object -- needed for CArray
CAMSEdit::MaskedBehavior::Symbol::Symbol() :
m_cSymbol(0),
m_fnValidation(NULL),
m_fnConversion(NULL)
{
}

// Constructs the object with the given character and set of functions
CAMSEdit::MaskedBehavior::Symbol::Symbol(TCHAR cSymbol, ValidationFunction fnValidation, ConversionFunction fnConversion /*= NULL*/) :
m_cSymbol(cSymbol),
m_fnValidation(fnValidation),
m_fnConversion(fnConversion)
{
}

// Destroys the object (virtual).
CAMSEdit::MaskedBehavior::Symbol::~Symbol()
{
}

// Returns true if the given character (usually just entered by the user) is a match for self's symbol.
// This is tested by passing it to the validation function passed in the constructor (if valid).
bool CAMSEdit::MaskedBehavior::Symbol::Validate(TCHAR c) const
{
if (m_fnValidation)
return (m_fnValidation(c) != 0);
return true;
}

// Returns the given character converted as a result of calling the conversion function was passed in the constructor.
// If no conversion function was passed, the character is returned intact.
TCHAR CAMSEdit::MaskedBehavior::Symbol::Convert(TCHAR c) const
{
if (m_fnConversion)
return (TCHAR)m_fnConversion(c);
return c;
}

// Sets the character for the symbol to be used in the mask.
void CAMSEdit::MaskedBehavior::Symbol::Set(TCHAR cSymbol)
{
m_cSymbol = cSymbol;
}

// Returns the character for the symbol to be used in the mask.
TCHAR CAMSEdit::MaskedBehavior::Symbol::Get() const
{
return m_cSymbol;
}

// Returns the character for the symbol to be used in the mask.
CAMSEdit::MaskedBehavior::Symbol::operator TCHAR() const
{
return m_cSymbol;
}

#endif // (AMSEDIT_COMPILED_CLASSES & AMSEDIT_MASKED_CLASS)


#if (AMSEDIT_COMPILED_CLASSES & AMSEDIT_NUMERIC_CLASS)

/////////////////////////////////////////////////////////////////////////////
// CAMSEdit::NumericBehavior

// Constructs the object using the given nMaxWholeDigits and nMaxDecimalPlaces.
CAMSEdit::NumericBehavior::NumericBehavior(CAMSEdit* pEdit, int nMaxWholeDigits /*= 9*/, int nMaxDecimalPlaces /*= 4*/) :
Behavior(pEdit),
m_nMaxWholeDigits(nMaxWholeDigits >= 0 ? nMaxWholeDigits : -nMaxWholeDigits),
m_nMaxDecimalPlaces(nMaxDecimalPlaces),
m_cNegativeSign('-'),
m_cDecimalPoint('.'),
m_cGroupSeparator(','),
m_nDigitsInGroup(0),
m_dMin(AMS_MIN_NUMBER),
m_dMax(AMS_MAX_NUMBER),
m_bAdjustingSeparators(false)
{
ASSERT(m_nMaxWholeDigits > 0); // must have at least 1 digit to the left of the decimal
ASSERT(m_nMaxDecimalPlaces >= 0); // decimal places must be positive

if (nMaxWholeDigits < 0)
m_uFlags |= CannotBeNegative;

// Get the system's negative sign
if (::GetLocaleInfo(LOCALE_USER_DEFAULT, LOCALE_SNEGATIVESIGN, NULL, 0))
::GetLocaleInfo(LOCALE_USER_DEFAULT, LOCALE_SNEGATIVESIGN, &m_cNegativeSign, sizeof(m_cNegativeSign));

// Get the system's decimal point
if (::GetLocaleInfo(LOCALE_USER_DEFAULT, LOCALE_SDECIMAL, NULL, 0))
::GetLocaleInfo(LOCALE_USER_DEFAULT, LOCALE_SDECIMAL, &m_cDecimalPoint, sizeof(m_cDecimalPoint));

// Get the system's group separator
if (::GetLocaleInfo(LOCALE_USER_DEFAULT, LOCALE_STHOUSAND, NULL, 0))
::GetLocaleInfo(LOCALE_USER_DEFAULT, LOCALE_STHOUSAND, &m_cGroupSeparator, sizeof(m_cGroupSeparator));

// Ensure the separators are not the same
if (m_cDecimalPoint == m_cGroupSeparator)
m_cGroupSeparator = (m_cDecimalPoint == ',' ? '.' : ',');
}

// Returns decimal point separator plus group separator and negative sign characters if pointers are passed
TCHAR CAMSEdit::NumericBehavior::GetLocaleCharacters(PTCHAR pcGroupSeparator /*= NULL*/, PTCHAR pcNegativeSign /*= NULL*/) // (Added) Carlo
{
TCHAR cDecimalPoint;
struct lconv *plc = localeconv();

cDecimalPoint = '.'; // Preinitialize
if (*plc->decimal_point ) // If value present
cDecimalPoint = *plc->decimal_point ;

if (pcGroupSeparator)
{ // If pointer passed
*pcGroupSeparator = ','; // Preinitialize
if (*plc->thousands_sep) // If value present
*pcGroupSeparator = *plc->thousands_sep;
}
if (pcNegativeSign)
{ // If pointer passed
*pcNegativeSign = '-'; // Preinitialize
if (*plc->negative_sign) // If value present
*pcNegativeSign = *plc->negative_sign;
}

return cDecimalPoint;
}

// Sets the maximum number of digits before the decimal point.
// If nMaxWholeDigits is negative, then negative numbers will NOT be allowed.
void CAMSEdit::NumericBehavior::SetMaxWholeDigits(int nMaxWholeDigits)
{
ASSERT(nMaxWholeDigits);

// If nMaxWholeDigits is negative, don't allow negatives
bool bAllowNegative = (nMaxWholeDigits >= 0);
if (nMaxWholeDigits < 0)
nMaxWholeDigits = -nMaxWholeDigits;

if (m_nMaxWholeDigits == nMaxWholeDigits && IsNegativeAllowed() == bAllowNegative)
return;

m_nMaxWholeDigits = nMaxWholeDigits;
if (!ModifyFlags(bAllowNegative ? 0 : CannotBeNegative, bAllowNegative ? CannotBeNegative : 0))
_Redraw();
}

// Returns the maximum number of digits before the decimal point.
int CAMSEdit::NumericBehavior::GetMaxWholeDigits() const
{
return m_nMaxWholeDigits;
}

// Sets the maximum number of digits after the decimal point.
void CAMSEdit::NumericBehavior::SetMaxDecimalPlaces(int nMaxDecimalPlaces)
{
ASSERT(nMaxDecimalPlaces >= 0);
if (m_nMaxDecimalPlaces == nMaxDecimalPlaces)
return;

m_nMaxDecimalPlaces = nMaxDecimalPlaces;
_Redraw();
}

// Returns the maximum number of digits after the decimal point.
int CAMSEdit::NumericBehavior::GetMaxDecimalPlaces() const
{
return m_nMaxDecimalPlaces;
}

// Sets whether the negative sign is allowed in the number or not.
void CAMSEdit::NumericBehavior::AllowNegative(bool bAllowNegative /*= true*/)
{
ModifyFlags(bAllowNegative ? 0 : CannotBeNegative, bAllowNegative ? CannotBeNegative : 0);
}

// Returns true if the negative sign is allowed in the number.
bool CAMSEdit::NumericBehavior::IsNegativeAllowed() const
{
return !(m_uFlags & CannotBeNegative);
}

// Sets the number of digits to be grouped together (if any).
void CAMSEdit::NumericBehavior::SetDigitsInGroup(int nDigitsInGroup)
{
ASSERT(nDigitsInGroup >= 0);
if (m_nDigitsInGroup == nDigitsInGroup)
return;

m_nDigitsInGroup = nDigitsInGroup;
_Redraw();
}

// Returns the number of digits to be grouped together (if any).
int CAMSEdit::NumericBehavior::GetDigitsInGroup() const
{
return m_nDigitsInGroup;
}

// Sets the character to use for the decimal point and the group separator (thousands)
void CAMSEdit::NumericBehavior::SetSeparators(TCHAR cDecimal, TCHAR cGroup)
{
ASSERT(cDecimal);
ASSERT(cGroup);

// If nothing's changing, leave
if (m_cDecimalPoint == cDecimal && m_cGroupSeparator == cGroup)
return;

TCHAR cOldChars[2] = {m_cDecimalPoint, m_cGroupSeparator}; // Chars to search for // (Added) Carlo
CString sText = m_pEdit->GetText(); // Save old text

// Set them
m_cDecimalPoint = cDecimal;
m_cGroupSeparator = cGroup;

// Ensure they're not the same
if (m_cDecimalPoint == m_cGroupSeparator)
m_cGroupSeparator = (m_cDecimalPoint == ',' ? '.' : ',');

TCHAR cNewChars[2] = {m_cDecimalPoint, m_cGroupSeparator}; // Chars to replace with // (Added) Carlo
ASSERT(ELEMENTS_OF(cOldChars) == ELEMENTS_OF(cNewChars));

sText = CAMSEdit::MultiCharReplace(sText, ELEMENTS_OF(cOldChars), cOldChars, cNewChars); // Replace characters // (Added) Carlo
m_pEdit->SetText(sText); // Change text with new separators

_Redraw();
}

// Retrieves the character being used for the decimal point and group separator (thousands).
void CAMSEdit::NumericBehavior::GetSeparators(TCHAR* pcDecimal, TCHAR* pcGroup) const
{
if (pcDecimal)
*pcDecimal = m_cDecimalPoint;
if (pcGroup)
*pcGroup = m_cGroupSeparator;
}

// Sets the text to be automatically inserted in front of the number (such as a currency sign).
void CAMSEdit::NumericBehavior::SetPrefix(const CString& strPrefix)
{
if (m_strPrefix == strPrefix)
return;

m_strPrefix = strPrefix;
_Redraw();
}

// Returns the text to be automatically inserted in front of the number (such as a currency sign).
const CString& CAMSEdit::NumericBehavior::GetPrefix() const
{
return m_strPrefix;
}

// Parses the given strMask to set the control's configuration.
void CAMSEdit::NumericBehavior::SetMask(const CString& strMask)
{
int nDecimalPos = -1;
int nLen = strMask.GetLength();

m_nMaxWholeDigits = 0;
m_nMaxDecimalPlaces = 0;
m_nDigitsInGroup = 0;
m_uFlags = (m_uFlags & ~CannotBeNegative); // allow it to be negative
m_strPrefix = _T("");

for (int iPos = nLen - 1; iPos >= 0; iPos--)
{
TCHAR c = strMask[iPos];
if (c == '#')
{
if (nDecimalPos >= 0)
m_nMaxWholeDigits++;
else
m_nMaxDecimalPlaces++;
}
else if ((c == '.' || c == m_cDecimalPoint) && nDecimalPos < 0)
{
nDecimalPos = iPos;
m_cDecimalPoint = c;
}
else if (c == ',' || c == m_cGroupSeparator)
{
if (!m_nDigitsInGroup)
{
m_nDigitsInGroup = (((nDecimalPos >= 0) ? nDecimalPos : nLen) - iPos) - 1;
m_cGroupSeparator = c;
}
}
else
{
m_strPrefix = strMask.Left(iPos + 1);
break;
}
}

if (nDecimalPos < 0)
{
m_nMaxWholeDigits = m_nMaxDecimalPlaces;
m_nMaxDecimalPlaces = 0;
}

ASSERT(m_nMaxWholeDigits > 0); // must have at least one digit on left side of decimal point
_Redraw();
}

// Gets the mask corresponding to the maximum number than can be entered into the control
CString CAMSEdit::NumericBehavior::GetMask() const
{
CString strMask;

for (int iDigit = 0; iDigit < m_nMaxWholeDigits; iDigit++)
strMask += '0';

if (m_nMaxDecimalPlaces)
strMask += m_cDecimalPoint;

for (iDigit = 0; iDigit < m_nMaxDecimalPlaces; iDigit++)
strMask += '0';

strMask = GetSeparatedText(strMask);

for (int iPos = 0, nLen = strMask.GetLength(); iPos < nLen; iPos++)
{
if (strMask[iPos] == '0')
strMask.SetAt(iPos, '#');
}

return strMask;
}

// Sets the range of allowed values to the given minimum and maximum double values.
void CAMSEdit::NumericBehavior::SetRange(double dMin, double dMax)
{
ASSERT(dMin <= dMax);

m_dMin = dMin;
m_dMax = dMax;

_Redraw();
}

// Retrieves the range of allowed values inside the given set of double pointers.
void CAMSEdit::NumericBehavior::GetRange(double* pdMin, double* pdMax) const
{
if (pdMin)
*pdMin = m_dMin;
if (pdMax)
*pdMax = m_dMax;
}

// Returns the number of group separator characters in the given strText.
int CAMSEdit::NumericBehavior::GetGroupSeparatorCount(const CString& strText) const
{
for (int iPos = 0, nSepCount = 0, nLen = strText.GetLength(); iPos < nLen; iPos++)
{
if (strText[iPos] == m_cGroupSeparator)
nSepCount++;
}

return nSepCount;
}

// Returns the given strText as a numeric string.
CString CAMSEdit::NumericBehavior::GetNumericText(const CString& strText, bool bUseMathSymbols /*= false*/) const
{
CString strNewText;
bool bIsNegative = false;
bool bHasDecimalPoint = false;
TCHAR cNegativeSign; // (Added) Carlo
TCHAR cDecimalPoint = GetLocaleCharacters(NULL, &cNegativeSign); // (Added) Carlo

for (int iPos = 0, nLen = strText.GetLength(); iPos < nLen; iPos++)
{
TCHAR c = strText[iPos];
if (_istdigit(c))
strNewText += c;
else if (c == m_cNegativeSign)
bIsNegative = true;
else if (c == m_cDecimalPoint && !bHasDecimalPoint)
{
bHasDecimalPoint = true;
strNewText += (bUseMathSymbols ? cDecimalPoint /*'.'*/ : m_cDecimalPoint); // (Changed) Carlo
}
}

// Add the negative sign to the front of the number
if (bIsNegative)
strNewText.Insert(0, bUseMathSymbols ? cNegativeSign /*'-'*/ : m_cNegativeSign); // (Changed) Carlo

return strNewText;
}

// Returns the current double as a text value.
// If bTrimTrailingZeros is true, any insignificant zeros after the decimal point are removed.
CString CAMSEdit::NumericBehavior::GetDoubleText(double dText, bool bTrimTrailingZeros /*= true*/) const
{
CString strText;
TCHAR cDecimalPoint = GetLocaleCharacters(); // (Added) Carlo
strText.Format(_T("%lf"), dText);

strText.Replace(cDecimalPoint /*_T('.')*/, m_cDecimalPoint); // (Changed) Carlo

if (bTrimTrailingZeros)
{
strText.TrimRight('0');
strText.TrimRight(m_cDecimalPoint);
}

return strText;
}

// Sets the control's text to the given double value.
// If bTrimTrailingZeros is true, any insignificant zeros after the decimal point are removed.
void CAMSEdit::NumericBehavior::SetDouble(double dText, bool bTrimTrailingZeros /*= true*/)
{
m_pEdit->SetWindowText(GetDoubleText(dText, bTrimTrailingZeros));
}

// Returns the current text as a double value.
double CAMSEdit::NumericBehavior::GetDouble() const
{
return _tcstod(GetNumericText(m_pEdit->GetText(), true), NULL); // (Changed) Carlo
}

// Sets the control's text to the given integer value.
void CAMSEdit::NumericBehavior::SetInt(int nText)
{
CString strText;
strText.Format(_T("%d"), nText);
m_pEdit->SetWindowText(strText);
}

// Returns the current text as an integer value.
int CAMSEdit::NumericBehavior::GetInt() const
{
return _ttoi(GetNumericText(m_pEdit->GetText(), true)); // (Changed) Carlo
}

// Adjusts the location of separators based on the nCurrentSeparatorCount.
void CAMSEdit::NumericBehavior::AdjustSeparators(int nCurrentSeparatorCount)
{
SelectionSaver selection = m_pEdit;
m_bAdjustingSeparators = true;

CString strText = _GetValidText();
if (strText != m_pEdit->GetText())
m_pEdit->SetWindowText(strText);

// Adjust the current selection if separators were added/removed
int nNewSeparatorCount = GetGroupSeparatorCount(strText);
if (nCurrentSeparatorCount != nNewSeparatorCount && selection.GetStart() > m_strPrefix.GetLength())
selection += (nNewSeparatorCount - nCurrentSeparatorCount);

m_bAdjustingSeparators = false;
}

// Returns the given text with the group separator characters inserted in the proper places.
CString CAMSEdit::NumericBehavior::GetSeparatedText(const CString& strText) const
{
CString strNumericText = GetNumericText(strText);
CString strNewText = strNumericText;

// Retrieve the number without the decimal point
int nDecimalPos = strNumericText.Find(m_cDecimalPoint);
if (nDecimalPos >= 0)
strNewText = strNewText.Left(nDecimalPos);

if (m_nDigitsInGroup > 0)
{
int nLen = strNewText.GetLength();
BOOL bIsNegative = (!strNewText.IsEmpty() && strNewText[0] == m_cNegativeSign);

// Loop in reverse and stick the separator every m_nDigitsInGroup digits.
for (int iPos = nLen - (m_nDigitsInGroup + 1); iPos >= bIsNegative; iPos -= m_nDigitsInGroup)
strNewText = strNewText.Left(iPos + 1) + m_cGroupSeparator + strNewText.Mid(iPos + 1);
}

// Prepend the prefix, if the number is not empty.
if (!strNewText.IsEmpty() || nDecimalPos >= 0)
{
strNewText = m_strPrefix + strNewText;

if (nDecimalPos >= 0)
strNewText += strNumericText.Mid(nDecimalPos);
}

return strNewText;
}

// Inserts nCount zeros into the given string at the given position.
// If nPos is less than 0, the zeros are appended.
void CAMSEdit::NumericBehavior::InsertZeros(CString* pStrText, int nPos, int nCount)
{
ASSERT(pStrText);

if (nPos < 0 && nCount > 0)
nPos = pStrText->GetLength();

for (int iZero = 0; iZero < nCount; iZero++)
pStrText->Insert(nPos, '0');
}

// Returns the control's value in a valid format.
CString CAMSEdit::NumericBehavior::_GetValidText() const
{
CString strText = m_pEdit->GetText();
CString strNewText;
bool bIsNegative = false;

// Remove any invalid characters from the number
for (int iPos = 0, nDecimalPos = -1, nNewLen = 0, nLen = strText.GetLength(); iPos < nLen; iPos++)
{
TCHAR c = strText[iPos];

// Check for a negative sign
if (c == m_cNegativeSign && IsNegativeAllowed())
bIsNegative = true;

// Check for a digit
else if (_istdigit(c))
{
// Make sure it doesn't go beyond the limits
if (nDecimalPos < 0 && nNewLen == m_nMaxWholeDigits)
continue;

if (nDecimalPos >= 0 && nNewLen > nDecimalPos + m_nMaxDecimalPlaces)
break;

strNewText += c;
nNewLen++;
}
// Check for a decimal point
else if (c == m_cDecimalPoint && nDecimalPos < 0)
{
if (m_nMaxDecimalPlaces == 0)
break;

strNewText += c;
nDecimalPos = nNewLen;
nNewLen++;
}
}

// Check if we need to pad the number with zeros after the decimal point
if (m_nMaxDecimalPlaces > 0 && nNewLen > 0 &&
((m_uFlags & PadWithZerosAfterDecimalWhenTextChanges) ||
(!m_bAdjustingSeparators && (m_uFlags & PadWithZerosAfterDecimalWhenTextIsSet))))
{
if (nDecimalPos < 0)
{
if (nNewLen == 0 || strNewText == /*'-'*/m_cNegativeSign) // (Changed) Carlo
{
strNewText = '0';
nNewLen = 1;
}
strNewText += m_cDecimalPoint;
nDecimalPos = nNewLen++;
}

InsertZeros(&strNewText, -1, m_nMaxDecimalPlaces - (nNewLen - nDecimalPos - 1));
}

// Insert the negative sign if it's there
if (bIsNegative)
strNewText.Insert(0, m_cNegativeSign);

return GetSeparatedText(strNewText);
}

// Handles the WM_CHAR message, which is called when the user enters a regular character or Backspace
void CAMSEdit::NumericBehavior::_OnChar(UINT uChar, UINT nRepCnt, UINT nFlags)
{
// Check to see if it's read only
if (m_pEdit->IsReadOnly())
return;

TCHAR c = static_cast<TCHAR>(uChar);

int nStart, nEnd;
m_pEdit->GetSel(nStart, nEnd);

CString strText = m_pEdit->GetText();
CString strNumericText = GetNumericText(strText);
int nDecimalPos = strText.Find(m_cDecimalPoint);
int nNumericDecimalPos = strNumericText.Find(m_cDecimalPoint);
int nLen = strText.GetLength();
int nNumericLen = strNumericText.GetLength();
int nPrefixLen = m_strPrefix.GetLength();
int nSepCount = GetGroupSeparatorCount(strText);
bool bNeedAdjustment = false;

// Check if we're in the prefix's location
if (nStart < nPrefixLen && _istprint(c))
{
TCHAR cPrefix = m_strPrefix[nStart];

// Check if it's the same character as the prefix.
if (cPrefix == c && _ShouldEnter(c))
{
if (nLen > nStart)
{
nEnd = (nEnd == nLen ? nEnd : (nStart + 1));
m_pEdit->SetSel(nStart, nEnd);
m_pEdit->ReplaceSel(CString(c), TRUE);
}
else
Behavior::_OnChar(uChar, nRepCnt, nFlags);
}
// If it's a part of the number, enter the prefix
else if ((_istdigit(c) || c == m_cNegativeSign || c == m_cDecimalPoint) && _ShouldEnter(c))
{
nEnd = (nEnd == nLen ? nEnd : (nPrefixLen));
m_pEdit->SetSel(nStart, nEnd);
m_pEdit->ReplaceSel(m_strPrefix.Mid(nStart), TRUE);

NumericBehavior::_OnChar(uChar, nRepCnt, nFlags);
}

return;
}

// Check if it's a negative sign
if (c == m_cNegativeSign && IsNegativeAllowed())
{
// If it's at the beginning, determine if it should overwritten
if (nStart == nPrefixLen)
{
if (!strNumericText.IsEmpty() && strNumericText[0] == m_cNegativeSign && _ShouldEnter(c))
{
nEnd = (nEnd == nLen ? nEnd : (nStart + 1));
m_pEdit->SetSel(nStart, nEnd);
m_pEdit->ReplaceSel(CString(m_cNegativeSign), TRUE);
return;
}
}
// If we're not at the beginning, toggle the sign
else if (_ShouldEnter(c))
{
if (strNumericText[0] == m_cNegativeSign)
{
m_pEdit->SetSel(nPrefixLen, nPrefixLen + 1);
m_pEdit->ReplaceSel(_T(""), TRUE);
m_pEdit->SetSel(nStart - 1, nEnd - 1);
}
else
{
m_pEdit->SetSel(nPrefixLen, nPrefixLen);
m_pEdit->ReplaceSel(CString(m_cNegativeSign), TRUE);
m_pEdit->SetSel(nStart + 1, nEnd + 1);
}

return;
}
}

// Check if it's a decimal point (only one is allowed).
else if (c == m_cDecimalPoint && m_nMaxDecimalPlaces > 0)
{
if (nDecimalPos >= 0)
{
// Check if we're replacing the decimal point
if (nDecimalPos >= nStart && nDecimalPos < nEnd)
bNeedAdjustment = true;
else
{ // Otherwise, put the caret on it
if (_ShouldEnter(c))
m_pEdit->SetSel(nDecimalPos + 1, nDecimalPos + 1);
return;
}
}
else
bNeedAdjustment = true;
}

// Check if it's a digit
else if (_istdigit(c))
{
// Check if we're on the right of the decimal point.
if (nDecimalPos >= 0 && nDecimalPos < nStart)
{
if (strNumericText.Mid(nNumericDecimalPos + 1).GetLength() == m_nMaxDecimalPlaces)
{
if (nStart <= nDecimalPos + m_nMaxDecimalPlaces && _ShouldEnter(c))
{
nEnd = (nEnd == nLen ? nEnd : (nStart + 1));
m_pEdit->SetSel(nStart, nEnd);
m_pEdit->ReplaceSel(CString(c), TRUE);
}
return;
}
}

// We're on the left side of the decimal point
else
{
bool bIsNegative = (!strNumericText.IsEmpty() && strNumericText[0] == m_cNegativeSign);

// Make sure we can still enter digits.
if (nStart == m_nMaxWholeDigits + bIsNegative + nSepCount + nPrefixLen)
{
if (m_uFlags & AddDecimalAfterMaxWholeDigits && m_nMaxDecimalPlaces > 0)
{
nEnd = (nEnd == nLen ? nEnd : (nStart + 2));
m_pEdit->SetSel(nStart, nEnd);
m_pEdit->ReplaceSel(CString(m_cDecimalPoint) + c, TRUE);
}
return;
}

if (strNumericText.Mid(0, nNumericDecimalPos >= 0 ? nNumericDecimalPos : nNumericLen).GetLength() == m_nMaxWholeDigits + bIsNegative)
{
if (_ShouldEnter(c))
{
if (strText[nStart] == m_cGroupSeparator)
nStart++;

nEnd = (nEnd == nLen ? nEnd : (nStart + 1));
m_pEdit->SetSel(nStart, nEnd);
m_pEdit->ReplaceSel(CString(c), TRUE);
}
return;
}

bNeedAdjustment = true;
}
}

// Check if it's a non-printable character, such as Backspace or Ctrl+C
else if (!_istprint(c))
bNeedAdjustment = true;
else
return;

// Check if the character should be entered
if (!_ShouldEnter(c))
return;

Behavior::_OnChar(uChar, nRepCnt, nFlags);

// If the decimal point was added/removed or a separator needs adding/removing, adjust the text
if (bNeedAdjustment)
AdjustSeparators(nSepCount);
}

// Handles the WM_KEYDOWN message, which is called when the user enters a special character such as Delete or the arrow keys.
void CAMSEdit::NumericBehavior::_OnKeyDown(UINT uChar, UINT nRepCnt, UINT nFlags)
{
switch (uChar)
{
case VK_DELETE:
{
int nStart = 0, nEnd = 0;
m_pEdit->GetSel(nStart, nEnd);

CString strText = m_pEdit->GetText();
int nLen = strText.GetLength();

// If deleting the prefix, don't allow it if there's a number after it.
int nPrefixLen = m_strPrefix.GetLength();
if (nStart < nPrefixLen && nLen > nPrefixLen)
{
if (nEnd == nLen)
break;
return;
}

if (nStart < nLen && strText[nStart] == m_cGroupSeparator && nStart == nEnd)
Behavior::_OnKeyDown(VK_RIGHT, nRepCnt, nFlags);

// Allow the deletion and then adjust the value
int nSepCount = GetGroupSeparatorCount(strText);
Behavior::_OnKeyDown(uChar, nRepCnt, nFlags);

AdjustSeparators(nSepCount);

// If everything on the right was deleted, put the selection on the right
if (nEnd == nLen)
m_pEdit->SetSel(nStart, nStart);

return;
}
}

Behavior::_OnKeyDown(uChar, nRepCnt, nFlags);
}

// Handles the WM_KILLFOCUS message, which is called when the user leaves the control.
// It's used here to check if zeros need to be added to the value.
void CAMSEdit::NumericBehavior::_OnKillFocus(CWnd* pNewWnd)
{
Behavior::_OnKillFocus(pNewWnd);

// Check if the value is empty and we don't want to touch it
CString strOriginalText = GetNumericText(m_pEdit->GetText());
CString strText = strOriginalText;
int nLen = strText.GetLength();

// If desired, remove any extra leading zeros but always leave one in front of the decimal point
if (m_uFlags & OnKillFocus_RemoveExtraLeadingZeros && nLen > 0)
{
bool bIsNegative = (strText[0] == m_cNegativeSign);
if (bIsNegative)
strText.Delete(0);
strText.TrimLeft('0');
if (strText.IsEmpty() || strText[0] == m_cDecimalPoint)
strText.Insert(0, '0');
if (bIsNegative)
strText.Insert(0, m_cNegativeSign);
}
else if (!(m_uFlags & OnKillFocus_Max) || (nLen == 0 && m_uFlags & OnKillFocus_DontPadWithZerosIfEmpty))
return;

int nDecimalPos = strText.Find(m_cDecimalPoint);

// Check if we need to pad the number with zeros after the decimal point
if (m_uFlags & OnKillFocus_PadWithZerosAfterDecimal && m_nMaxDecimalPlaces > 0)
{
if (nDecimalPos < 0)
{
if (nLen == 0 || strText == m_cNegativeSign /*'-'*/) // (Changed) Carlo
{
strText = '0';
nLen = 1;
}
strText += m_cDecimalPoint;
nDecimalPos = nLen++;
}

InsertZeros(&strText, -1, m_nMaxDecimalPlaces - (nLen - nDecimalPos - 1));
}

// Check if we need to pad the number with zeros before the decimal point
if (m_uFlags & OnKillFocus_PadWithZerosBeforeDecimal && m_nMaxWholeDigits > 0)
{
if (nDecimalPos < 0)
nDecimalPos = nLen;

if (nLen && strText[0] == m_cNegativeSign /*'-'*/) // (Changed) Carlo
nDecimalPos--;

InsertZeros(&strText, (nLen ? strText[0] == m_cNegativeSign /*'-'*/ : -1), m_nMaxWholeDigits - nDecimalPos); // (Changed) Carlo
}

if (strText != strOriginalText)
{
.
.
.
(follows untouched source code)

GeneralI wanted a DateTime ActiveX control Pin
codestudy27-Mar-04 17:43
codestudy27-Mar-04 17:43 
GeneralRe: I wanted a DateTime ActiveX control Pin
cmarsura29-Mar-04 23:17
cmarsura29-Mar-04 23:17 
GeneralRe: I wanted a DateTime ActiveX control Pin
Alvaro Mendez30-Mar-04 3:30
Alvaro Mendez30-Mar-04 3:30 
GeneralRe: I wanted a DateTime ActiveX control Pin
codestudy31-Mar-04 15:22
codestudy31-Mar-04 15:22 
QuestionHow can I use this controls from a DLL? Pin
Member 4795218-Mar-04 5:12
Member 4795218-Mar-04 5:12 
AnswerRe: How can I use this controls from a DLL? Pin
Alvaro Mendez16-Mar-04 6:19
Alvaro Mendez16-Mar-04 6:19 
GeneralCompiler error Pin
Jim Chekerylla19-Feb-04 8:03
Jim Chekerylla19-Feb-04 8:03 
GeneralRe: Compiler error Pin
cmarsura19-Feb-04 22:09
cmarsura19-Feb-04 22:09 
GeneralRe: Compiler error Pin
Jim Chekerylla21-Feb-04 7:46
Jim Chekerylla21-Feb-04 7:46 
GeneralRe: Compiler error Pin
cmarsura24-Feb-04 0:07
cmarsura24-Feb-04 0:07 
GeneralCurrency edit Pin
cmarsura17-Dec-03 5:53
cmarsura17-Dec-03 5:53 
GeneralRe: Currency edit Pin
Alvaro Mendez16-Mar-04 11:00
Alvaro Mendez16-Mar-04 11:00 
GeneralBug in GetDoubleText() Pin
jnks18-Nov-03 12:26
sussjnks18-Nov-03 12:26 
GeneralRe: Bug in GetDoubleText() Pin
Alvaro Mendez16-Mar-04 10:58
Alvaro Mendez16-Mar-04 10:58 
GeneralCurrency edit Pin
Ken Keray4-Nov-03 16:48
Ken Keray4-Nov-03 16:48 
GeneralRe: Currency edit Pin
Alvaro Mendez5-Nov-03 10:38
Alvaro Mendez5-Nov-03 10:38 

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.