Introduction
When programming GUI, sometimes you need to provide an edit box to input numeric values. Usually the standard edit box is used for that; it has a ES_NUMBER
style that lets you restrict user's input, allowing only digits to be entered. It's a useful option but it does not cover all cases met in practice. For example, when you need to input floating-point numbers in exponential notation you should allow the user enter not only digits but also a decimal separator and an exponent symbol. Moreover, the floating-point number format is more complex than a simple sequence of digits, so you have to parse the entered text to make sure that it can be converted to a number and possibly let the user know about detected errors. So I've made a special ActiveX control based on the standard edit box that extends its functionality and offers additional options for handling numbers.
Features
First of all, I'd like to note that this control deals directly with numeric data types, but not text strings. Internally, it performs a conversion of numeric values to their textual representation and back. The conversion is performed according to a certain format defined by a mask and an additional set of parameters (format properties). The mask is a text string that defines a regular-like expression that matches certain syntaxes. You can specify your own mask or use the default one that is automatically generated according to the system/locale settings. Format properties are used during formatting, scanning, and generation of the default masks.
Fig. 1
The control operates in two modes: display and editing. Editing mode is switched on when the control gets the keyboard input focus; otherwise, it stays in display mode. Each mode has its own set of format configuration (mask and format properties). Therefore, the user can see two different textual representations of the same numeric value (see Fig. 1). In display mode, the value is only converted in text, but in editing mode a two-way conversion is performed. Usually, for editing mode, you should use a simplified format while, for display mode, you can enable a full set of features. Consider that you want to handle currency values. It would be convenient for the user to see a number like $4,499.98, but at the same time, during editing, the user should not be forced to enter monetary symbols and separate groups with commas; he or she just has to enter essential data: 4499.98. This is the reason why the two modes are provided.
Mask
A mask consists of the patterns, separated by semicolons. Every pattern corresponds to a certain value range or state. The number of patterns in a mask and their purpose depend on the data type handled by the control. See Table 1 for more information.
Table 1: Mask patterns corresponding to data types.
Datatype |
Pattern1 |
Pattern2 |
Pattern3 |
Pattern4 |
Pattern5 |
Pattern6 |
Pattern7 |
Pattern8 |
Pattern9 |
vtInt8 (VT_I1) |
positive number |
negative number |
zero |
null |
- |
- |
- |
- |
- |
vtInt16 (VT_I2) |
positive number |
negative number |
zero |
null |
- |
- |
- |
- |
- |
vtInt32 (VT_I4) |
positive number |
negative number |
zero |
null |
- |
- |
- |
- |
- |
vtInt64 (VT_I8) |
positive number |
negative number |
zero |
null |
- |
- |
- |
- |
- |
vtUInt8 (VT_UI1) |
non-zero number |
zero |
null |
- |
- |
- |
- |
- |
- |
vtUInt16 (VT_UI2) |
non-zero number |
zero |
null |
- |
- |
- |
- |
- |
- |
vtUInt32 (VT_UI4) |
non-zero number |
zero |
null |
- |
- |
- |
- |
- |
- |
vtUInt64 (VT_UI8) |
non-zero number |
zero |
null |
- |
- |
- |
- |
- |
- |
vtFloat (VT_R4) |
positive number |
negative number |
positive zero |
negative zero |
positive infinity |
negative infinity |
quiet NaN |
signaling NaN |
null |
vtDouble (VT_R8) |
positive number |
negative number |
positive zero |
negative zero |
positive infinity |
negative infinity |
quiet NaN |
signaling NaN |
null |
Any numeric value is formatted according to a definite pattern. For example, a mask for double values (vtDouble
) consists of 9 patterns; negative value will be formatted with pattern 2, positive infinity with pattern 5, and so on.
There are two types of patterns: value and literal. Value patterns are used to format definite numeric values such as positive and negative numbers and zero. The rest are literal patterns. Literal patterns are used to represent a special state of the value; for example when the value is NULL or the floating-point value is negative or positive infinity, it will be formatted with the corresponding literal pattern.
In its turn, a pattern consists of segments. All literal patterns have only one segment, but value patterns have at least one segment corresponding to the integer part of a number and can have two additional ones: prefix and suffix. Floating-point patterns additionally have segments for fraction and exponent parts. An exponent part exists only in E-notation (exponential) patterns. Integer, fraction and exponent segments are included in the number part of a pattern.
- Literal pattern schema:
{ literal }
- Integer value pattern schema:
{ prefix } | { integer } | { suffix }
- Floating-point value F-format pattern schema:
{ prefix } | { integer } { . fraction } | { suffix }
- Floating-point value E-format pattern schema:
{ prefix } | { integer } { . fraction } { e exponent } | { suffix }
Segments are always ordered as it's listed above. Prefixes and suffixes are separated with a "|
" symbol from the value part. They are optional, but when used, they both must be present; however, you may specify an empty prefix or suffix. An integer segment begins right after the "|
" prefix delimiter if the one is preset or from the pattern's beginning otherwise. Generally, a fraction starts from a ".
" symbol and exponent from the "e
" symbol. However, when the segment is completely included in an optional block (discussed below), it starts from the token opening that block. Integer fractions and exponent segments are mandatory for corresponding patterns.
During formatting and scanning, the digits of a number are handled sequentially in a definite order, depending on what segment is being processed. Integer and exponent parts are processed from right to left, but the fraction part is processed from left to right.
Segments are composed of tokens. Every token specifies a definite instruction for the formatting procedure or is used as a separator for different sections of a mask. Only three types of tokens can be used inside segments: control tokens, placeholders and literals.
Table 2: Tokens
Delimiters |
; |
end of pattern |
| |
prefix/suffix delimiter |
Control tokens |
( |
open repeatable block |
) |
close repeatable block |
[ |
open optional block |
] |
close optional block |
Placeholders |
0 |
digit placeholder with default zero value |
_ |
digit placeholder with default space value |
# |
digit placeholder |
- |
negative sign |
+ |
positive sign |
$ |
currency symbol |
% |
percent symbol |
� |
per mile symbol |
, |
thousand (group) separator |
. |
decimal separator |
e |
exponent |
Reserved |
{, }, <, > |
Reserved for future extensions. |
Literals |
\ |
Escape symbol. |
any character |
Any character can be used as a literal. Character literals corresponding to the reserved symbols should be preceded with a back slash "\".
In addition, there are three special escape symbols:
- \r - generates carriage return (CR)
- \n - generates line feed (LF)
- \t - generates tab
|
Here, you can see a few examples of the masks:
- Floating-point number in F-format (non-exponential representation):
(###,)##0.00(#);-(###,)##0.00(#);0.00;0.00;\+INF;\-INF;QNaN;SNaN;NULL
- Floating-point number in E-format (exponential representation):
(###,)##0.00(#)e\+(#)0;-(###,)##0.00(#)e\-(#)0;
0.00e\+0;0.00e\-0;\+INF;\-INF;QNaN;SNaN;NULL
- Floating-point number in simplified E-format (exponential representation):
(#)0[.0(#)][e\+(#)0];-(#)0[.0(#)][e\-(#)0];0[.0][e\+0];
0[.0][e\-0];\+INF;\-INF;QNaN;SNaN
- Floating-point currency in US-format:
$(###,)###.00(#);($|(###,)###.00(#)|);$.00;($|.00|);\+INF;\-INF;QNaN;SNaN
By using control tokens, you can define repeatable and optional blocks. The formatting procedure continues to use a repeatable block until all digits have been handled. An optional block is used once, only if there are unhandled digits. Blocks defined by control tokens cannot partially overlap each other, but one block can be nested inside another. Any block must be entirely located within only one segment. Each opened block must be closed with a corresponding token. Repeatable blocks are applicable only for the value part segments such as integer, fraction, and exponent parts.
Placeholders are replaced with the digits or symbols they are related to. For example, "+
" will be replaced with a positive sign, "$
" with a monetary symbol, and so on. "0
", "_
", and "#
" are digit placeholders; they are used not only for output but also for input during scanning in editing mode. There is a difference between them; during formatting, when there are no digits to handle, "0
" and "_
" are substituted with zero and space symbols correspondingly but nothing will be generated instead of "#
". Digit placeholders are used only inside value part segments.
Literals are just written as they are.
Format properties
By using format properties, you get additional options for the configuration of the formatting and scanning operations and customization of the default mask generation. In fact, fpidNegativeInfinity
, fpidPositiveInfinity
, fpidQuietNaN
, fpidSignalingNaN
, fpidNull
, fpidLeadingZero
, fpidDecimalDigitsNumber
, fpidExponentDigitsNumber
, fpidGrouping
, fpidNegativePattern
, and FpidPositivePattern
are only used for the generation of the default mask so that it has no effect when a custom mask is used. Other properties are also involved in formatting and scanning. See Table 3 for more information.
Table 3: Format properties
ID |
Description |
fpidWhiteSpace |
Symbol used as the default substitution of the white space token ("_"). |
fpidZero |
Symbol used as the default substitution of the zero token ("0"). |
fpidNegativeSign |
String value for the negative sign. |
fpidPositiveSign |
String value for the positive sign. If numbers are written without any sign it should be interpreted as positive, use empty string for this property value. |
fpidNegativeInfinity |
Representation of negative infinity. |
fpidPositiveInfinity |
Representation of positive infinity. |
fpidQuietNaN |
Representation of "quiet not a number" value. |
fpidSignalingNaN |
Representation of "signaling not a number" value. |
fpidNull |
Representation of a Null value. |
fpidCurrency |
String used as the monetary symbol. |
fpidPercent |
String used as the percent symbol. |
fpidPermille |
String used as the permille symbol. |
fpidExponent |
String used as the exponent symbol. |
fpidDecimalSeparator |
Character(s) used as the decimal separator. |
fpidGroupSeparator |
Character(s) used to separate groups of digits to the left of the decimal. |
fpidLeadingZero |
Specifier for leading zeros in decimal fields in the mask generated by default. If set to True - leading zeros will be added, otherwise no leading zeroes will precede the decimal separator. |
fpidDecimalDigitsNumber |
Minimal number of fractional digits to be printed. |
fpidExponentDigitsNumber |
Minimal number of exponent digits to be printed. |
fpidGrouping |
Sizes for each group of digits to the left of the decimal. An explicit size is needed for each group, and sizes are separated by semicolons. If the last value is zero, the preceding value is repeated. |
fpidNegativePattern |
Negative number mode, that is, the format for a negative number. |
fpidPositivePattern |
Positive number mode, that is, the format for a positive number. |
Control architecture
The control is implemented with several classes (see Fig. 2).
Fig. 2
The main class of the control is called NumericEditBox
. It implements the INumericEditBox
interface, which lets you change various properties related to the control's visualization and behavior; by using its Value
property you can access the numeric value handled by the control.
NumericEditBox
contains another object called Formatter
. It can be accessed through the Formatter
property at run time, or FormatterParams
in the control's designer.
Formatter
maintains the value type, format type, masks and format properties for display and editing modes. It actually manages the formatting process, and gives necessary facilities for its configuration.
In its turn, the Formatter
contains two collection objects (FormatProperties
) that represent format properties for display and editing modes.
How to use it
First of all, make sure that the SpNumericEdit.dll is registered on your PC. If it's not, use the command below to register the COM control.
regsvr32 SpNumericEdit.dll
If you have built the control with Visual Studio, it should be registered automatically.
During design-time, you can use the FormatterParams
property (see Fig. 3) to open the special property page (see Fig. 4) that makes the formatter configuration easier.
Fig. 3
Fig. 4
Also, it is possible to change the format parameters at run-time. You can do that using the IFormatter::Configure
method.
HRESULT Configure([in] ValueTypeConstants enValueType,
[in] FormatTypeConstants enFormatType,
[in] VARIANT vDisplayFmtProps,
[in] VARIANT vEditingFmtProps,
[in, defaultvalue(NULL)] BSTR bsDisplatMask,
[in, defaultvalue(NULL)] BSTR bsEditingMask);
The method takes six parameters:
enValueType |
Data type of the value (vtInt8 , vtInt16 , etc.). |
enFormatType |
Format type (ftNumeric , ftCurrency , etc.). |
vDisplayFmtProps , vEditingFmtProps |
Format properties for display and editing modes correspondingly. This parameter is a VARIANT that can hold SAFEARRAY or IFormatProperties . If SAFEARRAY is passed, each of its elements corresponds to the property value, while the element index corresponds to the property ID. If the element value is NULL , then the corresponding property will be set to the system default value. To assign default values to all the properties, just pass VARIANT of VT_NULL or VT_EMPTY type. |
bsDisplatMask , bsEditingMask |
Mask expressions for display and editing modes correspondingly. If a NULL value is passed, the default mask is generated according to the system/locale settings. |
The following C++/MFC code snippet demonstrates how to configure the formatter at run-time:
LPCTSTR lpcwszDisplayMask = _T("It is positive number: \\(+(###,)##0.00(#)\\);") \
_T("It is negative number: \\(-(###,)##0.00(#)\\);") \
_T("It is positive zero: \\(0.00\\);") \
_T("It is negative zero: \\(0.00\\);") \
_T("It is positive infinity: \\(\\+INF\\);") \
_T("It is negative infinity: \\(\\-INF\\);") \
_T("It is quiet not-a-number: \\(QNaN\\);") \
_T("It is signaling not-a-number: \\(SNaN\\);") \
_T("This is NULL");
const long lMin = CNumericeditbox::fpidWhiteSpace;
const long lMax = CNumericeditbox::fpidPositivePattern;
long rgIndices[1];
SAFEARRAYBOUND rgsabound[1];
rgsabound[0].lLbound = lMin;
rgsabound[0].cElements = static_cast<ULONG>(lMax - lMin + 1);
COleSafeArray arrDisplayFmtProps;
arrDisplayFmtProps.Create(VT_BSTR, 1, rgsabound);
CComBSTR cbsValue = L"null";
rgIndices[0] = long(CNumericeditbox::fpidNull);
arrDisplayFmtProps.PutElement(rgIndices, BSTR(cbsValue));
cbsValue = L"3;2;0";
rgIndices[0] = long(CNumericeditbox::fpidGrouping);
arrDisplayFmtProps.PutElement(rgIndices, BSTR(cbsValue));
cbsValue = L"'";
rgIndices[0] = long(CNumericeditbox::fpidGroupSeparator);
arrDisplayFmtProps.PutElement(rgIndices, BSTR(cbsValue));
CFormatter fmt = m_nedit.get_Formatter();
fmt.Configure(CNumericeditbox::vtDouble, CNumericeditbox::ftNumeric,
COleVariant(arrDisplayFmtProps), COleVariant(),
lpcwszDisplayMask, NULL);
Conclusion
The component is free, so please try it. Hope you'll find it useful. Please let me know about bugs and other problems if you find any.
Enjoy!
History
- 02/15/2005. Version 1.0 beta release.
- 03/24/2005. Version 1.0 release.
- New formatting library is used with the control.
- Provided range checks for the values during input.
- 08/16/2005. Version 1.2 alpha release.
- Formatting library and ActiveX has been redesigned.
- Some bugs have been fixed.
- 10/31/2005. Version 1.2 release.
- Several bugs found in alpha version have been fixed.
- Source code has been restructured.