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
{
[Serializable()]
[ToolboxBitmap(typeof(TextBox))]
public partial class NumericTextBox : TextBox
{
private Timer timer;
#region NumericTextBox Properties
private decimal value;
[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;
[Description("Indicates the minimum value for the numeric textbox control.")]
public decimal Minimum
{
get { return minimum; }
set { minimum = value; }
}
private decimal maximum;
[Description("Indicates the maximum value for the numeric textbox control.")]
public decimal Maximum
{
get { return maximum; }
set { maximum = value; }
}
private int decimalPlaces;
[Description("Indicates the number of decimal places to display.")]
public int DecimalPlaces
{
get { return decimalPlaces; }
set { decimalPlaces = value; }
}
private bool enableWarningValue;
[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;
[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;
[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;
[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;
[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;
[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;
[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;
[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;
[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; }
}
[Description("Indicates whether the expression entered should be automatically validated after a time set with the AutoValidationTime property.")]
public bool AutoValidate
{ get; set; }
[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
[Description("Occurs when the value in the numeric textbox control changes.")]
public event EventHandler ValueChanged;
#region NumericTextBox Initialization
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);
}
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);
}
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
void NumericTextBox_ValueChanged(object sender, EventArgs e)
{
if (AutoValidate)
{
timer.Interval = AutoValidationTime;
timer.Start();
}
}
void timer_Tick(object sender, EventArgs e)
{
timer.Stop();
Validate();
Select(Text.Length, 0);
}
void NumericTextBox_Leave(object sender, EventArgs e)
{
Validate();
}
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;
}
}
}
private void NumericTextBox_TextChanged(object sender, EventArgs e)
{
timer.Stop();
decimal v;
if (MathsEvaluator.MathsEvaluator.TryParse(Text, out v))
{
if (v > Maximum)
v = Maximum;
else if (v < Minimum)
v = Minimum;
Color c = Color.White;
if (EnableErrorValue && v > ErrorValue)
c = ErrorColor;
else if (EnableWarningValue && v > WarningValue)
c = WarningColor;
BackColor = c;
if (Value.CompareTo(v) != 0)
Value = v;
}
}
#endregion
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:
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Text.RegularExpressions;
namespace Rankep.MathsEvaluator
{
public static class MathsEvaluator
{
public static decimal Parse(string expression)
{
decimal d;
if (decimal.TryParse(expression, out d))
{
return d;
}
else
{
return CalculateValue(expression);
}
}
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;
}
}
public static bool IsExpression(string s)
{
Regex RgxUrl = new Regex("^[0-9+*-/^()., ]+$");
return RgxUrl.IsMatch(s);
}
private static List<string> TokenizeExpression(string expression, Dictionary<char, int> operators)
{
List<string> elements = new List<string>();
string currentElement = string.Empty;
int state = 0;
int bracketCount = 0;
for (int i = 0; i < expression.Length; i++)
{
switch (state)
{
case 0:
if (expression[i] == '(')
{
state = 1;
bracketCount = 0;
if (currentElement != string.Empty)
{
elements.Add(currentElement);
elements.Add("*");
currentElement = string.Empty;
}
}
else if (operators.Keys.Contains(expression[i]))
{
elements.Add(currentElement);
elements.Add(expression[i].ToString());
currentElement = string.Empty;
}
else if (expression[i] != ' ')
{
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] != ' ')
{
currentElement += expression[i];
}
break;
case 2:
if (operators.Keys.Contains(expression[i]))
{
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;
}
}
if (currentElement.Length > 0)
{
elements.Add(currentElement);
}
return elements;
}
private static decimal CalculateValue(string expression)
{
Dictionary<char, int> operators = new Dictionary<char, int>
{
{'+', 1}, {'-', 1}, {'*', 2}, {'/', 2}, {'^', 3}
};
List<string> elements = TokenizeExpression(expression, operators);
decimal value = 0;
for (int i = operators.Values.Max(); i >= operators.Values.Min(); i--)
{
while (elements.Count >= 3
&& elements.Any(element => element.Length == 1 &&
operators.Where(op => op.Value == i)
.Select(op => op.Key).Contains(element[0])))
{
int pos = elements
.FindIndex(element => element.Length == 1 &&
operators.Where(op => op.Value == i)
.Select(op => op.Key).Contains(element[0]));
value = EvaluateOperation(elements[pos], elements[pos - 1], elements[pos + 1]);
elements[pos - 1] = value.ToString();
elements.RemoveRange(pos, 2);
}
}
return value;
}
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.
CodeProject