Click here to Skip to main content
65,938 articles
CodeProject is changing. Read more.
Articles
(untagged)

Basic Calculating TextBox in VB.NET

0.00/5 (No votes)
27 Nov 2014 1  
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

  1. Pasting (verifying content of pasted text)
  2. Prevent double decimal separators (updated Nov 27)
  3. Setting decimal places
  4. Verify DefaultValue property (make sure it is numeric!)
  5. 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 to false on creation, handling what to do when the user moves on to another control without having completed the operation (version 2, uploaded)

License

This article has no explicit license attached to it but may contain usage terms in the article text or the download files themselves. If in doubt please contact the author via the discussion board below.

A list of licenses authors might use can be found here