Visual Basic - The MathTest Project

The MathTest project was the subject of an assignment that I gave to students to give them the opportunity to achieve a broad range of criteria. It is of especial interest here because it is the first example we have looked at of a project that uses multiple forms. It also features control arrays, a code module, and some database programming.

Like the other projects presented on these pages it is somewhat limited in its scope because we don't want to overwhelm those of you who are new to programming. It does however offer plenty of opportunities for further development. The basic idea of this program is to allow users to test their basic arithmetic skills. The user must log in using a valid username and password, and is then presented with a menu screen that allows them to take a test in addition, subtraction, multiplication or division.

Each test can be taken at one of three levels - easy, moderate or hard. If the user has taken a test previously and achieved a pass at a given level, they may not take a test at a lower level. After each test, the user is taken to a results screen that shows them how they fared on each question and their total score for the test. Before leaving this screen, and providing they have passed the test, they are offered the option of recording their result.

The difficulty level of each test is determined by the size of the numbers used for the operands in each question, which we have set on a somewhat arbitrary basis.

  1. Open a new project called "MathTest" and save the project immediately to create the project folder.
  2. Download the file "mathtest.zip" from the link below. This file contains the program icon and the database file for the MathTest project. Unzip the contents and copy or save them into your project folder's \bin\Debug\ subdirectory.

Download mathtest.zip here.

  1. Create an interface like the one illustrated below (this is both the application's login form and its opening screen).

The MathTest login form interface

The MathTest login form interface


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

frmMathTest Form Controls
ControlNameAdditional Properties
FormfrmMathTestIcon: mathtest.ico
Size: 275, 175
Text: "MathTest - Login"
LabellblLoginInstructionLocation: 12, 14
Text: "Please enter username and password to log in:"
LabellblUserNameLocation: 12, 44
Text: "Username:"
TextAlign: MiddleRight
LabellblPasswordLocation: 12, 69
Text: "Password:"
TextAlign: MiddleRight
TextBoxtxtUserNameLocation: 77, 40
MaxLength: 15
Size: 166, 20
TextBoxtxtPasswordLocation: 77, 65
MaxLength: 15
Size: 166, 20
ButtoncmdLoginLocation: 168, 100
Text: "Login"

The code for frmMathTest gets the username and password from the user and checks it against the database records to ensure that the user's credentials are valid before allowing them to proceed further. Before we implement this code however, we have a few other things to do. First of all, we will create the program's code module and set up the global variables that will be used by the various forms, including frmMathTest.

  1. Click on the Project menu and select Add Module...
  2. Create a code module with the name "modMain"
  3. The module's code editor window will appear. Add the following code between the Module modMain and End Module statements:

'All of the program's global variables are declared here

'Database variables
Public con As New OleDb.OleDbConnection
Public ds As New DataSet
Public da As OleDb.OleDbDataAdapter
Public sql As String
Public cmd As OleDb.OleDbCommand

Public userName As String 'Variable to hold user login name

'Test result variables
Public scoreAdd As Integer
Public levelAdd As Integer
Public scoreSub As Integer
Public levelSub As Integer
Public scoreMul As Integer
Public levelMul As Integer
Public scoreDiv As Integer
Public levelDiv As Integer

'Test parameter variables
Public type As Integer = 0
Public level As Integer = 0
Public qNum As Integer = 0
Public testScore As Integer = 0
Public pass As Boolean = False

'Question parameter variables
Public op1(0 To 9) As Integer
Public op2(0 To 9) As Integer
Public optr(0 To 3) As Integer
Public ans(0 To 9) As Integer
Public userAns(0 To 9) As Integer
Public multiplier As Integer

'Operator character set, stored as an array
Public opChar() As Char = {"+", "-", "x", "÷"}

'Control array variables
Public RadioArrayType(0 To 3) As RadioButton
Public RadioArrayLevel(0 To 2) As RadioButton
Public LabelArrayQuestion(0 To 9) As Label
Public LabelArrayAnswer(0 To 9) As Label
Public PicArrayMark(0 To 9) As PictureBox

Sub resetTest()
  'Re-initialise all test variables to their initial values
  Type = 0
  level = 0
  qNum = 0
  multiplier = 0
  For i = 0 To 9
  op1(i) = 0
  op2(i) = 0
  ans(i) = 0
  userAns(i) = 0
  Next
End Sub

The purpose of each of the global variables is hopefully clear from the comments and the variable names used (we have largely ignored the usual Microsoft naming conventions on this occasion). The subroutine resetTest() is defined here because it is called from two different forms. Code modules are quite useful for storing global variables, and for defining subroutines that will be accessed by more than one form. Before writing any more code, it is probably a good idea to create the application's other forms, starting with the menu form.

  1. Click on the Project menu and select Add Windows Form...
  2. Name the form frmMenu
  3. Create an interface like the one illustrated below.

The MathTest menu form interface

The MathTest menu form interface


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

frmMenu Form Controls
ControlNameAdditional Properties
FormfrmMenuIcon: mathtest.ico
Size: 300, 360
Text: "MathTest - Main Menu"
PanelpnlSelectTestTypeLocation: 12, 10
Size: 260, 110
LabellblSelectTypeLocation: 15, 15
Text: "Select a test type:"
RadioButtonradAddLocation: 150, 15
Text: "Addition"
RadioButtonradSubLocation: 150, 37
Text: "Subtraction"
RadioButtonradMulLocation: 150, 59
Text: "Multiplication"
RadioButtonradDivLocation: 150, 81
Text: "Division"
PanelpnlSelectLevelLocation: 12, 130
Size: 260, 90
LabellblSelectLevelLocation: 15, 15
Text: "Select a difficulty level:"
RadioButtonradEasyLocation: 150, 15
Text: "Easy"
RadioButtonradModeLocation: 150, 37
Text: "Moderate"
RadioButtonradHardLocation: 150, 59
Text: "Hard"
LabellblResultsFont: Microsoft Sans Serif, 8.25pt, style=Underline
ForeColor: Blue
Location: 12, 240
Text: "View your previous scores"
ButtoncmdStartLocation: 197, 240
Text: "Start test"
ButtoncmdExitForeColor: Red
Location: 197, 287
Text: "Exit"

  1. Click on the Project menu and select Add Windows Form...
  2. Name the form frmTestPage
  3. Create an interface like the one illustrated below.

The MathTest test form interface

The MathTest test form interface


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

frmTestPage Form Controls
ControlNameAdditional Properties
FormfrmTestPageIcon: mathtest.ico
Size: 440, 160
Text: "MathTest - Test Page"
LabellblQNoAutoSize: False
Location: 15, 24
Size: 90, 22
Text: "Question No:"
TextAlign: MiddleLeft
LabellblOp1AutoSize: False
BorderStyle: FixedSingle
Location: 116, 25
Size: 30, 20
Text: None
TextAlign: MiddleCenter
LabellblOperatorAutoSize: False
Location: 157, 27
Size: 26, 16
Text: None
TextAlign: MiddleCenter
LabellblOp2AutoSize: False
BorderStyle: FixedSingle
Location: 194, 25
Size: 30, 20
Text: None
TextAlign: MiddleCenter
LabellblEqualsAutoSize: False
Location: 230, 25
Size: 26, 20
Text: "="
TextAlign: MiddleCenter
TextBoxtxtAnswerLocation: 262, 25
Size: 50, 20
TextAlign: Centre
ButtoncmdSubmitLocation: 323, 24
Size: 87, 23
Text: "Submit Answer"
ButtoncmdCancelForeColor: Red
Location: 323, 80
Size: 87, 23
Text: "Cancel Test"

  1. Click on the Project menu and select Add Windows Form...
  2. Name the form "frmTestResults"
  3. Create an interface like the one illustrated below.

The MathTest results form interface

The MathTest results form interface


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

frmTestResults Form Controls
ControlNameAdditional Properties
FormfrmTestResultsIcon: mathtest.ico
Size: 620, 520
Text: "MathTest - Results Page"
LabellblHeaderFont: Microsoft Sans Serif, 16pt, style=Bold
Location: 10, 10
Text: "Your Test Results:"
LabellblQ01AutoSize: False
Font: Consolas, 10pt
Location: 10, 58
Size: 360, 16
Text: None
TextAlign: MiddleLeft
LabellblQ02AutoSize: False
Font: Consolas, 10pt
Location: 10, 89
Size: 360, 16
Text: None
TextAlign: MiddleLeft
LabellblQ03AutoSize: False
Font: Consolas, 10pt
Location: 10, 120
Size: 360, 16
Text: None
TextAlign: MiddleLeft
LabellblQ04AutoSize: False
Font: Consolas, 10pt
Location: 10, 151
Size: 360, 16
Text: None
TextAlign: MiddleLeft
LabellblQ05AutoSize: False
Font: Consolas, 10pt
Location: 10, 182
Size: 360, 16
Text: None
TextAlign: MiddleLeft
LabellblQ06AutoSize: False
Font: Consolas, 10pt
Location: 10, 213
Size: 360, 16
Text: None
TextAlign: MiddleLeft
LabellblQ07AutoSize: False
Font: Consolas, 10pt
Location: 10, 244
Size: 360, 16
Text: None
TextAlign: MiddleLeft
LabellblQ08AutoSize: False
Font: Consolas, 10pt
Location: 10, 275
Size: 360, 16
Text: None
TextAlign: MiddleLeft
LabellblQ09AutoSize: False
Font: Consolas, 10pt
Location: 10, 306
Size: 360, 16
Text: None
TextAlign: MiddleLeft
LabellblQ10AutoSize: False
Font: Consolas, 10pt
Location: 10, 337
Size: 360, 16
Text: None
TextAlign: MiddleLeft
PictureBoxpicRes01Location: 389, 58
Size: 14, 14
PictureBoxpicRes02Location: 389, 89
Size: 14, 14
PictureBoxpicRes03Location: 389, 120
Size: 14, 14
PictureBoxpicRes04Location: 389, 151
Size: 14, 14
PictureBoxpicRes05Location: 389, 182
Size: 14, 14
PictureBoxpicRes06Location: 389, 215
Size: 14, 14
PictureBoxpicRes07Location: 389, 246
Size: 14, 14
PictureBoxpicRes08Location: 389, 277
Size: 14, 14
PictureBoxpicRes09Location: 389, 308
Size: 14, 14
PictureBoxpicRes10Location: 389, 339
Size: 14, 14
LabellblCAns01AutoSize: False
Font: Consolas, 10pt
Location: 423, 58
Size: 170, 16
Text: None
TextAlign: MiddleLeft
LabellblCAns02AutoSize: False
Font: Consolas, 10pt
Location: 423, 89
Size: 170, 16
Text: None
TextAlign: MiddleLeft
LabellblCAns03AutoSize: False
Font: Consolas, 10pt
Location: 423, 120
Size: 170, 16
Text: None
TextAlign: MiddleLeft
LabellblCAns04AutoSize: False
Font: Consolas, 10pt
Location: 423, 151
Size: 170, 16
Text: None
TextAlign: MiddleLeft
LabellblCAns05AutoSize: False
Font: Consolas, 10pt
Location: 423, 182
Size: 170, 16
Text: None
TextAlign: MiddleLeft
LabellblCAns06AutoSize: False
Font: Consolas, 10pt
Location: 423, 213
Size: 170, 16
Text: None
TextAlign: MiddleLeft
LabellblCAns07AutoSize: False
Font: Consolas, 10pt
Location: 423, 244
Size: 170, 16
Text: None
TextAlign: MiddleLeft
LabellblCAns08AutoSize: False
Font: Consolas, 10pt
Location: 423, 275
Size: 170, 16
Text: None
TextAlign: MiddleLeft
LabellblCAns09AutoSize: False
Font: Consolas, 10pt
Location: 423, 306
Size: 170, 16
Text: None
TextAlign: MiddleLeft
LabellblCAns10AutoSize: False
Font: Consolas, 10pt
Location: 423, 337
Size: 170, 16
Text: None
TextAlign: MiddleLeft
LabellblResultMessageAutoSize: False
Location: 10, 400
Size: 375, 18
Text: None
TextAlign: MiddleRight
PictureBoxpicSymbolLocation: 395, 389 Size: 40, 40
LabellblScoreAutoSize: False
Location: 450, 400
Size: 60, 18
Text: "Test score:"
TextAlign: MiddleRight
LabellblTestScoreAutoSize: False
BorderStyle: FixedSingle
Font: Microsoft Sans Serif, 10pt
Location: 514, 398
Size: 69, 23
Text: None
TextAlign: MiddleCenter
ButtoncmdResultsOKLocation: 514, 447
Size: 69, 23
Text: "OK"

  1. Click on the Project menu and select Add Windows Form...
  2. Name the form "frmPreviousScores"
  3. Create an interface like the one illustrated below.

The MathTest test status form interface

The MathTest test status form interface


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

frmPreviousScores Form Controls
ControlNameAdditional Properties
FormfrmPreviousScoresIcon: mathtest.ico
Size: 330, 295
Text: "MathTest - Test Status"
LabellblHeaderFont: Microsoft Sans Serif, 16pt, style=Bold
Location: 10, 10
Text: "Previous Test Results:"
LabellblTypeAutoSize: False
Font: Arial, 10pt, style=Bold
Location: 11, 57
Size: 120, 16
Text: "Test Type"
TextAlign: MiddleLeft
LabellblLevelAutoSize: False
Font: Arial, 10pt, style=Bold
Location: 137, 57
Size: 80, 16
Text: "Level"
TextAlign: MiddleCenter
LabellblScoreAutoSize: False
Font: Arial, 10pt, style=Bold
Location: 223, 57
Size: 80, 16
Text: "Score"
TextAlign: MiddleCenter
LabellblAddAutoSize: False
Font: Arial, 9pt
Location: 10, 88
Size: 120, 16
Text: "Addition"
TextAlign: MiddleLeft
LabellblSubAutoSize: False
Font: Arial, 9pt
Location: 10, 119
Size: 120, 16
Text: "Subtraction"
TextAlign: MiddleLeft
LabellblMulAutoSize: False
Font: Arial, 9pt
Location: 10, 150
Size: 120, 16
Text: "Multiplication"
TextAlign: MiddleLeft
LabellblDivAutoSize: False
Font: Arial, 9pt
Location: 10, 181
Size: 120, 16
Text: "Division"
TextAlign: MiddleLeft
LabellblAddLevelAutoSize: False
Font: Arial, 9pt
Location: 137, 88
Size: 80, 16
Text: None
TextAlign: MiddleCenter
LabellblSubLevelAutoSize: False
Font: Arial, 9pt
Location: 137, 119
Size: 80, 16
Text: None
TextAlign: MiddleCenter
LabellblMulLevelAutoSize: False
Font: Arial, 9pt
Location: 137, 150
Size: 80, 16
Text: None
TextAlign: MiddleCenter
LabellblDivLevelAutoSize: False
Font: Arial, 9pt
Location: 137, 181
Size: 80, 16
Text: None
TextAlign: MiddleCenter
LabellblAddScoreAutoSize: False
Font: Arial, 9pt
Location: 223, 88
Size: 80, 16
Text: None
TextAlign: MiddleCenter
LabellblSubScoreAutoSize: False
Font: Arial, 9pt
Location: 223, 119
Size: 80, 16
Text: None
TextAlign: MiddleCenter
LabellblMulScoreAutoSize: False
Font: Arial, 9pt
Location: 223, 150
Size: 80, 16
Text: None
TextAlign: MiddleCenter
LabellblDivScoreAutoSize: False
Font: Arial, 9pt
Location: 223, 181
Size: 80, 16
Text: None
TextAlign: MiddleCenter
ButtoncmdOKLocation: 120, 221
Text: "OK"

  1. Click on the Project menu and select Add Windows Form...
  2. Name the form "frmAdmin"
  3. Create an interface like the one illustrated below.

The MathTest admin form interface

The MathTest admin form interface


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

frmAdmin Form Controls
ControlNameAdditional Properties
FormfrmAdminIcon: mathtest.ico
Size: 248, 205
Text: "MathTest - Admin"
ButtoncmdAddUserLocation: 12, 12
Text: "Add User"
ButtoncmdDelUserLocation: 12, 41
Text: "Delete User"
ButtoncmdExitForeColor: Red
Location: 145, 132
Text: "Exit"

  1. We are now ready to add the code to the login form. Open the code editor window for the form frmMathTest and add the following code in the body of the form's class definition:

'This is the application's opening screen - it allows the user to log in
Private Sub cmdLogin_Click(sender As Object, e As EventArgs) _
    Handles cmdLogin.Click
  'Make sure the user has entered both a username and a password
  If txtUserName.Text = "" Then
    MsgBox("You have not entered a username.")
    txtUserName.Focus()
    Exit Sub
  ElseIf txtPassword.Text = "" Then
    MsgBox("You have not entered a password.")
    txtPassword.Focus()
    Exit Sub
  End If

  userName = txtUserName.Text 'Store the username for later use

  'Set up the database connection string and open the database
  con.ConnectionString = _
    "Provider=Microsoft.ACE.OLEDB.12.0; Data Source=mathtest.accdb"
  con.Open()
  'Create the sql command string
  Sql = "SELECT * FROM MathTest WHERE usrLoginName = '" & txtUserName.Text & _
    "' AND usrPassword = '" & txtPassword.Text & "'"
  da = New OleDb.OleDbDataAdapter(Sql, con) 'Create a data adapter object
  da.Fill(ds, "UserInfo") 'Populate the dataset
  con.Close() 'Close the database

  'Check that the user actually exists
  If ds.Tables("UserInfo").Rows.Count = 0 Then
    MsgBox("Invalid username or password.")
    Exit Sub
  ElseIf userName = "admin" Then
    'If the admin user has logged in, go to the admin screen
    frmAdmin.Show()
    Me.Hide()
  Else
    'Otherwise get the user's test scores and level attained in each type of test
    scoreAdd = ds.Tables("UserInfo").Rows(0).Item(3)
    levelAdd = ds.Tables("UserInfo").Rows(0).Item(4)
    scoreSub = ds.Tables("UserInfo").Rows(0).Item(5)
    levelSub = ds.Tables("UserInfo").Rows(0).Item(6)
    scoreMul = ds.Tables("UserInfo").Rows(0).Item(7)
    levelMul = ds.Tables("UserInfo").Rows(0).Item(8)
    scoreDiv = ds.Tables("UserInfo").Rows(0).Item(9)
    levelDiv = ds.Tables("UserInfo").Rows(0).Item(10)
    'Display the main menu screen and hide the login screen
    frmMenu.Show()
    Me.Hide()
  End If
End Sub

Private Sub cmdExit_Click(sender As Object, e As EventArgs)
  End 'Exit the program immediately
End Sub


  1. Before proceeding further, run the program and log in using the default user name ("user") and password ("pass") to check that everything is working OK (if so, you should see the main menu screen appear).
  2. We are now ready to add the code to the menu form. Open the code editor window for the form frmMenu and add the following code in the body of the form's class definition:

Private Sub frmMenu_Load(sender As Object, e As EventArgs) _
    Handles MyBase.Load
  'Make sure no menu items are selected when form first loads
  initMenuArrays() 'Set up the control arrays for the menu form
  For i = 0 To 3
    RadioArrayType(i).Checked = False
  Next
  For i = 0 To 2
    RadioArrayLevel(i).Checked = False
  Next
End Sub

Sub initLevels()
  'Enable choice of levels and clear any existing selection
  For i = 0 To 2
    RadioArrayLevel(i).Enabled = True
    RadioArrayLevel(i).Checked = False
  Next
End Sub

Private Sub radAdd_Click(sender As Object, e As EventArgs) _
    Handles radAdd.Click
  'This procedure executes if the user selects the addition test type
  initLevels() 'Reset levels to none selected
  If radAdd.Checked = True Then
    Select Case levelAdd
    'Make sure user cannot select a level lower than the highest level
    'they have previously attained, and set default selection
      Case 0
        RadioArrayLevel(0).Checked = True
      Case 1
        RadioArrayLevel(0).Checked = True
      Case 2
        RadioArrayLevel(0).Enabled = False
        RadioArrayLevel(1).Checked = True
      Case 3
        RadioArrayLevel(0).Enabled = False
        RadioArrayLevel(1).Enabled = False
        RadioArrayLevel(2).Checked = True
    End Select
  End If
End Sub

Private Sub radSub_Click(sender As Object, e As EventArgs) _
    Handles radSub.Click
  'This procedure executes if the user selects the subtraction test type
  initLevels() 'Reset levels to none selected
  If radSub.Checked = True Then
    Select Case levelSub
      'Make sure user cannot select a level lower than the highest level
      'they have previously attained, and set default selection
      Case 0
        RadioArrayLevel(0).Checked = True
      Case 1
        RadioArrayLevel(0).Checked = True
      Case 2
        RadioArrayLevel(0).Enabled = False
        RadioArrayLevel(1).Checked = True
      Case 3
        RadioArrayLevel(0).Enabled = False
        RadioArrayLevel(1).Enabled = False
        RadioArrayLevel(2).Checked = True
    End Select
  End If
End Sub

Private Sub radMul_Click(sender As Object, e As EventArgs) _
    Handles radMul.Click
  'This procedure executes if the user selects the multiplication test type
  initLevels() 'Reset levels to none selected
  If radMul.Checked = True Then
    Select Case levelMul
      'Make sure user cannot select a level lower than the highest level
      'they have previously attained, and set default selection
      Case 0
        RadioArrayLevel(0).Checked = True
      Case 1
        RadioArrayLevel(0).Checked = True
      Case 2
        RadioArrayLevel(0).Enabled = False
        RadioArrayLevel(1).Checked = True
      Case 3
        RadioArrayLevel(0).Enabled = False
        RadioArrayLevel(1).Enabled = False
        RadioArrayLevel(2).Checked = True
    End Select
  End If
End Sub

Private Sub radDiv_Click(sender As Object, e As EventArgs) _
    Handles radDiv.Click
  'This procedure executes if the user selects the division test type
  initLevels() 'Reset levels to none selected
  If radDiv.Checked = True Then
    Select Case levelDiv
      'Make sure user cannot select a level lower than the highest level
      'they have previously attained, and set default selection
      Case 0
        RadioArrayLevel(0).Checked = True
      Case 1
        RadioArrayLevel(0).Checked = True
      Case 2
        RadioArrayLevel(0).Enabled = False
        RadioArrayLevel(1).Checked = True
      Case 3
        RadioArrayLevel(0).Enabled = False
        RadioArrayLevel(1).Enabled = False
        RadioArrayLevel(2).Checked = True
    End Select
  End If
End Sub

Private Sub cmdStart_Click(sender As Object, e As EventArgs) _
    Handles cmdStart.Click
  type = 0 'Initialise test type to none
  level = 0 'Initialise level to zero
  For i = 0 To 3
    'Get user selection for type of test to be taken
    If RadioArrayType(i).Checked = True Then
      type = i + 1
    End If
  Next
  For i = 0 To 2
    'Get user selection for level of test to be taken
    If RadioArrayLevel(i).Checked = True Then
      level = i + 1
    End If
  Next
  If type = 0 Then
    'If the user has not selected a test type, prompt them
    MsgBox("You have not selected a test type.")
  Else
    'Otherwise, open the test page and close the menu page
    frmTestPage.Show()
    Me.Close()
  End If
End Sub

Private Sub lblResults_Click(sender As Object, e As EventArgs) _
    Handles lblResults.Click
  'If the user has previous test scores on record, display them
  If levelAdd = 0 And levelSub = 0 And levelMul = 0 And levelDiv = 0 Then
    MsgBox("You have no test scores currently on record.")
  Else
    frmPreviousScores.Show()
  End If
End Sub

Public Sub initMenuArrays()
  'Set up the control arrays for the menu form
  RadioArrayType(0) = radAdd
  RadioArrayType(1) = radSub
  RadioArrayType(2) = radMul
  RadioArrayType(3) = radDiv
  RadioArrayLevel(0) = radEasy
  RadioArrayLevel(1) = radMode
  RadioArrayLevel(2) = radHard
End Sub

Private Sub cmdExit_Click(sender As Object, e As EventArgs) _
    Handles cmdExit.Click
  End 'Exit the program immediately
End Sub

  1. The code for the test form needs to be added next. Open the code editor window for the form frmTestPage and add the following code in the body of the form's class definition:

Private Sub frmTestPage_Load(sender As Object, e As EventArgs) _
    Handles MyBase.Load
  'Generate the first question when the form loads
  generateQuestion()
End Sub

Private Sub cmdCancel_Click(sender As Object, e As EventArgs) _
    Handles cmdCancel.Click
  'If the user cancels the test, reset the test variables and close the form
  resetTest()
  frmMenu.Show()
  Me.Close()
End Sub

Private Sub cmdSubmit_Click(sender As Object, e As EventArgs) _
    Handles cmdSubmit.Click
  'This procedure executes when the user clicks on the Submit button
  If txtAnswer.Text = "" Then
    'Make sure the user has entered an answer
    MsgBox("You have not given an answer.")
    txtAnswer.Focus()
    Exit Sub
  End If
  userAns(qNum) = CInt(txtAnswer.Text) 'Convert the answer to an integer and save it
  qNum += 1 'Increment the question number
  If qNum > 9 Then
    'If ten questions have been answered, the test is complete
    MsgBox("You have completed the test!")
    frmTestResults.Show() 'Open the test result form
    Me.Close() 'Close the test form
  Else
    'If test is not complete, generate another question
    generateQuestion()
    txtAnswer.Focus()
  End If
End Sub

Private Sub txtAnswer_KeyPress _
    (sender As Object, e As Windows.Forms.KeyPressEventArgs) _
      Handles txtAnswer.KeyPress
  'This routine handles characters typed into the answer box and discards
  'all non-numeric keystrokes
  If(e.KeyChar < "0" Or e.KeyChar > "9") And e.KeyChar <> vbBack Then
    e.Handled = True
  End If
End Sub

Public Sub generateQuestion()
  Dim temp As Integer 'Variable to hold result of intermediate calculations

  Randomize() 'Create a new seed for generating random numbers
  Select Case type 'Determine what type of test has been selected
    Case 1 'Addition test
      Select Case level 'Determine which test level has been selected
        Case 1
          'Generate random numbers between 1 and 15
          op1(qNum) = Rnd() * 14 + 1
          op2(qNum) = Rnd() * 14 + 1
        Case 2
          'Generate random numbers between 10 and 25
          op1(qNum) = Rnd() * 15 + 10
          op2(qNum) = Rnd() * 15 + 10
        Case 3
          'Generate random numbers between 25 and 100
          op1(qNum) = Rnd() * 75 + 25
          op2(qNum) = Rnd() * 75 + 25
      End Select
      ans(qNum) = op1(qNum) + op2(qNum) 'Calculate correct answer
    Case 2 'Subtraction test
      Select Case level 'Determine which test level has been selected
        Case 1
          'Generate random numbers between 1 and 15
          op1(qNum) = Rnd() * 14 + 1
          op2(qNum) = Rnd() * 14 + 1
        Case 2
          'Generate random numbers between 10 and 25
          op1(qNum) = Rnd() * 15 + 10
          op2(qNum) = Rnd() * 15 + 10
        Case 3
          'Generate random numbers between 25 and 100
          op1(qNum) = Rnd() * 75 + 25
          op2(qNum) = Rnd() * 75 + 25
      End Select
      'If first operand is smaller than second operand, swap them
      'to ensure that result of subtraction will always be positive
      If op1(qNum) < op2(qNum) Then
        temp = op2(qNum)
        op2(qNum) = op1(qNum)
        op1(qNum) = temp
      End If
      ans(qNum) = op1(qNum) - op2(qNum) 'Calculate correct answer
    Case 3 'Multiplication test
      Select Case level 'Determine which test level has been selected
        Case 1
          'Generate random numbers between 2 and 12
          op1(qNum) = Rnd() * 10 + 2
          op2(qNum) = Rnd() * 10 + 2
        Case 2
          'Generate random numbers between 10 and 25
          op1(qNum) = Rnd() * 15 + 10
          op2(qNum) = Rnd() * 15 + 10
        Case 3
          'Generate random numbers between 25 and 50
          op1(qNum) = Rnd() * 25 + 25
          op2(qNum) = Rnd() * 25 + 25
      End Select
      ans(qNum) = op1(qNum) * op2(qNum) 'Calculate correct answer

    Case 4 'Division test
      Select Case level 'Determine which test level has been selected
        Case 1
          'Generate random divisor(number to divide by) between 2 and 6
          op2(qNum) = Rnd() * 4 + 2
          'Generate multiplier between 2 and 6
          multiplier = Rnd() * 4 + 2
          'Calculate dividend(number to be divided) as divisor x multiplier
          op1(qNum) = op2(qNum) * multiplier
        Case 2
          'Generate random divisor (number to divide by) between 7 and 12
          op2(qNum) = Rnd() * 5 + 7
          'Generate multiplier between 7 and 12
          multiplier = Rnd() * 5 + 7
          'Calculate dividend (number to be divided) as divisor x multiplier
          op1(qNum) = op2(qNum) * multiplier
        Case 3
          'Generate random divisor (number to divide by) between 13 and 25
          op2(qNum) = Rnd() * 12 + 13
          'Generate multiplier between 13 and 25
          multiplier = Rnd() * 12 + 13
          'Calculate dividend (number to be divided) as divisor x multiplier
          op1(qNum) = op2(qNum) * multiplier
      End Select
      ans(qNum) = op1(qNum) / op2(qNum) 'Calculate correct answer
  End Select

  'Display question on test page form
  txtAnswer.Text = ""
  lblOp1.Text = op1(qNum)
  lblOp2.Text = op2(qNum)
  lblOperator.Text = opChar(type - 1)
  lblQNo.Text = "Question " & qNum + 1
End Sub


  1. Before you can run a complete test and see the results, you need to add the code that displays the results of the test to the user. Open the code editor window for the form frmTestResults and add the following code in the body of the form's class definition:

Private Sub frmTestResults_Load(sender As Object, e As EventArgs) _
    Handles MyBase.Load
  'This routine compares the user's answers to the questions with the correct
  'answers, calculates the user's test score, and displays the results
  initResultArrays() 'Set up the control arrays for the results form
  testScore = 0 'Initialise test score to zero
  pass = False 'Initialise pass status to False

  For i = 0 To 9
    'Display each question and the user's answer
    LabelArrayQuestion(i).Text = _
      "Question" & CStr(i + 1).PadLeft(3) & CStr(op1(i)).PadLeft(10)
    LabelArrayQuestion(i).Text &= " " & opChar(type - 1) & _
      CStr(op2(i)).PadLeft(5) & " ="
    LabelArrayQuestion(i).Text &= CStr(userAns(i)).PadLeft(6)
    If ans(i) <> userAns(i) Then
      'If the user's answer is not correct, display a cross
      PicArrayMark(i).Image = Image.FromFile("cross.bmp")
    Else
      'If the user's answer is correct, display a tick
      PicArrayMark(i).Image = Image.FromFile("tick.bmp")
    End If
    'Display the correct answer for each question
    LabelArrayAnswer(i).Text = _
      " (Correct answer = " & CStr (ans(i)).PadLeft (3) & ") "
  Next

  For i = 0 To 9
    'Calculate the user's test score
    If userAns(i) = ans(i) Then
      testScore += 1
    End If
  Next

  lblTestScore.Text = testScore & "/10" 'Display user's test score

  Select Case level
    'Determine whether the user has achieved the pass mark for the test at the
    'level attempted and display an appropriate message based on the outcome
    Case 1
      If testScore < 10 Then
        lblResultMessage.Text = _
          "Sorry, you need to get all questions right to pass."
        picSymbol.Image = Image.FromFile("thumbs_dn.bmp")
      Else
        pass = True
        lblResultMessage.Text = "Well done, you got all the questions right!"
        picSymbol.Image = Image.FromFile("thumbs_up.bmp")
      End If
    Case 2
      If testScore < 9 Then
        lblResultMessage.Text = _
          "Sorry, you are only allowed one mistake at this level."
        picSymbol.Image = Image.FromFile("thumbs_dn.bmp")
      Else
        pass = True
        lblResultMessage.Text = "Well done, you got all the questions right!"
        picSymbol.Image = Image.FromFile("thumbs_up.bmp")
      End If
    Case 3
      If testScore < 8 Then
        lblResultMessage.Text = _
          "Sorry, you are only allowed two mistakes at this level."
        picSymbol.Image = Image.FromFile("thumbs_dn.bmp")
      Else
        pass = True
        lblResultMessage.Text = "Well done, you got all the questions right!"
        picSymbol.Image = Image.FromFile("thumbs_up.bmp")
      End If
  End Select

End Sub

Private Sub cmdResultsOK_Click(sender As Object, e As EventArgs) _
    Handles cmdResultsOK.Click
  Dim response As Integer _
    'Variable to hold value returned by message box (vbYesNoCancel)

  If pass = True Then _
    'Option to save test result is given if user has achieved pass mark
    'Offer user the choice of saving the test result
    response = MsgBox("Do you want to save your test result?" & vbNewLine & _
      " (this will overwrite any existing result for this test) ", vbYesNoCancel)
    If response = vbCancel Then
      Exit Sub 'If user clicks Cancel, do nothing
    ElseIf response = vbYes Then
      'If user clicks Yes, insert result into appropriate field in dataset
      Select Case type
        Case 1
          scoreAdd = testScore
          levelAdd = level
          ds.Tables("UserInfo").Rows(0).Item(3) = testScore
          ds.Tables("UserInfo").Rows(0).Item(4) = level
        Case 2
          scoreSub = testScore
          levelSub = level
          ds.Tables("UserInfo").Rows(0).Item(5) = testScore
          ds.Tables("UserInfo").Rows(0).Item(6) = level
        Case 3
          scoreMul = testScore
          levelMul = level
          ds.Tables("UserInfo").Rows(0).Item(7) = testScore
          ds.Tables("UserInfo").Rows(0).Item(8) = level
        Case 4
          scoreDiv = testScore
          levelDiv = level
          ds.Tables("UserInfo").Rows(0).Item(9) = testScore
          ds.Tables("UserInfo").Rows(0).Item(10) = level
      End Select
      Dim cb As New OleDb.OleDbCommandBuilder(da) _
        'Create a command builder object
      da.Update(ds, "UserInfo") 'Update the database
      MsgBox("Test result saved") _
        'Inform the user that the result has been saved
    End If
  End If
  'Reset the test variables, open the menu form and close the results form
  resetTest()
  frmMenu.Show()
  Me.Close()
End Sub

Public Sub initResultArrays()
  'Set up the control arrays for the results form
  LabelArrayQuestion(0) = lblQ01
  LabelArrayQuestion(1) = lblQ02
  LabelArrayQuestion(2) = lblQ03
  LabelArrayQuestion(3) = lblQ04
  LabelArrayQuestion(4) = lblQ05
  LabelArrayQuestion(5) = lblQ06
  LabelArrayQuestion(6) = lblQ07
  LabelArrayQuestion(7) = lblQ08
  LabelArrayQuestion(8) = lblQ09
  LabelArrayQuestion(9) = lblQ10

  LabelArrayAnswer(0) = lblCAns01
  LabelArrayAnswer(1) = lblCAns02
  LabelArrayAnswer(2) = lblCAns03
  LabelArrayAnswer(3) = lblCAns04
  LabelArrayAnswer(4) = lblCAns05
  LabelArrayAnswer(5) = lblCAns06
  LabelArrayAnswer(6) = lblCAns07
  LabelArrayAnswer(7) = lblCAns08
  LabelArrayAnswer(8) = lblCAns09
  LabelArrayAnswer(9) = lblCAns10

  PicArrayMark(0) = picRes01
  PicArrayMark(1) = picRes02
  PicArrayMark(2) = picRes03
  PicArrayMark(3) = picRes04
  PicArrayMark(4) = picRes05
  PicArrayMark(5) = picRes06
  PicArrayMark(6) = picRes07
  PicArrayMark(7) = picRes08
  PicArrayMark(8) = picRes09
  PicArrayMark(9) = picRes10
End Sub

  1. Before proceeding further, run the program and log in using the default user name and password, and take a test (or as many tests as you like) to check that everything is working OK so far.
  2. In order for the user to be able to see how they have performed on previous tests, we need to add some code to the test status form. Open the code editor window for the form frmPreviousScores and add the following code in the body of the form's class definition:

Private Sub frmPreviousScores_Load(sender As Object, e As EventArgs) _
    Handles MyBase.Load
  'Display user's score for each test taken, and at what level achieved
  If levelAdd > 0 Then
    lblAddLevel.Text = levelAdd
    lblAddScore.Text = scoreAdd
  Else
    lblAddLevel.Text = "N/A"
    lblAddScore.Text = "N/A"
  End If
  If levelSub > 0 Then
    lblSubLevel.Text = levelSub
    lblSubScore.Text = scoreSub
  Else
    lblSubLevel.Text = "N/A"
    lblSubScore.Text = "N/A"
  End If
  If levelMul > 0 Then
    lblMulLevel.Text = levelMul
    lblMulScore.Text = scoreMul
  Else
    lblMulLevel.Text = "N/A"
    lblMulScore.Text = "N/A"
  End If
  If levelDiv > 0 Then
    lblDivLevel.Text = levelDiv
    lblDivScore.Text = scoreDiv
  Else
    lblDivLevel.Text = "N/A"
    lblDivScore.Text = "N/A"
  End If
End Sub

Private Sub cmdOK_Click(sender As Object, e As EventArgs) _
    Handles cmdOK.Click
  Me.Close() 'Close the form
End Sub

  1. Run the program again, log in using the default user name and password, and click on the link "View your previous scores" on the application's main menu form to check that the above code is doing what it is supposed to do (you don't need to take any further tests to access this bit of the program). The screenshots below show the program in operation.

Log in using "user" and "pass"

Log in using "user" and "pass"



The current selection is Subtraction (Easy)

The current selection is Subtraction (Easy)



The questions at this level are relatively easy

The questions at this level are relatively easy



Even I can get ten out of ten!

Even I can get ten out of ten!



So far so good . . .

So far so good . . .


  1. The final piece of code is for the program's admin screen, and allows the admin user to add or delete users to or from the database. Open the code editor window for the form frmAdmin and add the following code in the body of the form's class definition:

Dim newUser As String 'New user login name
Dim newPass As String 'New user password
Dim delUser As String 'Name of user to be deleted
Dim result As Integer 'Result of attempt to execute database command

Private Sub cmdAddUser_Click(sender As Object, e As EventArgs) _
    Handles cmdAddUser.Click
  'This subroutine adds a new user to the database
  result = 0 'Initialise the value of result to zero
  'Get new user's details
  newUser = InputBox("Please enter a username: ")
  If newUser = "" Then Exit Sub
  newPass = InputBox("Please enter a password: ")
  If newPass = "" Then Exit Sub
  'Create the sql command string
  sql = "INSERT INTO MathTest(usrLoginName, usrPassword) VALUES('" & newUser & "', '" & newPass & "')"
  con.Open() 'Open the database
  cmd = New OleDb.OleDbCommand(sql, con) 'Create a command object
  On Error Resume Next 'Prevent the program from crashing by trapping system errors
  result = cmd.ExecuteNonQuery() 'Attempt to insert user details into the database
  cmd.Dispose() 'Dispose of the command object
  con.Close() 'Close the database
  If result = 1 Then
    MsgBox("User added.") 'The operation was successful
  Else
    MsgBox("Could not add user.") 'The program encountered an error
  End If
End Sub

Private Sub cmdDelUser_Click(sender As Object, e As EventArgs) _
    Handles cmdDelUser.Click
  'This subroutine deletes a user from the database, if they exist
  result = 0 'Initialise the value of result to zero
  'Get the name of the user to delete
  delUser = InputBox("Please enter a username: ")
  If delUser = "" Then Exit Sub
  If LCase(delUser) = "admin" Then
    'Prevent the admin user from deleting themself!
    MsgBox("You cannot delete the Admin user!")
    Exit Sub
  End If
  'Create the sql command string
  sql = "DELETE FROM MathTest WHERE usrLoginName = '" & delUser & "'"
  con.Open() 'Open the database
  cmd = New OleDb.OleDbCommand(sql, con) 'Create a command object
  On Error Resume Next 'Prevent the program from crashing by trapping system errors
  result = cmd.ExecuteNonQuery() 'Attempt to delete the user from the database
  cmd.Dispose() 'Dispose of the command object
  con.Close() 'Close the database
  If result = 1 Then
    MsgBox("User deleted.") 'The operation was successful
  Else
    MsgBox("User not found.") 'The program encountered an error
  End If
End Sub

Private Sub cmdExit_Click(sender As Object, e As EventArgs) _
    Handles cmdExit.Click
  End 'Exit the program immediately
End Sub

  1. Run the program again and test its functionality to ensure that everything is working, including the admin function. You will of course need to login as the admin user to do that - the username is "admin" and the password is "pass". Once you ar logged in as the admin user, add a couple of new users and delete them again to test the code.


General Notes

The application is intentionally somewhat unsophisticated in order to reduce the overall complexity of the interface design and the amount of code needed. The choice of numbers used for the different levels for each type of test is somewhat arbitrary, as are the pass marks chosen. A more sophisticated version might allow the administrative user to set up the range of numbers used for each level for each type of test, change the pass marks, or change the number of questions in a test.

It would also be feasible to change the program so that the user could determine how many questions they wanted to answer in a test, or for the admin user to set a time limit for tests at the various levels. There are in fact a great number of possibilities for extending the program's functionality and adding new features. I leave that to those of you with the inclination to do so.

The main purpose of this exercise is to demonstrate how a number of different forms can work together in an application. One point to note in this respect is that the application's main form (which is the login screen, frmMathTest) is never closed, only hidden. Otherwise, the application would close as soon as frmMathTest was closed. You could of course choose a different form to serve as the application's Startup form in the project's properties window.

The startup form can be hidden when not required, but should not be closed while the application is active. Other forms can be either hidden (which means they are still open but invisible to the user) or closed, depending on circumstances. If you want the form's on load event to run each time the form re-appears, it probably needs to be closed first.

Although the comments throughout the code go a long way towards explaining what is going on, there are one or two points that you should be aware of. Logging in to the program is relatively straightforward (the username and password either exist or they don't). Reading, adding or updating user test scores should also be non-problematic if the database is where it is supposed to be.

When it comes to the admin function however, problems can arise if the administrative user tries to insert a new user with a username that already exists (which would create a duplicate value in a field that does not accept duplicates), or tries to delete a user that does not exist (which would try and carry out a delete operation on a non-existent record). When either of these things occur, the database generates a system error and effectively halts (or if you prefer, crashes) the program.

There are various ways in which we could enhance the code in the admin form to ensure this does not cause a problem. To keep things simple, I have again chosen a somewhat simplistic approach by using the command "on error resume next" which effectively ignores the error and resumes program execution at the line of code immediately following the point where the error occurs.

The down side is that this is probably not the ideal way to handle such errors. The good thing is it works perfectly well in this instance, because all we really want to know for the moment is whether the insert or delete commands have been successful. Since these commands return an integer value of 1 if they are successful, we can test for this value in the code and display the appropriate user message. We will look at more sophisticated procedures for trapping and handling system errors elsewhere.

The other thing worth mentioning is the use of the randomize() function each time we generate a question. Maybe the best way to convince yourself that this is necessary is to comment out this command and run one of the tests twice at the same level, making a note of the questions. You should find (although I give no guarantee of this!) that the same (apparently random) numbers generated for the first test run are generated for the second test. This is because under the same set of conditions (i.e. providing nothing else has changed) the Rnd() function alone will keep coming up with the same numbers.

A computer, by its very nature, is incapable of generating a truly random number! What the Randomize() function does is to generate a seed based on the values generated by the system clock, which is then used by the Rnd() function to generate different numbers each time it is called. Although still not truly random, the numbers generated are always different because the system time is different each time the Randomize() function is called (in Windows systems, this is apparently expressed as the number of 100 nanosecond intervals that have elapsed since January 1601).