SP Numeric Edit Control

Introduction

When programming a GUI, quite often you use edit boxes to enter numeric values. A standard edit box has the ES_NUMBER style that lets you restrict a 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, you should let the user enter not only digits but also a decimal separator and exponent symbol. Moreover, the floating-point format is more complex than a simple sequence of digits, so entered text has to be parsed to make sure 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 number values to their textual representation and back. The conversion is performed according to the certain format defined by a mask and an additional set of parameters (format properties). The mask is a text string that defines a format expression that matches certain syntaxes. You can specify your own mask or use the default one that is automatically generated according to system/locale settings. Format properties are used during formatting, scanning, and generation of the default masks.

The control operates in two modes: display and editing. Editing mode is turned on when the control gets keyboard input focus; otherwise, it stays in display mode. Each mode has its own format parameters, such as mask and format properties. Therefore, the user can see two different textual representations of the same numeric value. In display mode, the value is only converted in text, but in editing mode 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 number like $4,499.98, but at the same time, during editing, the user should not be forced to enter monetary symbol and separate groups with commas; he or she just has to enter essential data: 4499.98. This is the reason why two modes are provided.

Mask

A mask consists of the patterns, separated with semicolons. Every pattern corresponds to a certain value range or state. The number of the mask's patterns 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.

Data
Type
Pattern
1
Pattern
2
Pattern
3
Pattern
4
Pattern
5
Pattern
6
Pattern
7
Pattern
8
Pattern
9
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 certain pattern. For example, a mask for double values (vtDouble) consists of nine patterns; a 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. These are "number" and "zero" patterns (patterns related to positive number, negative number, non-zero number, positive zero, negative zero and zero). The rest are literal patterns. Literal patterns are used to represent special state of the value; for example when value is NULL or floating point value is negative or positive infinity.

In 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-format (exponential) patterns.

  • 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 placed in the order 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. There are three types of the tokens that 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. Characters 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

SP Numeric Edit Control

Here, you can see a few examples of the masks:

  1. Floating-point number in F-format (non-exponential representation).
    (###,)##0.00(#);-(###,)##0.00(#);0.00;0.00;\+INF;\-INF;QNaN;SNaN;NULL

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

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

  4. 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 will continue to use a repeatable block until all digits have been handled. An optional block is used once, only if there are unhanded digits. Blocks defined by control tokens cannot partially overlap each other, but one block can be nested inside another. Any block must be located within only one segment. Each opened block must be closed with a corresponding token. Repeatable blocks are applicable only for the value segments such as integer, fraction, and exponent parts.

Placeholders are replaced with the digits or symbols they are bound 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. 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 "#". They can be used as placeholders only inside value segments.

Literals are just written as they are.

Format properties

By using format properties, you can specify additional parameters for the formatting and scanning operations and customize generation of the default mask. 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 they have no effect when a custom mask is used. Other properties are involved in formatting and scanning. See Table 3 for more information.

Table 3: Format properties

ID Description
fpidWhiteSpace Symbol used as a default substitution of white space token ("_").
fpidZero Symbol used as a default substitution of zero token ("0").
fpidNegativeSign String value for the negative sign.
fpidPositiveSign String value for the positive sign. If numbers without any sign should be interpreted as positive, use an empty string for this parameter.
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 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 Figure 1).

Figure 1

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 through the FormatterParams property in the control's designer.

Formatter maintains the value type, format type, masks, and format properties for editing and display modes. It actually manages the formatting process, and gives necessary facilities for its configuration.

How to Use It

First of all, make sure that 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 built the control with Visual Studio, it should be registered automatically.

During design time, you can use the FormatterParams property (see Figure 2) to open the special property page (see Figure 3) that makes formatter configuration easy.

Figure 2

Figure 3

Also, it is possible to change format parameters at run time. You can do that by 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, as shown in Table 4:

Table 4: Configuration Parameters

Parameter Description
enValueType Data type of value (vtInt8, vtInt16, and so forth)
enFormatType Format type (ftNumeric, ftCurrency, and so on)
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 element index corresponds to property ID. If the element value is NULL, the corresponding property will be set to the system default value.
To assign default values to all properties, just pass the VARIANT of the 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 system settings.

The following C++/MFC code snippet demonstrates how to configure the formatter at run-time.

// Custom display mask
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");

// Allocate safe array with bounds corresponding to the range of
// property ID
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);

// Change fpidNull property
CComBSTR cbsValue = L"null";
rgIndices[0] = long(CNumericeditbox::fpidNull);
arrDisplayFmtProps.PutElement(rgIndices, BSTR(cbsValue));

// Change fpidGrouping property
cbsValue = L"3;2;0";
rgIndices[0] = long(CNumericeditbox::fpidGrouping);
arrDisplayFmtProps.PutElement(rgIndices, BSTR(cbsValue));

// Change fpidGroupSeparator property
cbsValue = L"'";
rgIndices[0] = long(CNumericeditbox::fpidGroupSeparator);
arrDisplayFmtProps.PutElement(rgIndices, BSTR(cbsValue));

// Update formatter parameters
CFormatter fmt = m_nedit.get_Formatter();

fmt.Configure(CNumericeditbox::vtDouble,
              CNumericeditbox::ftNumeric,
              COleVariant(arrDisplayFmtProps), COleVariant(),
              lpcwszDisplayMask, NULL);

Conclusion

This component is free, so please try it. I 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 has been redesigned.
    Some bugs have been fixed.
  • 10/31/2005. Version 1.2 release.
    Some bugs found in alpha version have been fixed.
    Sorce code has been restructured.


Downloads

Comments

  • There are no comments yet. Be the first to comment!

Leave a Comment
  • Your email address will not be published. All fields are required.

Top White Papers and Webcasts

  • Live Event Date: December 11, 2014 @ 1:00 p.m. ET / 10:00 a.m. PT Market pressures to move more quickly and develop innovative applications are forcing organizations to rethink how they develop and release applications. The combination of public clouds and physical back-end infrastructures are a means to get applications out faster. However, these hybrid solutions complicate DevOps adoption, with application delivery pipelines that span across complex hybrid cloud and non-cloud environments. Check out this …

  • CentreCorp is a fully integrated and diversified property management and real estate service company, specializing in the "shopping center" segment, and is one of the premier retail service providers in North America. Company executives travel a great deal, carrying a number of traveling laptops with critical current business data, and no easy way to back up to the network outside the office. Read this case study to learn how CentreCorp implemented a suite of business continuity services that included …

Most Popular Programming Stories

More for Developers

RSS Feeds