A Visual Basic Calculator Program

Before Visual Basic .NET arrived in 2002, the highly popular Visual Basic 6 provided programmers with the ability to create what were known as control arrays. In real terms, a control array was actually a collection of controls that could be programmed as an array thanks to some fairly complex behind-the-scenes code provided by Microsoft.

Like other array variables, each member of a control array had an array subscript by which it could be referenced. If one button in an array of button controls was clicked, the control's array subscript was passed to the event handler, which would determine which button had been clicked and take the required action for that button.

From the programmer's point of view, the ability to create control arrays at design time by simply copying an existing control and pasting it onto the form as many times as required was an attractive feature of Visual Basic 6.

Microsoft abandoned control arrays with VB .NET, somewhat to the chagrin of a large number of VB6 developers. One of the main complaints was that existing VB6 applications that employed control arrays were difficult to upgrade to VB .NET. Microsoft provided a workaround that involving writing some rather inelegant-looking and complex code in order to create an array of controls at run time. However, since one of the attractions of Visual Basic in the past has been that it is an easy programming language for new programmers to learn, this was not an ideal solution.

Microsoft also provided a number of "control array compatibility classes" that could be added to the VB .NET toolbox, but at the same time advised developers not to use them as they could not guarantee continued support in future versions of Visual Basic .NET. Indeed, Microsoft subsequently declared these classes to be obsolete.

A lively debate ensued between developers over whether or not control arrays should have been omitted from the .NET platform. Many methods were suggested for creating control arrays programmatically, and the subject continued to generate controversy. Ultimately, one had to accept that Visual Basic .NET was very different to Visual Basic 6, and adopt a new way of working.

Despite the demise of the control array, it is still possible to manage a related group of objects as if they were members of an array. Although things are no longer quite so straightforward in this respect, Visual Basic now has features that provide more flexibility to the programmer. For example, a control array in Visual Basic 6 could only include controls of one type. By contrast, we can now assign the same event handler to the Click event of any number of controls on a form, even if they are not all of the same type.

The program presented in this section makes use of this feature to enable a single event handler to handle the Click event for all of the button controls for a simple calculator application. The application is very similar to the Standard version of the desktop calculator provided by Microsoft Windows.

To implement the calculator program, proceed as follows:

  1. Open a new project called "Calculator" and create an interface like the one illustrated below.

The Calculator program interface

The Calculator program interface


  1. Set the control names and other properties as shown in the table below.

Calculator Form Controls
ControlNameAdditional Properties
FormfrmCalculatorSize: 240, 300
Text: "Calculator"
LabellblDisplayLocation: 12, 9
AutoSize: False
Size: 199, 45
BackColor: White
BorderStyle: Fixed3D
Font: Courier New, 12pt, style=Bold
TextAlign: BottomRight
LabellblSuperScriptLocation: 17, 10
AutoSize: False
Size: 189, 20
BackColor: White
BorderStyle: None
Font: Courier New, 8.25pt
TextAlign: MiddleRight
LabellblMemStatusLocation: 17, 35
AutoSize: False
Size: 16, 20
BackColor: White
BorderStyle: None
Text: "M"
Font: Courier New, 12pt
TextAlign: MiddleRight
Visible: False
ButtoncmdMemClearLocation: 12, 69
Size: 35, 25
Text: "MC"
TextAlign: MiddleCenter
TabIndex: 24
ButtoncmdMemRecallLocation: 53, 69
Size: 35, 25
Text: "MR"
TextAlign: MiddleCenter
TabIndex: 25
ButtoncmdMemSaveLocation: 94, 69
Size: 35, 25
Text: "MS"
TextAlign: MiddleCenter
TabIndex: 23
ButtoncmdMemPlusLocation: 135, 69
Size: 35, 25
Text: "M+"
TextAlign: MiddleCenter
TabIndex: 26
ButtoncmdMemMinusLocation: 176, 69
Size: 35, 25
Text: "M-"
TextAlign: MiddleCenter
TabIndex: 27
ButtoncmdBSLocation: 12, 100
Size: 35, 25
Text: "←"
TextAlign: MiddleCenter
TabIndex: 11
ButtoncmdClearEntryLocation: 53, 100
Size: 35, 25
Text: "CE"
TextAlign: MiddleCenter
TabIndex: 22
ButtoncmdClearAllLocation: 94, 100
Size: 35, 25
Text: "C"
TextAlign: MiddleCenter
TabIndex: 21
ButtoncmdChangeSignLocation: 135, 100
Size: 35, 25
Text: "±"
TextAlign: MiddleCenter
TabIndex: 16
ButtoncmdSqrRootLocation: 176, 100
Size: 35, 25
Text: "√"
TextAlign: MiddleCenter
TabIndex: 17
Buttoncmd07Location: 12, 131
Size: 35, 25
Text: "7"
TextAlign: MiddleCenter
TabIndex: 7
Buttoncmd08Location: 53, 131
Size: 35, 25
Text: "8"
TextAlign: MiddleCenter
TabIndex: 8
Buttoncmd09Location: 94, 131
Size: 35, 25
Text: "9"
TextAlign: MiddleCenter
TabIndex: 9
ButtoncmdDivLocation: 135, 131
Size: 35, 25
Text: "/"
TextAlign: MiddleCenter
TabIndex: 15
ButtoncmdPercentLocation: 176, 131
Size: 35, 25
Text: "%"
TextAlign: MiddleCenter
TabIndex: 19
Buttoncmd04Location: 12, 162
Size: 35, 25
Text: "4"
TextAlign: MiddleCenter
TabIndex: 4
Buttoncmd05Location: 53, 162
Size: 35, 25
Text: "5"
TextAlign: MiddleCenter
TabIndex: 5
Buttoncmd06Location: 94, 162
Size: 35, 25
Text: "6"
TextAlign: MiddleCenter
TabIndex: 6
ButtoncmdMulLocation: 135, 162
Size: 35, 25
Text: "*"
TextAlign: MiddleCenter
TabIndex: 14
ButtoncmdInvLocation: 176, 162
Size: 35, 25
Text: "1/x"
TextAlign: MiddleCenter
TabIndex: 18
Buttoncmd01Location: 12, 193
Size: 35, 25
Text: "1"
TextAlign: MiddleCenter
TabIndex: 1
Buttoncmd02Location: 53, 193
Size: 35, 25
Text: "2"
TextAlign: MiddleCenter
TabIndex: 2
Buttoncmd03Location: 94, 193
Size: 35, 25
Text: "3"
TextAlign: MiddleCenter
TabIndex: 3
ButtoncmdMinLocation: 135, 193
Size: 35, 25
Text: "-"
TextAlign: MiddleCenter
TabIndex: 13
ButtoncmdEqualsLocation: 176, 193
Size: 35, 57
Text: "="
TextAlign: MiddleCenter
TabIndex: 20
Buttoncmd00Location: 12, 225
Size: 76, 25
Text: "0"
TextAlign: MiddleCenter
TabIndex: 0
ButtoncmdDecimalLocation: 94, 225
Size: 35, 25
Text: "."
TextAlign: MiddleCenter
TabIndex: 10
ButtoncmdAddLocation: 135, 225
Size: 35, 25
Text: "+"
TextAlign: MiddleCenter
TabIndex: 12

  1. In the code editor, add the following program statements to the frmCalculator class declaration:

Dim inputLength As Integer
Dim charLimit As Integer = 16
Dim opFlag As Boolean = False
Dim eqFlag As Boolean = False
Dim funcFlag As Boolean = False
Dim firstTerm, secondTerm, memory As Double
Dim activeOp As Integer = 0
Dim lastOp As Integer = 0
Dim startFlag As Boolean = False
Dim opCharArray() As Char = {"", "+", "-", "*", "/"}

For practical reasons, the number of digits that the calculator can display is limited to a maximum of eighteen characters, including any decimal point and a leading zero. The integer variables charLimit and inputLength are used to keep track of the number of characters allowed for the current input string, and the number of characters entered by the user, at any given time. The Boolean variables opFlag, eqFlag and funcFlag are used to determine whether the user has clicked on any of the operator buttons ("+", "-", "*", or "/"), the equals button ("="), or a function button ("±", "", "1/x" or "%").

Double-precision floating point variables are used to store numeric input and results. In an ongoing calculation, the number currently being entered by the user is stored in the variable secondTerm, and any value held in memory is stored in the variable memory. The integer variables activeOp and lastOp store numeric references to the operator most recently clicked and the operator previously clicked, if applicable.

The addition, subtraction, multiplication and division operators are referenced by the integer values 1, 2, 3 and 4 respectively. The Boolean variable startFlag is set to True if a number has been entered and an operator button has been pressed, to indicate that a calculation is in progress and that a value has been assigned to firstTerm. The character array variable opCharArray() simply holds the operator symbols that will be displayed by the program when an operator button is clicked.

The program has only one event handler - cmdButton_Click() - that is invoked when the user clicks on any of the calculator buttons. There are a number of subroutines that must be created first, however. The code for these procedures is given below, and should be entered as shown somewhere within the form's class definition. The first procedure is numClick().


The numClick() procedure:

Sub numClick(index As Integer)
  If opFlag = True Then
    If index = 11 Then
      Beep()
      Exit Sub
    Else
      lblDisplay.Text = ""
      opFlag = False
    End If
  End If

  If eqFlag = True Then
    eqFlag = False
    lblDisplay.Text = ""
  End If

  If funcFlag = True Then
    funcFlag = False
    lblDisplay.Text = ""
  End If

  inputLength = Len(lblDisplay.Text)

  If index = 11 Then
    Dim strLen = Len(lblDisplay.Text)
    If strLen > 1 Then
      lblDisplay.Text = Mid(lblDisplay.Text, 1, strLen - 1)
    Else
      lblDisplay.Text = "0"
    End If
    Exit Sub
  End If

  If index = 10 Then
    If InStr(1, lblDisplay.Text, ".") = 0 Then
      lblDisplay.Text &= "."
      If Microsoft.VisualBasic.Left (lblDisplay.Text, 1) = "0" Then
        charLimit = 18
      Else
        charLimit = 17
      End If
    End If
    Exit Sub
  End If

  If inputLength >= charLimit Then
    Exit Sub
  ElseIf inputLength = 1 Then
    If lblDisplay.Text = "0" Then
      If index = 0 Then
        Exit Sub
      Else
        lblDisplay.Text = index
      End If
    Else
      lblDisplay.Text &= index
    End If
  Else
    lblDisplay.Text &= index
  End If
End Sub

The numClick() subroutine is called whenever a numerical button, the decimal point or the backspace key is clicked on the calculator. The opening If . . . End If code block checks to see whether the previous click was an operator button (in which case the opFlag variable will be True). If so, a backspace at this point would be inappropriate.

The nested If . . . Else . . . End If block checks to see whether the current click is a backspace. If so, the program will issue an audible beep and exit the subroutine. Otherwise, it will clear the lblDisplay control's text and set opFlag to False.

The next If . . . End If block checks to see whether the previous click was the equals key (in which case the eqFlag variable will be True). If so, the program would have completed a calculation and be ready for the next operation. The lblDisplay control's text will be cleared and eqFlag set to False. A similar situation arises if a function key has just been clicked, so the third If . . . End If block checks to see whether the previous click was a function key (in which case the funcFlag variable will be True). If so, the main text window will be cleared and funcFlag set to False.

If the preceding conditional statements do not apply, then either the last button clicked (if any) was a numeric key, a decimal point or a backspace (or the input was cleared). The next line of code sets the inputLength variable to the length of the text currently displayed in the main section of the output window. The next If . . . End If block after this statement checks for a Backspace key click (index = 11).

If this is the case, a nested If . . . Else . . . End If block checks to see whether inputLength is greater than 1. If so, the last character added to the lblDisplay control's text is removed (the Mid() string function is used to achieve this). Otherwise (since the input would effectively now be cleared), the lblDisplay control's text is set to "0".

The next If . . . End If block checks for a decimal point key click (index = 10). A nested If . . . End If block checks to see if a decimal point already exists in the lblDisplay control's text. If so, no action will be taken and the code exits the subroutine. Otherwise, a decimal point is added to the control's text and a nested If . . . Else . . . End If block checks to see whether the leftmost character in the string is a zero. If so, the charLimit variable is set to 18, otherwise it is set to 17 (the string is allowed to contain up to sixteen significant digits, not including a decimal point or a leading zero).

The final If . . . Else . . . End If block checks to see if inputLength has reached or exceeded the current value of charLimit, and if so exits the subroutine. Otherwise the code checks to see if inputLength is 1. If not, the digit for the key that was clicked is appended to the lblDisplay control's text. If inputLength is equal to 1 but the existing digit is not zero, the same thing happens. If the existing digit is zero, the same thing still happens unless the input key itself was also zero. If this occurs, the code exits the subroutine.

The next procedure we shall write is opClick().


The opClick() procedure:

Sub opClick(index As Integer)
  If opFlag = True Then
    lastOp = index
    lblSuperScript.Text = lblDisplay.Text & " " & opCharArray(index)
    Exit Sub
  Else
    opFlag = True
    activeOp = lastOp
    lastOp = index
    If startFlag = False Then
      firstTerm = CDbl(lblDisplay.Text)
      startFlag = True
    Else
      secondTerm = CDbl(lblDisplay.Text)
      Select Case activeOp
        Case 1
          firstTerm += secondTerm
        Case 2
          firstTerm -= secondTerm
        Case 3
          firstTerm *= secondTerm
        Case 4
          firstTerm /= secondTerm
      End Select
      lblDisplay.Text = firstTerm
    End If
    lblSuperScript.Text = lblDisplay.Text & " " & opCharArray(index)
  End If
End Sub

The opClick() subroutine is called each time the user clicks on an operator button and accepts the index value assigned by the cmdButton_Click() event handler (1, 2, 3 or 4) as its argument. If no operator is currently active, the opFlag variable is set to True, and lastOp is set to the index value.

If startFlag is set to False (which will be the case if this is the first operator that has been invoked in the current calculation), then the firstTerm variable is set to the value obtained by converting the lblDisplay control's text to a double-precision floating point value, and startFlag is set to True. Otherwise, the value of the conversion is assigned to the secondTerm variable, and the activeOp variable is assessed using a Select Case block.

Depending on the value of activeOp, the value of firstTerm is modified by adding or subtracting the value of secondTerm from it, multiplying it by secondTerm, or dividing it by secondTerm. The lblDisplay control's text is then set to the text representation of the current value of firstTerm. Finally, the lblSuperscript control's text is set to the same value as the lblDisplay control's text, followed by a space, followed by the character that represents the operator key clicked by the user.

The next procedure is funcClick(), which is (thankfully!) somewhat shorter.


The funcClick() procedure:

Sub funcClick(index As Integer)
  Select Case index
    Case 1
      lblDisplay.Text = -CDbl(lblDisplay.Text)
      funcFlag = True
    Case 2
      lblDisplay.Text = Math.Sqrt(CDbl(lblDisplay.Text))
      funcFlag = True
    Case 3
      lblDisplay.Text = 1 / CDbl(lblDisplay.Text)
      funcFlag = True
    Case 4
      Dim percentage As Double
      percentage = CDbl(lblDisplay.Text)
      lblDisplay.Text = firstTerm * percentage / 100
      funcFlag = True
  End Select
End Sub

The funcClick() subroutine is called whan a user clicks on one of the function keys ("±", "", "1/x" or "%"), and accepts the index value assigned by the cmdButton_Click() event handler (1, 2, 3 or 4) as its argument. A Select Case block is used to determine which of the function keys has been clicked and take the appropriate action.

If the change sign function has been invoked (index value = 1) , the lblDisplay control's text value is converted to a double-precision floating point value, negated, and then re-displayed as the control's text. If the square root function has been invoked, the lblDisplay control's text value is converted to a double-precision floating point value, the Math.Sqrt() function is used to find the square root of the value, and the new value is re-displayed as the lblDisplay control's text.

A similar process occurs for the inverse function, which re-displays the original value as its inverse value (for example, "5" becomes "0.2"). If the percentage key ("%") has been clicked, a local double-precision floating point variable called percentage is declared, and the lblDisplay control's text value is converted to a floating point value and assigned to percentage. The variable firstTerm is then multiplied by percentage and divided by 100. The result is assigned to the lblDisplay control's text property.

The subroutine that provides the final result our our calculation is evaluate ().


The evaluate() procedure:

Sub evaluate()
  Dim result As Double
  If startFlag = False Then
    Exit Sub
  Else
    activeOp = lastOp
    secondTerm = CDbl(lblDisplay.Text)
    Select Case activeOp
      Case 1
        result = firstTerm + secondTerm
      Case 2
        result = firstTerm - secondTerm
      Case 3
        result = firstTerm * secondTerm
      Case 4
        result = firstTerm / secondTerm
    End Select
    clearAll()
    lblDisplay.Text = result
    eqFlag = True
  End If
End Sub

The evaluate() subroutine first checks whether any operator has been invoked. If not, the code exits the procedure without taking any further action. Otherwise, activeOp is assigned the value of lastOp, and secondTerm is assigned the value represented by the lblDisplay control's text property after it has been converted to a double-precision floating point value.

A Select Case block evaluates activeOp to determine which operator has been invoked, and then applies the operator to firstTerm and secondTerm to calculate the value of result. The clearAll() procedure is then called to clear the display and reset the value of various global variables. Finally, the lblDisplay control's text property is set to display the result of the calculation, and the value of eqFlag is set to True (this is necessary to ensure that any further input is treated as a new calculation).

The final subroutine is clearAll(), which is used to clear the display and do a little housekeeping (essentially, it resets most of the global variables to their initial values, with the notable exception of memory).


The clearAll() procedure:

Sub clearAll()
  inputLength = 0
  charLimit = 16
  opFlag = False
  firstTerm = 0
  secondTerm = 0
  activeOp = 0
  lastOp = 0
  startFlag = False
  lblDisplay.Text = "0"
  lblSuperScript.Text = ""
End Sub

The main part of the code is the cmdButton_Click() event handler, which deals with all button clicks made by the user.


The cmdButton_Click() event handler:

Private Sub cmdButton_Click(sender As Object, e As EventArgs) _
  Handles cmd00.Click, cmd01.Click, cmd02.Click, cmd03.Click, cmd04.Click, _
  cmd05.Click, cmd06.Click, cmd07.Click, cmd08.Click, cmd09.Click, _
  cmdDecimal.Click, cmdBS.Click, cmdAdd.Click, cmdMin.Click, cmdMul.Click, _
  cmdDiv.Click, cmdChangeSign.Click, cmdSqrRoot.Click, cmdInv.Click, _
  cmdPercent.Click, cmdEquals.Click, cmdClearAll.Click, cmdClearEntry.Click, _
  cmdMemSave.Click, cmdMemClear.Click, cmdMemRecall.Click, cmdMemPlus.Click, _
  cmdMemMinus.Click

  Select Case sender.TabIndex
    Case 0 To 11
      numClick(sender.TabIndex)
    Case 12 To 15
      opClick(sender.TabIndex - 11)
    Case 16 To 19
      funcClick(sender.TabIndex - 15)
    Case 20
      evaluate()
    Case 21
      clearAll()
    Case 22
      lblDisplay.Text = "0"
    Case 23
      memory = CDbl(lblDisplay.Text)
      If memory <> 0 Then
        lblMemStatus.Visible = True
      Else
        lblMemStatus.Visible = False
      End If
    Case 24
      memory = 0
      lblMemStatus.Visible = False
    Case 25
      lblDisplay.Text = memory
    Case 26
      memory += CDbl (lblDisplay.Text)
      If memory = 0 Then
        lblMemStatus.Visible = False
      Else
        lblMemStatus.Visible = True
      End If
    Case 27
      memory -= CDbl (lblDisplay.Text)
      If memory = 0 Then
        lblMemStatus.Visible = False
      Else
        lblMemStatus.Visible = True
      End If
  End Select
End Sub

The cmdButton_Click() subroutine begins with a rather long list of events for which it is the handler. The bulk of the code is taken up by a Select Case block that determines which key has been clicked by the user, and takes the appropriate action. Although our collection of calculator buttons does not constitute a control array in Visual Basic 6 terms, it can be handled almost like an array by using the TabIndex property of each button as an array subscript. This requires a certain amount of care on the part of the programmer to ensure that each button has a unique TabIndex property, and that the correct TabIndex number is referenced elsewhere in the code for each button.

The event handler code itself is for the most part relatively simple. For values of sender.TabIndex up to 19, the handler simply calls the appropriate function with the relevant numeric argument. For a sender.TabIndex value of 20, the evaluate() subroutine is called. For a value of 21, the clearAll() subroutine is called, and a value of 22 (the Clear Entry button) resets the value of the lblDisplay control's text to "0".

The remaining cases (sender.TabIndex values of 23 - 27) all relate to calculator memory operations. For a sender.TabIndex value of 23, the lblDisplay control's text is converted to a double-precision floating point value and stored in the variable memory. If memory is not equal to zero, the lblMemStatus control (which displays the character "M") becomes visible to indicate that memory contains a non-zero value.

For a value of 23 (the Memory Clear button), memory is set to zero and the lblMemStatus control is hidden once more. For a value of 25 (the Memory Recall button), the lblDisplay control's text is set to the text representation of the value stored in memory. A value of 26 represents the Memory+ button, so the lblDisplay control's text is converted to a double-precision floating point value and added to the value of memory.

Given the possibility that under certain circumstances this could leave the value of memory as zero, an If . . . Else . . . End If block tests to see if memory is zero, and sets the visibility of the lblMemStatus control accordingly. A value of 27 represents the Memory- button, which requires similar handling to the Memory+ button. The only difference is that the lblDisplay control's text is converted to a double-precision floating point value and is subtracted from the value of memory.