Click here to Skip to main content
15,867,453 members
Articles / Desktop Programming / Win32

Basic Calculating TextBox in VB.NET

Rate me:
Please Sign up or sign in to vote.
4.00/5 (6 votes)
27 Nov 2014CPOL8 min read 49.6K   1.4K   5   4
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:

VB.NET
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).

VB.NET
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:

VB.NET
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.

VB.NET
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.

VB.NET
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:

VB.NET
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):

VB.NET
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:

VB.NET
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.

VB.NET
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:

VB.NET
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, along with any associated source code and files, is licensed under The Code Project Open License (CPOL)


Written By
Systems / Hardware Administrator Trim-Line Abitibi
Canada Canada
This member has not yet provided a Biography. Assume it's interesting and varied, and probably something to do with programming.

Comments and Discussions

 
QuestionBehavior ? Pin
BillWoodruff30-Nov-14 14:00
professionalBillWoodruff30-Nov-14 14:00 
AnswerRe: Behavior ? Pin
Member 109517101-Dec-14 7:22
professionalMember 109517101-Dec-14 7:22 
GeneralThoughts Pin
PIEBALDconsult28-Nov-14 15:39
mvePIEBALDconsult28-Nov-14 15:39 
GeneralRe: Thoughts Pin
Member 1095171029-Nov-14 13:06
professionalMember 1095171029-Nov-14 13:06 

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.