Click here to Skip to main content
15,885,216 members
Articles / Desktop Programming / Windows Forms

NumericTextBox (C#.NET)

Rate me:
Please Sign up or sign in to vote.
4.94/5 (8 votes)
7 Dec 2011CPOL3 min read 59.2K   20   9
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): 

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

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

        #region NumericTextBox Properties
        private decimal value;
        /// <summary>
        /// The current value of the numeric textbox control.
        /// </summary>
        [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;
        /// <summary>
        /// Indicates the minimum value for the numeric textbox control.
        /// </summary>
        [Description("Indicates the minimum value for the numeric textbox control.")]
        public decimal Minimum
        {
            get { return minimum; }
            set { minimum = value; }
        }

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

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

        private bool enableWarningValue;
        /// <summary>
        /// Indicates whether the background should change if the warning value is exceeded.
        /// </summary>
        [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;
        /// <summary>
        /// Indicates whether the background should change if the error value is exceeded.
        /// </summary>
        [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;
        /// <summary>
        /// Indicates the value from which the background of the numeric textbox control
        /// changes to the WarningColor
        /// </summary>
        [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;
        /// <summary>
        /// Indicates the value from which the background of the numeric textbox control
        /// changes to the ErrorColor
        /// </summary>
        [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;
        /// <summary>
        /// Indicates whether the numeric textbox control 
        /// will increment and decrement the value 
        /// when the UP ARROW and DOWN ARROW keys are pressed.
        /// </summary>
        [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;
        /// <summary>
        /// Indicates the amount to increment or decrement on each UP or DOWN ARROW press.
        /// </summary>
        [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;
        /// <summary>
        /// Indicates whether the thousands separator will be inserted between every three decimal digits.
        /// </summary>
        [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;
        /// <summary>
        /// Indicates the background color of the numeric textbox control if the value exceeds the WarningValue.
        /// </summary>
        [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;
        /// <summary>
        /// Indicates the background color of the numeric textbox control if the value exceeds the ErrorValue.
        /// </summary>
        [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; }
        }

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

        /// <summary>
        /// Gets or sets the time, in milliseconds, before the entered expression will be validated, after the last value change
        /// </summary>
        [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

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

        #region NumericTextBox Initialization
        /// <summary>
        /// Constructor
        /// </summary>
        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);
        }

        /// <summary>
        /// Constructor
        /// </summary>
        /// <param name="container"></param>
        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);
        }

        /// <summary>
        /// Initialize some default values
        /// </summary>
        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
        /// <summary>
        /// Starts a timer to validate the expression if the AutoValidate is set to true.
        /// </summary>
        /// <param name="sender"></param>
        /// <param name="e"></param>
        void NumericTextBox_ValueChanged(object sender, EventArgs e)
        {
            if (AutoValidate)
            {
                timer.Interval = AutoValidationTime;
                timer.Start();
            }
        }

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

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

        /// <summary>
        /// Handles the Up or Down key up events, if InterceptArrowKeys is true
        /// </summary>
        /// <param name="sender"></param>
        /// <param name="e"></param>
        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;
                }
            }
        }

        /// <summary>
        /// Handles the TextChanged event and tries to parse the text to a decimal value.
        /// </summary>
        /// <param name="sender"></param>
        /// <param name="e"></param>
        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 > Maximum)
                    v = Maximum;
                else if (v < Minimum)
                    v = Minimum;

                //Change the background color according to the warning and error levels
                Color c = Color.White;
                if (EnableErrorValue && v > ErrorValue)
                    c = ErrorColor;
                else if (EnableWarningValue && v > WarningValue)
                    c = WarningColor;

                BackColor = c;

                //Set the value property
                if (Value.CompareTo(v) != 0)
                    Value = v;
            }
        }
        #endregion

        /// <summary>
        /// Exits editing mode, and replaces the Text to the formatted version of Value
        /// </summary>
        public void Validate()
        {
            string dec = "";
            for (int i = 0; i < DecimalPlaces; i++)
                dec += "#";
            if (dec.Length > 0)
                dec = "." + dec;

            string s;
            if (ThousandsSeparator)
                s = String.Format("{0:0,0" + dec + "}", Value);
            else
                s = String.Format("{0:0" + dec + "}", Value);

            Text = s;
        }

    }
}

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:

C#
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Text.RegularExpressions;
namespace Rankep.MathsEvaluator
{
    /// <summary>
    /// A class to evaluate mathematical expressions
    /// </summary>
    public static class MathsEvaluator
    {
        /// <summary>
        /// Returns the evaluated value of the expression
        /// </summary>
        /// <remarks>Throws ArgumentExpression</remarks>
        /// <param name="expression">A mathematical expression</param>
        /// <returns>Value of the expression</returns>
        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);
            }
        }
        /// <summary>
        /// Tries to evaluate a mathematical expression.
        /// </summary>
        /// <param name="expression">The expression to evaluate</param>
        /// <param name="value">The parsed value</param>
        /// <returns>Indicates whether the evaluation was succesful</returns>
        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;
            }
        }
        /// <summary>
        /// Determines if an expression contains only valid characters
        /// </summary>
        /// <param name="s">The expression to check</param>
        /// <returns>Indicates whether only valid characters were used in the expression</returns>
        public static bool IsExpression(string s)
        {
            //Determines whether the string contains illegal characters
            Regex RgxUrl = new Regex("^[0-9+*-/^()., ]+$");
            return RgxUrl.IsMatch(s);
        }
        /// <summary>
        /// Splits an expression into elements
        /// </summary>
        /// <param name="expression">Mathematical expression</param>
        /// <param name="operators">Operators used as delimiters</param>
        /// <returns>The list of elements</returns>
        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;
        }
        /// <summary>
        /// Calculates the value of an expression
        /// </summary>
        /// <param name="expression">The expression to evaluate</param>
        /// <returns>The value of the expression</returns>
        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 >= operators.Values.Min(); i--)
            {
                //loop while there are any operators left in the list from the current precedence level
                while (elements.Count >= 3
                    && elements.Any(element => element.Length == 1 &&
                        operators.Where(op => op.Value == i)
                        .Select(op => op.Key).Contains(element[0])))
                {
                    //get the position of this element
                    int pos = elements
                        .FindIndex(element => element.Length == 1 &&
                        operators.Where(op => op.Value == i)
                        .Select(op => op.Key).Contains(element[0]));
                    //evaluate it's value
                    value = EvaluateOperation(elements[pos], elements[pos - 1], elements[pos + 1]);
                    //change the first operand of the operation to the calculated value of the operation
                    elements[pos - 1] = value.ToString();
                    //remove the operator and the second operand from the list
                    elements.RemoveRange(pos, 2);
                }
            }
            return value;
        }
        /// <summary>
        /// Performs an operation on the operands
        /// </summary>
        /// <param name="oper">Operator</param>
        /// <param name="operand1">Left operand</param>
        /// <param name="operand2">Right operand</param>
        /// <returns>Value of the operation</returns>
        private static decimal EvaluateOperation(string oper, string operand1, string operand2)
        {
            if (oper.Length == 1)
            {
                decimal op1 = Parse(operand1);
                decimal op2 = Parse(operand2);
                decimal value = 0;
                switch (oper[0])
                {
                    case '+':
                        value = op1 + op2;
                        break;
                    case '-':
                        value = op1 - op2;
                        break;
                    case '*':
                        value = op1 * op2;
                        break;
                    case '/':
                        value = op1 / op2;
                        break;
                    case '^':
                        value = Convert.ToDecimal(Math.Pow(Convert.ToDouble(op1), Convert.ToDouble(op2)));
                        break;
                    default:
                        throw new ArgumentException("Unsupported operator");
                }
                return value;
            }
            else
            {
                throw new ArgumentException("Unsupported operator");
            }
        }
    }
}  
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)


Written By
Student
Poland Poland
This member has not yet provided a Biography. Assume it's interesting and varied, and probably something to do with programming.

Comments and Discussions

 
QuestionDownload link is broken Pin
Arethrid28-May-18 2:57
Arethrid28-May-18 2:57 
BugWrong output when leaving an empty text field with thousands separator on Pin
Timmeey30-Dec-14 22:36
Timmeey30-Dec-14 22:36 
GeneralMy vote of 5 Pin
Paul Boothroyd14-Dec-11 3:12
Paul Boothroyd14-Dec-11 3:12 
Excellent Article
GeneralGood work Orban Pin
sudhansu_k1237-Dec-11 19:31
sudhansu_k1237-Dec-11 19:31 
GeneralMy vote of 5 Pin
Kurniawan Prasetyo7-Dec-11 0:09
Kurniawan Prasetyo7-Dec-11 0:09 
AnswerRe: My vote of 5 Pin
Akos Orban7-Dec-11 1:23
Akos Orban7-Dec-11 1:23 
GeneralMy vote of 5 Pin
Kanasz Robert29-Nov-11 22:38
professionalKanasz Robert29-Nov-11 22:38 
QuestionVisualizing the result Pin
VallarasuS27-Nov-11 2:57
VallarasuS27-Nov-11 2:57 
AnswerRe: Visualizing the result Pin
Akos Orban27-Nov-11 4:29
Akos Orban27-Nov-11 4:29 

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.