Basic Calculating TextBox in VB.NET





4.00/5 (6 votes)
A simple extension to the original TextBox, allowing simple calculations (+, /, *, -)
Overview
This is a simple extension of the System.Windows.Forms.TextBox
component. Only the following keystrokes are allowed: digits, operators (+,-,*,/), escape, enter, backspace, decimal separator, group separator. This is an adaptation of the Numeric TextBox found on MSDN.
Background
I was working on a project where we have to add costs to items. At first, I wanted to restrict the input to digits and other needed keystrokes (backspace, decimal point, negative sign). But sometimes, our costs involve a few basic calculations. The last thing I wanted was for users to pull out their calculators (most of which, such as myself, don't even have one or don't bother taking it out and simply use Windows Calculator). I looked around and couldn't find anything. So I figure I am not the only one who will benefit from this simple code.
The Code
First, let's create our <CalcTextBox>
class:
Imports System.Globalization
Public Class CalcTextBox
Inherits TextBox
Private valDefault As String = "0.00" ' Starting value, can be changed to ""
Private SpaceOK As Boolean = False ' Allow spaces or not
Private Event TotalValidated() ' See notes below
Public Property DefaultValue As String
Get
Return valDefault
End Get
Set(value As String)
valDefault = value
End Set
End Property
Public ReadOnly Property IntValue() As Integer
Get
Return Int32.Parse(Me.Text)
End Get
End Property
Public Property AllowSpace() As Boolean
Get
Return Me.SpaceOK
End Get
Set(ByVal value As Boolean)
Me.SpaceOK = value
End Set
End Property
Public Sub New()
Me.Text= valDefault
Me.TextAlign= HorizontalAlignment.Right
End Sub
End Class
As it is now, this is a simple TextBox
. The IntValue
property returns the integer portion of the TextBox
. The DefaultValue
property allows to change the default TextBox
value to whatever number you decide. Having a default value makes it obvious to the user what it is they should input. Also when the user will press 'Escape', the TextBox
will reset to this value. The AllowSpace
property was taken from the original MSDN post, I didn't bother changing it. Some languages separate the thousands using spaces, so this allows for that to happen.
The New() sub
ensures we put the DefaultValue
into the TextBox
upon creation (visible in design mode as well). It also aligns the text to the right (this could be done as a property as well, but I figured since this is a calculator-type TextBox
, a right-alignment is best.
Select content when control gets Focus
Now, I also wanted the contents of the textbox
to be selected as soon as the control gets focus. This is done using two methods: OnGotFocus
(this happens on tab stop) and OnMouseUp
(when the user clicks on the control). Now in doing this, we don't want EVERY mouse click to cause the selection. So we'll use a flag that we will set when the control already has focus (alreadyFocused
). I snipped this from Tim Murphy (see 2nd answer on this page).
Private alreadyFocused As Boolean ' Self explanatory
Protected Overrides Sub OnMouseUp(ByVal mevent As MouseEventArgs)
MyBase.OnMouseUp(mevent)
' This event selects the whole text on mouseup if the control doesn't already have focus
If Not Me.alreadyFocused AndAlso Me.SelectionLength = 0 Then
Me.alreadyFocused = True
Me.SelectAll()
End If
End Sub
Protected Overrides Sub OnLeave(ByVal e As EventArgs)
If Not calculated Then
' Calculation underway but not complete
' Reset to original value (or last calculated value)
' Reset valOperator and calculated, then Beep
' Raise TotalValidated event (v2)
Me.Text = valOriginal
calculated = True
valOperator = Nothing
Beep()
End If
RaiseEvent TotalValidated()
MyBase.OnLeave(e)
Me.alreadyFocused = False
End Sub
Protected Overrides Sub OnGotFocus(e As EventArgs)
MyBase.OnGotFocus(e)
' This event selects the whole text on tab stop if the control doesn't already have focus
If MouseButtons = MouseButtons.None Then
Me.SelectAll()
Me.alreadyFocused = True
End If
End Sub
The Interesting Part!
Ok, so now we have a TextBox
with a few addons, but it doesn't do much. The next step is to filter the keystrokes. First, we handle the ones we accept (digits, separators, backspace, enter, escape, etc.) and lastly we'll handle all the rest that we don't want in a simple 'else
' statement. Let's see how it works. Here is the full code of the OnKeyPress
subroutine:
Private valOriginal As Double = valDefault ' First number in any operation
Private valCalculated As Double = valDefault ' Final calculated value in an operation
Private valOperator As Char = Nothing ' +, -, /, *
Private calculated As Boolean = True ' False if operation is in progress,
' True if operation is calculated
' Restricts the entry of characters to digits (including hex),
' the negative sign, the e decimal point, and editing keystrokes (backspace)
' as well as standard operators (+,-,*,/).
Protected Overrides Sub OnKeyPress(ByVal e As KeyPressEventArgs)
MyBase.OnKeyPress(e)
Dim numberFormatInfo As NumberFormatInfo = _
System.Globalization.CultureInfo.CurrentCulture.NumberFormat
Dim decimalSeparator As String = numberFormatInfo.NumberDecimalSeparator
Dim groupSeparator As String = numberFormatInfo.NumberGroupSeparator
Dim negativeSign As String = numberFormatInfo.NegativeSign
Dim keyInput As String = e.KeyChar.ToString()
If [Char].IsDigit(e.KeyChar) Then
' Digits are OK, nothing to do
ElseIf keyInput.Equals(decimalSeparator) OrElse keyInput.Equals(groupSeparator) Then
' Decimal separator is OK, make sure we don't have one already
If keyInput.Equals(decimalSeparator) And Me.Text.Contains(decimalSeparator) then
e.Handled=True
Beep()
End If
ElseIf e.KeyChar = vbBack Then
' Backspace key is OK, nothing to do
ElseIf Me.SpaceOK AndAlso e.KeyChar = " "c Then
' If spaces are allowed, nothing to do
ElseIf e.KeyChar = Microsoft.VisualBasic.ChrW(Keys.Escape) Then
' Escape = reset to default values
Me.Text = valDefault
Me.SelectAll()
valOriginal = 0
valCalculated = 0
valOperator = Nothing
calculated = True
RaiseEvent TotalValidated() ' See TotalValidated notes
ElseIf e.KeyChar = Microsoft.VisualBasic.ChrW(Keys.Return) Then
' Enter (proceed with calculation)
If Not calculated then
If CalculateTotal(e)=True then
' The operation was a success
valOperator = Nothing
Me.Text = valCalculated
calculated = True
Me.SelectAll()
End If
RaiseEvent TotalValidated() ' See TotalValidated notes
End If
e.Handled = True
ElseIf e.KeyChar = "/"c OrElse e.KeyChar = "*"c _
OrElse e.KeyChar = "+"c OrElse e.KeyChar = "-"c Then
' Operation required
If Me.Text <> "" Then
' Previous text was not an operator
If calculated = False Then
' This is the 2nd operator, so we have to get the result of the first
' operation before proceeding with this one
Dim tmpResult as Boolean = CalculateTotal(e) ' Result stored in valOriginal
Else
' This is the first operator, store the first operand into valOriginal
valOriginal = CDbl(Me.Text)
End If
' Indicate that an operation is active but the total has not been calculated
' (2nd operand to follow)
calculated = False
Me.Text = ""
End If
valOperator = e.KeyChar ' Store the operator before we get the 2nd operand
e.Handled = True ' Swallow this key
Else
' Swallow this invalid key and beep
e.Handled = True
Beep()
End If
End Sub
Let's try to explain what is happening here.
Protected Overrides Sub OnKeyPress(ByVal e As KeyPressEventArgs)
MyBase.OnKeyPress(e)
When the user types anything, whether it is a valid keystroke or not, it will fire the OnKeyPress
event. Since we're overriding it, the first line in the Sub
ensures that we call the parent version of this event. The important thing to know is that the OnKeyPress
event happens before anything is added to the TextBox. This is important since we want to control the input. Some keystrokes will be ignored, others will go through.
Dim numberFormatInfo As NumberFormatInfo = System.Globalization.CultureInfo.CurrentCulture.NumberFormat
Dim decimalSeparator As String = numberFormatInfo.NumberDecimalSeparator
Dim groupSeparator As String = numberFormatInfo.NumberGroupSeparator
Dim negativeSign As String = numberFormatInfo.NegativeSign
Next, we get the decimal separator, negative symbol and group separator from the user's settings. This is especially useful where I live, since some use the different settings (comma as the decimal separator and space as the thousand group separator -- although this last one is never used when inputting numbers, it could be useful when pasting). In any case, it's just a few more lines of code, and makes things almost universal.
If... elseif... andif... else... endif... That's a lot of if's!!!
When the user types digits, separators, or backspace, there's no need to do anything. We will let the TextBox
act normally. This is what these lines of code do:
If [Char].IsDigit(e.KeyChar) Then
ElseIf keyInput.Equals(decimalSeparator) OrElse keyInput.Equals(groupSeparator) Then
' Decimal separator is OK, make sure we don't have one already
If keyInput.Equals(decimalSeparator) And Me.Text.Contains(decimalSeparator) then
e.Handled=True
Beep()
End If
ElseIf e.KeyChar = vbBack Then
ElseIf M.SpaceOK AndAlso e.KeyChar = " "c Then
The first line checks if the keystroke is a digit. The following blocks basically work the same way, but look for different keystrokes. The second block looks for a decimal or group separator but also checks if the decimal separator is already present, preventing it from being entered twice. The third block looks for the backspace key and the last one for the space key (and only if it is allowed through the SpaceOK
variable). In all these cases (except the 2nd one if we already have a decimal separator), the keystroke is allowed, so there is no need to do anything. We simply let the process go through.
In the last line, you might have noticed the 'c' after " ". This is not a typo. It simply converts " " (a string containing only a space) to a KeyChar. You will see this later on in the code for other comparisons.
Let's proceed to the 'escape' key. Basically, this key should reset everything to default values, including the .Text
property. It should also select all the contents so the client can easily proceed with the next operation. This is done using the following code (we're still in the same if
...then
clause):
ElseIf e.KeyChar = Microsoft.VisualBasic.ChrW(Keys.Escape) Then
' Escape = reset to default values
Me.Text = valDefault
Me.SelectAll()
valOriginal = 0
valCalculated = 0
valOperator = Nothing
calculated = True
Don't worry if you're not sure why some of these variables are assigned these values, you'll understand later on.
Let's skip to the last 'else
' statement of our giant if
..then
clause. This basically is where the logic will bring us everytime an invalid keystroke is pressed. If the keystroke does not correspond to those mentioned above it, then we need to 'swallow' the keystroke, in other words, prevent it from being added to the TextBox
. As you might have noticed, the subroutine has the <KeyPressEventArg>
'e
' is passed along when it is fired. This is not just a variable, but a class that includes functions and properties. So how do we tell it to skip those unwanted keystrokes? We do this using e.Handled=True
. This basically says we've handled the keystroke, no need to do anything else with it. The next events in the chain will not process it (i.e., the event in charge of drawing or painting the character in the TextBox
will not draw anything). We'll also add a beeping sound to advise the user of his mistake. Here is the code:
Else
' Swallow this invalid key and beep
e.Handled = True
Beep()
End If
Next Step
So far, we've modified our TextBox
to act only on digits (and a few other keystrokes), autoselect on focus (either by clicking or tab stop) and reset when the user presses 'escape'.
Before we dig any further into the code, let's define how we want our TextBox
to work. The user can type any number and as soon as he types one of the operators, we have to store the first operand in a variable (valOriginal
) and the operator in another variable (valOperator
). Then the TextBox
clears out, ready for the user to input the 2nd number, or operand, in our operation. Usually when he's done, the user will press 'enter'. When this happens, we calculate using the first operand (valOriginal
), the operator (valOperator
, telling us what to do), and the current value of our TextBox
(which is not empty) as the last operand.
Too simple. What if we have multiple operations in a row? Then, we have to start calculating after the second operand but before the second operator. For example, if the user types 23*1.5+5, we want to calculate 23x1.5 before adding 5 (we won't use operator precedence, at least not in this version). In order to do this, we will use a variable called 'calculated
' which will always be true
, except when we catch an operator. When false
, it will tell us that we've started a new operation and the next time the user presses 'Enter' or another operator key (and the TextBox.Text
value is not empty, giving us our second operand), we must not store the number in our valOriginal
variable but instead do the math right away and then store the result in that very same variable, passing it on to the next operation. Here is the code with comments to help along. This part of code is just before the last 'else
' statement we just discussed.
ElseIf e.KeyChar = Microsoft.VisualBasic.ChrW(Keys.Return) Then
' Enter (proceed with calculation)
If Not calculated Then
If CalculateTotal(e) = True Then
' The operation was a success
valOperator = Nothing
Me.Text = valCalculated
calculated = True
Me.SelectAll()
End If
RaiseEvent TotalValidated()
End If
e.Handled = True
ElseIf e.KeyChar = "/"c OrElse e.KeyChar = "*"c _
OrElse e.KeyChar = "+"c OrElse e.KeyChar = "-"c Then
' Operation required
If Me.Text <> "" Then
' Previous text was not an operator
If calculated = False Then
' This is the 2nd operator, so we have to get the result of the first
' operation before proceeding with this one
Dim tmpResult as Boolean = CalculateTotal(e) ' Result stored in valOriginal
Else
' This is the first operator, store the first operand into valOriginal
valOriginal = CDbl(Me.Text)
End If
' Indicate that an operation is active but the total has not been calculated
' (2nd operand to follow)
calculated = False
Me.Text = ""
End If
valOperator = e.KeyChar ' Store the operator before we get the 2nd operand
e.Handled = True ' Swallow this key
Next is the CalculateTotal
function:
Private Function CalculateTotal(ByRef e As KeyPressEventArgs) As Boolean
' This function will return True if successful otherwise False (v2)
If calculated = False And valOperator <> Nothing And Me.Text <> "" Then
' Make sure we have an operation to do (calculated=false),
' and operator (valOperator) and a 2nd operand (Me.Text)
Select Case valOperator
Case "*"c
valCalculated = valOriginal * [Double].Parse(Me.Text)
Case "-"c
valCalculated = valOriginal - [Double].Parse(Me.Text)
Case "+"c
valCalculated = valOriginal + [Double].Parse(Me.Text)
Case "/"c
If [Double].Parse(Me.Text) = 0 Then
' Division by 0, stop everything, reset and Beep
Me.Text = valDefault
valOperator = Nothing
valOriginal = 0.0
valCalculated = 0.0
calculated = True
e.Handled = True
Me.SelectAll()
Beep()
Return False ' Unsuccessful, we had to reset
End If
valCalculated = valOriginal / [Double].Parse(Me.Text)
End Select
valOriginal = valCalculated
e.Handled = True ' Swallow this key after operation
End If
Return True
End Function
You'll notice that in both 'elseif
' cases we just added, we end up swallowing the key. This is important, we don't want those keys to show in the TextBox
.
The TotalCalculated event
This is a new one in version 2. I simply wanted a way to be noticed either when the user pressed 'Enter', when the contents was reset or when the control lost focus. This is useful if you want to use the content of this CalcTextBox
is used to calculate other items. In my case, as soon as the event is fired in my application, I use the contents to calculate the total costs and update a label on the form.
Conclusion
The only problem I've seen is that you can't negate, since the negative sign is an operator. But for my purpose, we have no need to negate, or do operations using negative operands. Our costs will only be positive, if anything we will subtract numbers from other amounts. All of which is possible using this code.
So there you have it. Simple and easily improvable. If you want to use the provided class file, simply include it in your project and compile your project. The new control will then be available from the toolbox.
Future Addons
- Pasting (verifying content of pasted text)
Prevent double decimal separators(updated Nov 27)- Setting decimal places
- Verify
DefaultValue
property (make sure it is numeric!) - Ensure the 2nd operand is not just a decimal separator, group separator or space before calculating
History
- November 27, 2014: Initial version
- November 27, 2014: Added double decimal separator verification
- November 28, 2014: Added
TotalValidated()
event, multiline set tofalse
on creation, handling what to do when the user moves on to another control without having completed the operation (version 2, uploaded)