Click here to Skip to main content
11,567,143 members (50,856 online)
Click here to Skip to main content

NumericTextBox (C#.NET)

, 7 Dec 2011 CPOL 44.1K 1 19
Rate this:
Please Sign up or sign in to vote.
A TextBox for numbers with built-in mathematical expression evaluation

You can use the code and the compiled DLLs for any purpose, however if possible please mark the source.

Update 

The article was updated to the version 1.2. The links are already pointing to the new versions of the files. See details below. 

NumericTextBox 

In my project, I had a TextBox on one of the forms for entering quantity, and I thought it would be a cool feature if it could understand such basic expressions as 10+42 or 4*8 and so on...

So I decided to create a new Control derived from the TextBox, because this could be useful in the future, and now I'm sharing my result.

First of all, since I decided to create a new control, I wanted to make it as flexible as possible, so the expressions that can be understood by the control can include the four basic operations (+, -, *, /) and parentheses, in any valid combination (spaces can be used, they will be skipped).
e.g.: 40+2 or (12+3/ 4) * 4-3*3 or 10+2*(2+5* (4-(2-0.5))+1.5)

Second, I added some extra functionality to the TextBox, some are taken from NumericUpDown control, but there are some which I used already but now I built them in the control (changing background color based on value).

Functionality based on NumericUpDown Control

  • Value property (also the Text property is still available)
  • ValueChanged event: Use this instead of the TextChanged event if you want to use the value of the expression, since the Value will only change when the expression in the text box could be evaluated (mostly those can't which are in the middle of editing: e.g. 1+2* ), while the Text changes with each character change.
  • Minimum/Maximum properties: The Value has to be between these limits, if it would be outside the interval, it will be set to the Minimum or Maximum value.
  • DecimalPlaces property
  • InterceptArrowKeys property
  • Increment property
  • ThousandsSeparator property

Extra Functionality

A warning and error value can be set, and if the Value will exceed these values, then the background color of the control will be changed accordingly.

Note: If the error value is exceeded, then the error color will be used, otherwise if the value is over the warning level then the warning color, if it's lower then it will be white.

The properties associated with this feature:

  • bool EnableWarningValue
  • bool EnableErrorValue
  • decimal WarningValue
  • decimal ErrorValue
  • Color WarningColor
  • Color ErrorColor 

New functionality in version 1.1  

As VallarasuS suggested I added the feature that if the Value changed the expression will be automatically validated after a given time, meaning instead of 1+2 it will show in the textbox 3.

This is achieved by adding an event handler for the ValueChanged event, and starting a timer. When the timer ticks the Validate() method is called, and it updates the Text of the TextBox

Note: Also a bug is corrected, which caused the ValueChanged event fire every time when the Text was changed.  

For this there are two new properties:

  • bool AutoValidate To turn on or off this feature. Default: true
  • int AutoValidationTime To set the validation time in milliseconds. Default: 5000

I guess the usage is quite straight forward, so I wouldn't spend more time explaining it, let's see the code (v1.2, the code only changed since v1.1 to work with the new version of MathsEvaluator, which has minor differences in method names): 

using System;
using System.ComponentModel;
using System.Windows.Forms;
using System.Drawing; 
using Rankep.MathsEvaluator;

namespace Rankep.NumericTextBox
{
    /// <span class="code-SummaryComment"><summary>
</span>    /// A TextBox component to display numeric values.
    /// The 4 basic operations are supported in input.
    /// <span class="code-SummaryComment"></summary>
</span>    [Serializable()]
    [ToolboxBitmap(typeof(TextBox))]
    public partial class NumericTextBox : TextBox
    {
        private Timer timer;

        #region NumericTextBox Properties
        private decimal value;
        /// <span class="code-SummaryComment"><summary>
</span>        /// The current value of the numeric textbox control.
        /// <span class="code-SummaryComment"></summary>
</span>        [Description("The current value of the numeric textbox control.")]
        public decimal Value
        {
            get { return this.value; }
            set 
            { 
                this.value = value;
                if (ValueChanged != null)
                    ValueChanged(this, new EventArgs());
            }
        }

        private decimal minimum;
        /// <span class="code-SummaryComment"><summary>
</span>        /// Indicates the minimum value for the numeric textbox control.
        /// <span class="code-SummaryComment"></summary>
</span>        [Description("Indicates the minimum value for the numeric textbox control.")]
        public decimal Minimum
        {
            get { return minimum; }
            set { minimum = value; }
        }

        private decimal maximum;
        /// <span class="code-SummaryComment"><summary>
</span>        /// Indicates the maximum value for the numeric textbox control.
        /// <span class="code-SummaryComment"></summary>
</span>        [Description("Indicates the maximum value for the numeric textbox control.")]
        public decimal Maximum
        {
            get { return maximum; }
            set { maximum = value; }
        }

        private int decimalPlaces;
        /// <span class="code-SummaryComment"><summary>
</span>        /// Indicates the number of decimal places to display.
        /// <span class="code-SummaryComment"></summary>
</span>        [Description("Indicates the number of decimal places to display.")]
        public int DecimalPlaces
        {
            get { return decimalPlaces; }
            set { decimalPlaces = value; }
        }

        private bool enableWarningValue;
        /// <span class="code-SummaryComment"><summary>
</span>        /// Indicates whether the background should change if the warning value is exceeded.
        /// <span class="code-SummaryComment"></summary>
</span>        [Description("Indicates whether the background should change if the warning value is exceeded.")]
        public bool EnableWarningValue
        {
            get { return enableWarningValue; }
            set { enableWarningValue = value; }
        }

        private bool enableErrorValue;
        /// <span class="code-SummaryComment"><summary>
</span>        /// Indicates whether the background should change if the error value is exceeded.
        /// <span class="code-SummaryComment"></summary>
</span>        [Description("Indicates whether the background should change if the error value is exceeded.")]
        public bool EnableErrorValue
        {
            get { return enableErrorValue; }
            set { enableErrorValue = value; }
        }

        private decimal warningValue;
        /// <span class="code-SummaryComment"><summary>
</span>        /// Indicates the value from which the background of the numeric textbox control
        /// changes to the WarningColor
        /// <span class="code-SummaryComment"></summary>
</span>        [Description("Indicates the value from which the background of the numeric textbox control changes to the WarningColor")]
        public decimal WarningValue
        {
            get { return warningValue; }
            set { warningValue = value; }
        }

        private decimal errorValue;
        /// <span class="code-SummaryComment"><summary>
</span>        /// Indicates the value from which the background of the numeric textbox control
        /// changes to the ErrorColor
        /// <span class="code-SummaryComment"></summary>
</span>        [Description("Indicates the value from which the background of the numeric textbox control changes to the ErrorColor")]
        public decimal ErrorValue
        {
            get { return errorValue; }
            set { errorValue = value; }
        }

        private bool interceptArrowKeys;
        /// <span class="code-SummaryComment"><summary>
</span>        /// Indicates whether the numeric textbox control 
        /// will increment and decrement the value 
        /// when the UP ARROW and DOWN ARROW keys are pressed.
        /// <span class="code-SummaryComment"></summary>
</span>        [Description("Indicates whether the numeric textbox control will increment and decrement the value when the UP ARROW and DOWN ARROW keys are pressed.")]
        public bool InterceptArrowKeys
        {
            get { return interceptArrowKeys; }
            set { interceptArrowKeys = value; }
        }

        private decimal increment;
        /// <span class="code-SummaryComment"><summary>
</span>        /// Indicates the amount to increment or decrement on each UP or DOWN ARROW press.
        /// <span class="code-SummaryComment"></summary>
</span>        [Description("Indicates the amount to increment or decrement on each UP or DOWN ARROW press.")]
        public decimal Increment
        {
            get { return increment; }
            set { increment = value; }
        }

        private bool thousandsSeparator;
        /// <span class="code-SummaryComment"><summary>
</span>        /// Indicates whether the thousands separator will be inserted between every three decimal digits.
        /// <span class="code-SummaryComment"></summary>
</span>        [Description("Indicates whether the thousands separator will be inserted between every three decimal digits.")]
        public bool ThousandsSeparator
        {
            get { return thousandsSeparator; }
            set { thousandsSeparator = value; }
        }

        private Color warningColor;
        /// <span class="code-SummaryComment"><summary>
</span>        /// Indicates the background color of the numeric textbox control if the value exceeds the WarningValue.
        /// <span class="code-SummaryComment"></summary>
</span>        [Description("Indicates the background color of the numeric textbox control if the value exceeds the WarningValue.")]
        public Color WarningColor
        {
            get { return warningColor; }
            set { warningColor = value; }
        }

        private Color errorColor;
        /// <span class="code-SummaryComment"><summary>
</span>        /// Indicates the background color of the numeric textbox control if the value exceeds the ErrorValue.
        /// <span class="code-SummaryComment"></summary>
</span>        [Description("Indicates the background color of the numeric textbox control if the value exceeds the ErrorValue.")]
        public Color ErrorColor
        {
            get { return errorColor; }
            set { errorColor = value; }
        }

        /// <span class="code-SummaryComment"><summary>
</span>        /// Indicates whether the expression entered should be automatically validated after a time set with the AutoValidationTime property.
        /// <span class="code-SummaryComment"></summary>
</span>        [Description("Indicates whether the expression entered should be automatically validated after a time set with the AutoValidationTime property.")]
        public bool AutoValidate
        { get; set; }

        /// <span class="code-SummaryComment"><summary>
</span>        /// Gets or sets the time, in milliseconds, before the entered expression will be validated, after the last value change
        /// <span class="code-SummaryComment"></summary>
</span>        [Description("Gets or sets the time, in milliseconds, before the entered expression will be validated, after the last value change")]
        public int AutoValidationTime
        { get; set; }

        #endregion

        /// <span class="code-SummaryComment"><summary>
</span>        /// Occurs when the value in the numeric textbox control changes.
        /// <span class="code-SummaryComment"></summary>
</span>        [Description("Occurs when the value in the numeric textbox control changes.")]
        public event EventHandler ValueChanged;

        #region NumericTextBox Initialization
        /// <span class="code-SummaryComment"><summary>
</span>        /// Constructor
        /// <span class="code-SummaryComment"></summary>
</span>        public NumericTextBox()
        {
            InitializeComponent();
            InitializeValues();
            TextChanged += new EventHandler(NumericTextBox_TextChanged);
            KeyUp += new KeyEventHandler(NumericTextBox_KeyUp);
            Leave += new EventHandler(NumericTextBox_Leave);
            ValueChanged += new EventHandler(NumericTextBox_ValueChanged);
            timer = new Timer();
            timer.Enabled = false;
            timer.Tick += new EventHandler(timer_Tick);
        }

        /// <span class="code-SummaryComment"><summary>
</span>        /// Constructor
        /// <span class="code-SummaryComment"></summary>
</span>        /// <span class="code-SummaryComment"><param name="container"></param>
</span>        public NumericTextBox(IContainer container)
        {
            container.Add(this);

            InitializeComponent();
            InitializeValues();
            TextChanged += new EventHandler(NumericTextBox_TextChanged);
            KeyUp += new KeyEventHandler(NumericTextBox_KeyUp);
            Leave += new EventHandler(NumericTextBox_Leave);
            ValueChanged += new EventHandler(NumericTextBox_ValueChanged);
            timer = new Timer();
            timer.Enabled = false;
            timer.Tick += new EventHandler(timer_Tick);
        }

        /// <span class="code-SummaryComment"><summary>
</span>        /// Initialize some default values
        /// <span class="code-SummaryComment"></summary>
</span>        private void InitializeValues()
        {
            warningColor = System.Drawing.Color.Gold;
            errorColor = System.Drawing.Color.OrangeRed;
            enableErrorValue = false;
            enableWarningValue = false;
            maximum = 100;
            minimum = 0;
            interceptArrowKeys = true;
            increment = 1;
            decimalPlaces = 0;
            AutoValidationTime = 5000;
            AutoValidate = true;
        }
        #endregion

        #region NumericTextBox Event handles
        /// <span class="code-SummaryComment"><summary>
</span>        /// Starts a timer to validate the expression if the AutoValidate is set to true.
        /// <span class="code-SummaryComment"></summary>
</span>        /// <span class="code-SummaryComment"><param name="sender"></param>
</span>        /// <span class="code-SummaryComment"><param name="e"></param>
</span>        void NumericTextBox_ValueChanged(object sender, EventArgs e)
        {
            if (AutoValidate)
            {
                timer.Interval = AutoValidationTime;
                timer.Start();
            }
        }

        /// <span class="code-SummaryComment"><summary>
</span>        /// Validates the expression.
        /// <span class="code-SummaryComment"></summary>
</span>        /// <span class="code-SummaryComment"><param name="sender"></param>
</span>        /// <span class="code-SummaryComment"><param name="e"></param>
</span>        void timer_Tick(object sender, EventArgs e)
        {
            timer.Stop();
            Validate();
            Select(Text.Length, 0);
        }

        /// <span class="code-SummaryComment"><summary>
</span>        /// Handles the event when the focus leaves the control, and validates it's value.
        /// <span class="code-SummaryComment"></summary>
</span>        /// <span class="code-SummaryComment"><param name="sender"></param>
</span>        /// <span class="code-SummaryComment"><param name="e"></param>
</span>        void NumericTextBox_Leave(object sender, EventArgs e)
        {
            Validate();
        }

        /// <span class="code-SummaryComment"><summary>
</span>        /// Handles the Up or Down key up events, if InterceptArrowKeys is true
        /// <span class="code-SummaryComment"></summary>
</span>        /// <span class="code-SummaryComment"><param name="sender"></param>
</span>        /// <span class="code-SummaryComment"><param name="e"></param>
</span>        void NumericTextBox_KeyUp(object sender, KeyEventArgs e)
        {
            if (InterceptArrowKeys)
            {
                switch (e.KeyCode)
                {
                    case Keys.Up:
                        Value += Increment;
                        Validate();
                        break;

                    case Keys.Down:
                        Value -= Increment;
                        Validate();
                        break;
                }
            }
        }

        /// <span class="code-SummaryComment"><summary>
</span>        /// Handles the TextChanged event and tries to parse the text to a decimal value.
        /// <span class="code-SummaryComment"></summary>
</span>        /// <span class="code-SummaryComment"><param name="sender"></param>
</span>        /// <span class="code-SummaryComment"><param name="e"></param>
</span>        private void NumericTextBox_TextChanged(object sender, EventArgs e)
        {
            timer.Stop();
            decimal v;
            if (MathsEvaluator.MathsEvaluator.TryParse(Text, out v))
            {
                //check if it's between min and max
                if (v >

The code is using the static methods of my MathsEvaluator class, which I have written for this, and I'm sharing it together with this control.

Changes in v1.2  

The code of this has changed in v1.2, mostly small maintenance and a bug is corrected which caused false evaluation of expressions containing brackets inside brackets. Also a new operator (^ aka power) is supported, and the multiplication sign can be omitted next to a bracket (e.g. 2(3+4)5 is the same as 2*(3+4)*5 or (1+1)(1+1) is the same as (1+1)*(1+1)). 

Here is the code:

using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Text.RegularExpressions;
namespace Rankep.MathsEvaluator
{
    /// <span class="code-SummaryComment"><summary>
</span>    /// A class to evaluate mathematical expressions
    /// <span class="code-SummaryComment"></summary>
</span>    public static class MathsEvaluator
    {
        /// <span class="code-SummaryComment"><summary>
</span>        /// Returns the evaluated value of the expression
        /// <span class="code-SummaryComment"></summary>
</span>        /// <span class="code-SummaryComment"><remarks>Throws ArgumentExpression</remarks>
</span>        /// <span class="code-SummaryComment"><param name="expression">A mathematical expression</param>
</span>        /// <span class="code-SummaryComment"><returns>Value of the expression</returns>
</span>        public static decimal Parse(string expression)
        {
            decimal d;
            if (decimal.TryParse(expression, out d))
            {
                //The expression is a decimal number, so we are just returning it
                return d;
            }
            else
            {
                return CalculateValue(expression);
            }
        }
        /// <span class="code-SummaryComment"><summary>
</span>        /// Tries to evaluate a mathematical expression.
        /// <span class="code-SummaryComment"></summary>
</span>        /// <span class="code-SummaryComment"><param name="expression">The expression to evaluate</param>
</span>        /// <span class="code-SummaryComment"><param name="value">The parsed value</param>
</span>        /// <span class="code-SummaryComment"><returns>Indicates whether the evaluation was succesful</returns>
</span>        public static bool TryParse(string expression, out decimal value)
        {
            if (IsExpression(expression))
            {
                try
                {
                    value = Parse(expression);
                    return true;
                }
                catch
                {
                    value = 0;
                    return false;
                }
            }
            else
            {
                value = 0;
                return false;
            }
        }
        /// <span class="code-SummaryComment"><summary>
</span>        /// Determines if an expression contains only valid characters
        /// <span class="code-SummaryComment"></summary>
</span>        /// <span class="code-SummaryComment"><param name="s">The expression to check</param>
</span>        /// <span class="code-SummaryComment"><returns>Indicates whether only valid characters were used in the expression</returns>
</span>        public static bool IsExpression(string s)
        {
            //Determines whether the string contains illegal characters
            Regex RgxUrl = new Regex("^[0-9+*-/^()., ]+$");
            return RgxUrl.IsMatch(s);
        }
        /// <span class="code-SummaryComment"><summary>
</span>        /// Splits an expression into elements
        /// <span class="code-SummaryComment"></summary>
</span>        /// <span class="code-SummaryComment"><param name="expression">Mathematical expression</param>
</span>        /// <span class="code-SummaryComment"><param name="operators">Operators used as delimiters</param>
</span>        /// <span class="code-SummaryComment"><returns>The list of elements</returns>
</span>        private static List<string> TokenizeExpression(string expression, Dictionary<char, int> operators)
        {
            List<string> elements = new List<string>();
            string currentElement = string.Empty;
            int state = 0;
            /* STATES
                 * 0 - start
                 * 1 - after opening bracket '('
                 * 2 - after closing bracket ')'
                 * */
            int bracketCount = 0;
            for (int i = 0; i < expression.Length; i++)
            {
                switch (state)
                {
                    case 0:
                        if (expression[i] == '(')
                        {
                            //Change the state after an opening bracket is received
                            state = 1;
                            bracketCount = 0;
                            if (currentElement != string.Empty)
                            {
                                //if the currentElement is not empty, then assuming multiplication
                                elements.Add(currentElement);
                                elements.Add("*");
                                currentElement = string.Empty;
                            }
                        }
                        else if (operators.Keys.Contains(expression[i]))
                        {
                            //The current character is an operator
                            elements.Add(currentElement);
                            elements.Add(expression[i].ToString());
                            currentElement = string.Empty;
                        }
                        else if (expression[i] != ' ')
                        {
                            //The current character is neither an operator nor a space
                            currentElement += expression[i];
                        }
                        break;
                    case 1:
                        if (expression[i] == '(')
                        {
                            bracketCount++;
                            currentElement += expression[i];
                        }
                        else if (expression[i] == ')')
                        {
                            if (bracketCount == 0)
                            {
                                state = 2;
                            }
                            else
                            {
                                bracketCount--;
                                currentElement += expression[i];
                            }
                        }
                        else if (expression[i] != ' ')
                        {
                            //Add the character to the current element, omitting spaces
                            currentElement += expression[i];
                        }
                        break;
                    case 2:
                        if (operators.Keys.Contains(expression[i]))
                        {
                            //The current character is an operator
                            state = 0;
                            elements.Add(currentElement);
                            currentElement = string.Empty;
                            elements.Add(expression[i].ToString());
                        }
                        else if (expression[i] != ' ')
                        {
                            elements.Add(currentElement);
                            elements.Add("*");
                            currentElement = string.Empty;
                            if (expression[i] == '(')
                            {
                                state = 1;
                                bracketCount = 0;
                            }
                            else
                            {
                                currentElement += expression[i];
                                state = 0;
                            }
                        }
                        break;
                }
            }
            //Add the last element (which follows the last operation) to the list
            if (currentElement.Length > 0)
            {
                elements.Add(currentElement);
            }
            return elements;
        }
        /// <span class="code-SummaryComment"><summary>
</span>        /// Calculates the value of an expression
        /// <span class="code-SummaryComment"></summary>
</span>        /// <span class="code-SummaryComment"><param name="expression">The expression to evaluate</param>
</span>        /// <span class="code-SummaryComment"><returns>The value of the expression</returns>
</span>        private static decimal CalculateValue(string expression)
        {
            //Dictionary to store the supported operations
            //Key: Operation; Value: Precedence (higher number indicates higher precedence)
            Dictionary<char, int> operators = new Dictionary<char, int>
            { 
                {'+', 1}, {'-', 1}, {'*', 2}, {'/', 2}, {'^', 3}
            };
            //Tokenize the expression
            List<string> elements = TokenizeExpression(expression, operators);
            //define a value which will be used as the return value of the function
            decimal value = 0;
            //loop from the highest precedence to the lowest
            for (int i = operators.Values.Max(); i >
Thank you for reading! Any problems, ideas, help, constructive criticisms are welcome in the comments.

License

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

Share

About the Author

Akos Orban
Student
Poland Poland
No Biography provided

You may also be interested in...

Comments and Discussions

 
BugWrong output when leaving an empty text field with thousands separator on Pin
Timmeey30-Dec-14 22:36
memberTimmeey30-Dec-14 22:36 
GeneralMy vote of 5 Pin
Paul Boothroyd14-Dec-11 3:12
memberPaul Boothroyd14-Dec-11 3:12 
GeneralGood work Orban Pin
sudhansu_k1237-Dec-11 19:31
membersudhansu_k1237-Dec-11 19:31 
GeneralMy vote of 5 Pin
Kurniawan Prasetyo7-Dec-11 0:09
memberKurniawan Prasetyo7-Dec-11 0:09 
AnswerRe: My vote of 5 Pin
Akos Orban7-Dec-11 1:23
memberAkos Orban7-Dec-11 1:23 
GeneralMy vote of 5 Pin
Kanasz Robert29-Nov-11 22:38
memberKanasz Robert29-Nov-11 22:38 
QuestionVisualizing the result Pin
VallarasuS27-Nov-11 2:57
memberVallarasuS27-Nov-11 2:57 
AnswerRe: Visualizing the result Pin
Akos Orban27-Nov-11 4:29
memberAkos Orban27-Nov-11 4:29 

General General    News News    Suggestion Suggestion    Question Question    Bug Bug    Answer Answer    Joke Joke    Rant Rant    Admin Admin   

Use Ctrl+Left/Right to switch messages, Ctrl+Up/Down to switch threads, Ctrl+Shift+Left/Right to switch pages.

| Advertise | Privacy | Terms of Use | Mobile
Web04 | 2.8.150624.2 | Last Updated 7 Dec 2011
Article Copyright 2011 by Akos Orban
Everything else Copyright © CodeProject, 1999-2015
Layout: fixed | fluid