0% found this document useful (0 votes)
209 views

Project Codes

1. Visual Basic requires all executable code to be contained within procedures. Procedures can be subroutines or functions. Subroutines contain blocks of code that perform a task without returning a value, while functions return a value. 2. The document discusses creating subroutines in a separate module to organize code for reuse across applications. It provides an example of creating subroutines in a module to calculate the perimeter and area of a square using values entered in a form. 3. Functions are also discussed as procedures that return a value. The document amends the example form to illustrate functions by changing text boxes to input length and height instead of just side length.

Uploaded by

Stany Ganyani
Copyright
© © All Rights Reserved
Available Formats
Download as PDF, TXT or read online on Scribd
0% found this document useful (0 votes)
209 views

Project Codes

1. Visual Basic requires all executable code to be contained within procedures. Procedures can be subroutines or functions. Subroutines contain blocks of code that perform a task without returning a value, while functions return a value. 2. The document discusses creating subroutines in a separate module to organize code for reuse across applications. It provides an example of creating subroutines in a module to calculate the perimeter and area of a square using values entered in a form. 3. Functions are also discussed as procedures that return a value. The document amends the example form to illustrate functions by changing text boxes to input length and height instead of just side length.

Uploaded by

Stany Ganyani
Copyright
© © All Rights Reserved
Available Formats
Download as PDF, TXT or read online on Scribd
You are on page 1/ 296

Visual Basic Modules and Procedures

All executable statements in Visual Basic must belong to a procedure. A procedure is a block of code enclosed
between a Sub . . . or Function . . . statement and a matching End Sub or End Functionstatement. Once you
have written a procedure, it can be called by a program statement (sometimes referred to as the calling code)
somewhere else in your program. This is known as making a procedure call. When the procedure has finished
executing, it will (usually) return control to the procedure that called it (the calling procedure).

A procedure contains all of the code necessary to carry out a particular task. Once we have created the
procedure, we can call it every time we need to carry out that task. Procedures thus reduce the total amount of
code we need to write in order to implement a program. Procedures also allow us to construct our program
from a number of discrete building blocks, each of which contains only the code it needs to achieve its allotted
task and communicate with other parts of the program. Furthermore, because a procedure has such a specific
role, it is easy to determine whether the procedure's code is working as intended. Procedures thus facilitate the
testing and debugging of programs.

The applications we have created so far have all had a single form, and all of the code for the application has
been contained within the form's class definition. For relatively small-scale applications like the ones featured
in these pages, that's perfectly acceptable. For larger applications, which may require thousands or even tens
of thousands of lines of code in order to implement a program, the task of updating and maintaining the
program code can rapidly become unmanageable unless we can impose some kind of structure on it - which is
where modules come in.

A module is a file that contains a number of related procedures. Just as a procedure groups together the
program instructions required to carry out a particular task, a module groups together procedures that carry out
closely related tasks. For example, we could group together procedures that carry out file-handling tasks in one
module, and procedures dedicated to manipulating graphic images in various ways in another module.

It is often the case that procedures written for one application can be used in another application with little or
no modification. If these procedures are placed in a separate module, they can easily be imported into any
project that needs them. The ability to reuse code in this way represents the potential to significantly reduce
the amount of time and resources required to complete a project. In this page we will be taking a somewhat
closer look at procedures. Before we do that, let's create a Visual Basic module.

1. Create a new project called "Modules"

1
2. Change the Text property of the main form to "Geometry"
3. Change the name of the form to "frmGeometry"
4. Add controls to the form as shown below (we have used a form size of 300 x 240)

Your form should look like this screenshot

5. Change the names of the controls as specified in the table below

Form Controls

Control Name

Form frmGeometry
TextBox 1 txtSide
TextBox 2 txtPerimeter
TextBox 3 txtArea
Button 1 cmdPerimeter
Button 2 cmdArea
Button 3 cmdExit

The form's code module is created when the form is created, and is where the form's code will usually be
found. On this occasion, however, we are going to create a separate code module to hold re-usable
procedures. To create a module:

6. Click on Project ► Add Module . . . . You will see the following dialogue box:
2
The Add New Item ► Modules dialogue box

7. Change the default filename to "modGeometry.vb", and click Add. The code editor window will open
and display the new module's code window. Any code you create here will be available to other parts of
your project, and the module itself will appear in the Solution Explorer window.

Subroutines

The procedures we use fall into two general categories: those that are provided by the Visual Basic
programming language, and those we create ourselves. Beyond this distinction, however, there are two quite
different kinds of Visual Basic procedure - subroutines and functions. So far, we have not really discussed
the differences between these different kinds of procedure in any detail.

We'll talk about subroutines first, because the procedures we have created in our example programs - up to
now - have all been subroutines. A subroutine is created by typing the Sub keyword, followed by a procedure
name (which should be chosen to reflect the purpose of the subroutine), followed by parentheses. To end the
subroutine, we type the keywords End Sub. The general syntax of a subroutine declaration is shown below,
and should be familiar to you.
3
Sub ProcedureName()
.
.
.
End Sub

A widely used convention for naming subroutines (and procedures in general) is to start the name of the
subroutine with a capital letter. If a combination of words is used, each new word should begin with a capital
letter, e.g. CalculatePrice, or PrintRecord. The code that is placed between the Sub and End Substatements
is called the body of the subroutine, and defines what the subroutine actually does. The variables required by a
subroutine - there can be any number of these - can be declared within the body of the subroutine as follows:

Sub ProcedureName()
Dim strFirstName As String
strFirstName = "Fred"
End Sub

The actions performed by the subroutine will depend on the program statements that make up the body of the
subroutine. The above procedure simply declares a string variable called strFirstName, and assigns it the
value "Fred". Let's create a couple of subroutines for our Geometry application. We'll place these subroutines
in our newly created modGeometry module.

1. In the module's code editor window (between Module modGeometry and End Module), enter the
following code:

Sub SquarePerimeter()
Dim dblSide As Double
Dim dblPerimeter As Double
dblSide = frmGeometry.txtSide.Text
dblPerimeter = dblSide * 4
frmGeometry.txtPerimeter.Text = dblPerimeter
End Sub

Sub SquareArea()
Dim dblSide As Double
Dim dblArea As Double
dblSide = frmGeometry.txtSide.Text

4
dblArea = dblSide * dblSide
frmGeometry.txtArea.Text = dblArea
End Sub

When we use a procedure, we are said to be calling the procedure. To call a subroutine, we type its name in
the section of code where it is to be used. We are now going to call the subroutines defined in
the modGeometry module from the application's form:

2. Open the form designer window and double-click the Calculate Perimeter button. The code editor
window for the form will open and you will see that the following code has been created:

Private Sub cmdPerimeter_Click(sender As Object, e As EventArgs) _


Handles cmdPerimeter.Click

End Sub

3. In the body of the subroutine, type "SquarePerimeter()"


4. Now go back to the form designer window and double-click the Calculate Area button. Within the body
of its subroutine, type "SquareArea()".
5. Go back to the form designer window once more and double-click the Exit button. Within the body of its
subroutine type "End".

The code in your code editor window should now look like this:

Your form's code should look like this

5
6. Run and test the application to check that it works (you will need to type a value into the box labelled
"Side" before clicking on the Calculate Perimeter or Calculate Area buttons). You should see
something like the following:

Output from the Geometry program

Functions

Functions are procedures, just like subroutines. Unlike a subroutine, however, a function sends a value back to
the routine that calls it (it is said to return a value). When creating a function, a slightly different syntax is used,
as we shall see. To illustrate the use of functions, we need to modify our form somewhat, as follows (note that
we have changed the size of the form to 180 × 340 pixels):

6
The amended frmGeometry form

1. Name the form controls as follows (some controls stay as they were):

Control

Control Name

Form frmGeometry
TextBox 1 txtLength
TextBox 2 txtHeight
TextBox 3 txtPerimeter
TextBox 4 txtArea
Button 1 cmdCalculate
Button 3 cmdExit

We create a function by typing the Function keyword, followed by a procedure name (which should be chosen
to reflect the purpose of the function), followed by parentheses. Because the function (unlike a subroutine)
returns a value, you should specify the data type of the value the function will return by typing the As keyword
to the right of the closing parenthesis, followed by the datatype of the value to be returned. To end the function,
type the keywords End Function. The general syntax of a function declaration is shown below.

Function ProcedureName() As DataType


.
.
.
End Function

The naming conventions used for fuctions are the same as those used for subroutines. As with subroutines,
the code that is placed between the Function and End Function statements is called the body of the function,
and defines what the function actually does. The variables required by the function - again, there can be any
number of these - can be declared within the body of the function as follows:

Function GreetAlien() As String


Dim strGreeting As String
strGreeting = "Hello and welcome to Earth!"

7
GreetAlien = strGreeting
End Function

The actions performed by the procedure will depend on the program statements that make up the body of the
function. The above function simply declares a string variable called strGreeting and assigns it the value
"Hello and welcome to Earth!". The function returns a value with the datatype String. The value actually
returned by a function is determined by typing the name of the function, followed by an equals sign(=), followed
by the name of the variable that holds the value to be returned. In the above example, the function returns the
string value "Hello and welcome to Earth!".

Let's add some functions to our modGeometry module.

2. Open the module's code editor window, delete the existing subroutines, and enter the following code:

Function RectPerimeter() As Double


Dim dblLength As Double
Dim dblHeight As Double
dblLength = CDbl(frmGeometry.txtLength.Text)
dblHeight = CDbl(frmGeometry.txtHeight.Text)
RectPerimeter = (dblLength + dblHeight) * 2
End Function

Function RectArea() As Double


Dim dblLength As Double
Dim dblHeight As Double
dblLength = CDbl(frmGeometry.txtLength.Text)
dblHeight = CDbl(frmGeometry.txtHeight.Text)
RectArea = dblLength * dblHeight
End Function

A function is called in much the same way as a subroutine, by typing its name in the section of code where it is
to be used. The main difference is that the value returned by the function is assigned to a variable. In the
following example, the function GreetAlien() is called when the form loads:

Private Sub Form_Load()


Dim strCaption As String

8
strCaption = GreetAlien()
End Sub

The primary purpose of a function is to return a value. In the above example, the return value of
the GreetAlien() function is assigned to the variable strCaption when the form loads.

Let's call the functions we have added to the modGeometry module:

3. Open the form designer window and double-click the Calculate button. The code editor window for the
form will open and you will see that the following code has been created:

Private Sub cmdCalculate_Click(sender As Object, e As EventArgs) _


Handles cmdCalculate.Click

End Sub

4. In the body of this procedure, enter the code shown below:

txtPerimeter.Text = RectPerimeter()
txtArea.Text = RectArea()

5. Run and test the application to check that it works (you will need to type values into
the Length and Height boxes before clicking on the Calculate button). You should see something like
the following:

Output from the new version of the Geometry program

9
Arguments and parameters

In the subroutines and functions examined above, calculations are made using the values entered into various
text boxes on our form by the user. Although these procedures work perfectly well, they are explicitly coded to
work with the values of these particular external variables, and therefore cannot be used in any other context.
In order to make the procedures more generic - and thus reusable - we need to rewrite them to accept values
from different sources. External values can be passed in to a procedure for processing, rather than being hard-
wired into the procedure itself. The external values accepted by a procedure for processing are
called arguments.

In Visual Basic, a list of the arguments that will be accepted by a procedure will appear within parentheses
following the procedure name in a Sub . . . or Function . . . statement. The general syntax used for a
subroutine that takes one or more arguments is as follows:

Sub ProcedureName(Argument[,Argument . . .])


.
.
.
End Sub

And here is the construction used for a function:

Function ProcedureName(Argument[,Argument . . .]) As DataType


.
.
.
End Function

When we write a procedure that can accept arguments, we need to tell the procedure how many arguments it
will receive and specify the datatype for each argument. We also need to name each argument so that the
code within our procedure can differentiate between the arguments passed to the procedure. We achieve this
by declaring the name and datatype of each argument within the parentheses that follow the Sub
ProcedureName or Function ProcedureName statement as a comma-separated list.

10
It is worth pausing here to clarify what we mean by the terms "argument" and "parameter" - since both terms
are used in the heading for this section, and so far we have not mentioned parameters at all! Although the two
terms are often used interchangeably, they do in fact have subtly different meanings.

The term argument refers to the actual value passed to a procedure. The term parameter (or formal parameter)
refers to the declaration of the argument's name and datatype within parentheses in the Sub . . . or Function .
. . statement that begins the procedure declaration. The list of arguments within parentheses is called a formal
parameter list.

A procedure can accept any number of arguments, and each argument can be of a different type. The example
below declares a subroutine called CalculatePrice that takes a double-precision floating point variable
called dblItemPrice and an integer variable called intQty as its arguments.

Sub CalculatePrice(dblItemPrice As Double, intQty as Integer)


Double dblPrice = intQty * dblItemPrice
.
.
.
End Sub

We could also write a function that takes the same arguments as the subroutine, but assigns the result of the
calculation to its return value (a double-precision floating point value):

Function CalculatePrice(dblItemPrice As Double, intQty as Integer) As Double


Double dblPrice = intQty * dblItemPrice
CalculatePrice = dblPrice
End Function

Procedures can use the arguments passed to them in exactly the same way they use locally declared
variables. To demonstrate the use of arguments in procedures, modify
the RectPerimeter() and RectArea()functions in your code module as follows:

Function RectPerimeter(dblLength As Double, dblHeight As Double)


RectPerimeter = (dblLength + dblHeight) * 2
End Function

11
Function RectArea(dblLength As Double, dblHeight As Double)
RectArea = dblLength * dblHeight
End Function

To call a procedure that takes one or more arguments, we type its name, followed by comma separated list of
arguments within parentheses. The number of arguments should match the number of parameters specified in
the procedure's formal parameter list. The arguments must be provided in the same order in which they appear
in formal parameter list, and have the specified datatype.

In order for our application to work, we need to revise the code for the Calculate button as follows:

Private Sub cmdCalculate_Click(sender As Object, e As EventArgs) _


Handles cmdCalculate.Click
Dim dblInputLength, dblInputHeight As Double
dblInputLength = CDbl(txtLength.Text)
dblInputHeight = CDbl(txtHeight.Text)
txtPerimeter.Text = RectPerimeter(dblInputLength, dblInputHeight)
txtArea.Text = RectArea(dblInputLength, dblInputHeight)
End Sub

Once you have made the necessary changes, run the program again to test it.

Passing by reference

Each argument passed to a procedure usually consists of the current value of some variable. We call
this passing by value, and it effectively makes a copy of the variable available to the procedure - the value of
the original variable is not changed by anything the procedure does. Sometimes, however, we actually wanta
procedure to change the value of a variable. We can achieve this through a technique called passing by
reference.

When we pass by reference, the argument passed to a procedure is the address in memory of a variable
rather than its value. Any change the procedure makes to the value of the argument is applied to the original
variable, not a copy. In order to pass a variable to a procedure by reference, we use the ByRefkeyword before

12
the name of the argument within the parentheses of the function declaration. To demonstrate this technique,
make the following changes to the RectArea() function:

Public Function RectArea _


(dblLength As Double, dblHeight As Double, ByRef dblArea As Double)
RectArea = dblLength * dblHeight
dblArea = RectArea
End Function

Now make the following changes to the code for the Calculate button:

Private Sub cmdCalculate_Click(sender As Object, e As EventArgs) _


Handles cmdCalculate.Click
Dim dblInputLength, dblInputHeight, dblArea As Double
dblArea = 0
dblInputLength = CDbl(txtLength.Text)
dblInputHeight = CDbl(txtHeight.Text)
txtPerimeter.Text = RectPerimeter(dblInputLength, dblInputHeight)
RectArea(dblInputLength, dblInputHeight, dblArea)
txtArea.Text = dblArea
End Sub

Note that the changes we have made mean that the value of dblArea is passed to the RectArea() function by
reference, which means that the value of dblArea will be changed by this function call. Note also that by
default, even though we do not use the ByVal keyword in front of the formal parameters dblLength As
Double and dblHeight As Double for the RectArea() function, these variables will be passed to the
function by value. Variables are only passed to a function by reference if the ByRef keyword is used.

Database Coding with Visual Basic

13
Using the same database (Contacts.accdb) as for the page "An Address Book Database", we will create a
database application using Visual Basic program code rather than wizards. The first thing we need to do is to
create a connection object to enable us to connect to the database. The type of connection object used
depends on the type of database involved. For an Access database, this will be an OLE DB (Object Linking
and Embedding Database) connection object called Jet.

The connection string identifies the version of Jet to be used, which will depend on the version of Microsoft
Access used to create the database. The database we are using was created with Access 2007, so the version
used is ACE 12.0. For a database created with MSAccess 2010, the version to use would be ACE 14.0.

Note two more things here: first of all, the connection string will normally need to specify the precise path to the
database. You need to bear this in mind when creating database applications for distribution. Secondly, the
database needs to be in a trusted location on your computer, or on whatever computer it resides on. If this is
not the case, an attempt to add or update records may cause problems, as you will need write permission for
the target directory.

1. Create a new project with a different name (e.g. "AddressBook02").


2. Save your project immediately to create the project folder.
3. To simplify your code, copy the Contacts.accdb database into the \bin\Debug subdirectory of your
project folder.
4. Add a button to the application's main form and name it btnLoad.
5. Double click on the button to open up the code editor window, and insert the following line of code:

Dim con As New OleDb.OleDbConnection

The variable con now holds a reference to the connection object. The ConnectionString property of the
connection object should specify both the technology to be used to connect to the database (the
database provider), and the location of the database file (the data source). If the database were password
protected, it would also need to include a valid username and password.

6. Add the following line of code to your button's Click event handler:

con.ConnectionString = _
"Provider=Microsoft.ACE.OLEDB.12.0; Data Source=Contacts.accdb; _
Persist Security Info=False;"

Your code now should look like the illustration below.


14
This code creates a connection object and a connection string

To open the database, we use the Open() method of the connection object. After we have finished working
with the database, it should be closed using the Close() method.

7. Add the following code to your button's Click event handler:

con.Open()
MsgBox("A connection to the database is now open.")
con.Close()
MsgBox("The connection to the database is now closed.")

Your coding window should now look like the illustration below. You can run the program now to check that it
can open and close the database. Once the program is running, click on the button you created; you should
see a message that tells you that the connection to the database is open. Close this message box, and you
should see another message box telling you that the connection is closed (if you get an error message, make
sure that your database is in the location you specified).

The application can now open and close the database connection

15
ADO.NET uses an object called a DataSet to hold information read from the database (alternatively, an object
called a DataTable is available if you just want to read information, as opposed to writing new or modified
information to the database). Another object, called a Data Adapter, is used to communicate between the
connection object and the dataset. We will need to create additional variables to hold references to the data
adapter and dataset objects. We will also need to create string variables to hold the commands we will be
sending to the database. These will be written in Structured Query Language(SQL), which is the common
language that all databases understand.

8. Add the following lines of code at the points shown in the illustration below:

Dim ds As New DataSet


Dim da As OleDb.OleDbDataAdapter
Dim sql As String
.
.
.
sql = "SELECT * FROM Contact"
da = New OleDb.OleDbDataAdapter(sql, con)

Your code window should now look like the illustration.

The application now has a data adapter, a dataset and an SQL command

16
The data adapter can fill a dataset with records from a table using its Fill() method, which takes two
parameters. The first parameter will be the name of the variable that holds the reference to the dataset object
(in this case ds). The second is a name that will be used to identify this particular data adapter fill, and can be
anything you like (but it should be meaningful).

9. Add the following line of code, immediately following the line that creates the new data adapter object:

da.Fill (ds, "Contacts")

This code fills the dataset ds with data from the Contact table in the database. The only problem is that the
data itself cannot be seen by the user. The next thing we need to do, therefore, is to display the data in an
appropriate format. To do this:

10. Switch to the form design window, add two text boxes to the form, and name
them txtFirstNameand txtLastName
11. Switch back to the code editor, and add the following lines of code after the line of code that closes the
database connection:

txtFirstName.Text = ds.Tables("Contacts").Rows(0).Item(1).ToString()
txtLastName.Text = ds.Tables("Contacts").Rows(0).Item(2).ToString()

The first line of code assigns the contents of row 0, column 1 of the table to the Text property of txtFirstName.
The second line of code assigns the contents of row 0, column 2 of the table to the Textproperty
of txtLastName. The ToString() function is called here because any field that does not contain data will return
a Null value, which cannot be displayed in a text box - the function replaces the Null value with an empty string
("").

Remember that rows and columns are indexed from zero, and that the first column in the table is
the ContactID field, which we are not currently interested in. Row 0 thus points at the first record in the
table, column 1 is the FirstName field, and column 2 is the LastName field (you could also use the field name
to reference the required column, rather than the column number).

The code editor window should now look like the illustration below.

17
The code editor window should now look like this

If you run the program and click on the button (just click on OK when the connection status messages are
displayed, to get rid of the message boxes), you should see the name "Chris Wells" displayed in the two text
boxes.

The application can now display some data from a database table

18
In order to see a different record, we need to use a different row number. In fact, we can scroll through the
entire table by incrementing the row number to view each record in turn. Proceed as follows:

12. Switch to the form design window, delete the button you created earlier, and switch back to the code
editing window.
13. Delete all of the code between "Public Class Form1" and "End Class"
14. Enter the following code:

Dim con As New OleDb.OleDbConnection


Dim ds As New DataSet
Dim da As OleDb.OleDbDataAdapter
Dim sql As String

15. Switch to the form design window again, and double-click anywhere on the main form. Visual Basic will
create an empty Form1_Load procedure for you.
16. Within the new procedure, enter the following code:

con.ConnectionString = "Provider=Microsoft.ACE.OLEDB.12.0; Data


Source=Contacts.accdb; Persist Security Info=False;"
con.Open()
sql = "SELECT * FROM Contact"
da = New OleDb.OleDbDataAdapter(sql, con)
da.Fill(ds, "Contacts")
con.Close()

17. Add four buttons to your form as follows:

Form Controls

Button Name Button Text

btnNext Next Record


btnPrevious Previous Record
btnFirst First Record
btnLast Last Record

19
Your form should look something like the illustration below.

The form now has navigation buttons

18. Add the following code to the Form1 declarations:

Dim maxRows As Integer


Dim inc As Integer

19. Add the following code to the Form1_Load event procedure:

maxRows = ds.Tables("Contacts").Rows.Count
inc = 0
NavigateRecords()

The two integer variables (maxRows and inc) are used to store the total number of records in the data set
(using the Count() method of the Rows property), and to keep track of the current record number when
navigating backwards and forwards through table (using the variable inc, which is initialised to 0).

20. Add the following procedure underneath the Form1_Load procedure:

Private Sub NavigateRecords()


txtFirstName.Text = ds.Tables("Contacts").Rows(inc).Item(1).ToString()
txtLastName.Text = ds.Tables("Contacts").Rows(inc).Item(2).ToString()
End Sub

20
The code so far should look like the illustration below.

Your code should like like this

21. Switch to the form design window again and double-click the Next Record button. Visual Basic will
create an empty btnNext_Click() procedure for you.
22. Add the following code to the btnNext_Click() procedure.

If inc <> maxRows - 1 Then


inc = inc + 1
NavigateRecords()
Else
MsgBox("No More Rows")
End If

If we have not reached the last record in the table, the code will increment the inc variable, and then
call NavigateRecords(). Otherwise, it will display a message telling us there are no more rows.
The NavigateRecords() procedure displays whatever record is at the row currently pointed to by inc. We will
now add the code for the Previous Record button (switch to the form design window again, and double-click
the Previous Record button. Visual Basic will create an empty procedure for you, as before).

21
23. Add the following code to the btnPrevious_Click() procedure:

If inc > 0 Then


inc = inc - 1
NavigateRecords()
Else
MsgBox("First Record")
End If

If we have not reached the first record in the table, this code will decrement the inc variable, and then
call NavigateRecords(). Otherwise, it will display a message telling us that we are already at the first record.
As before, the NavigateRecords() procedure displays whatever record is at the row currently pointed to
by inc.

24. Create a btnFirst_Click() procedure, and add the following code:

inc = 0
NavigateRecords()

25. Create a btnLast_Click() procedure, and add the following code:

inc = maxRows - 1
NavigateRecords()

Run the programme to test the navigation buttons. You should be able to navigate backwards and forwards
through the records in the table, or jump to the first or last records. The next step will be to add controls to
allow us to add, delete or update records. Bear in mind throughout this exercise that once the data has been
read from the database into the dataset object, the connection with the database is closed. Any changes you
make to the data, therefore, will not be written to the database unless, and until, the connection is re-opened,
and the changes are committed to the database.

26. Add a further five buttons to your form as follows:

Form Controls

Button Name Button Text

btnAdd Add Record


btnCommit Commit Changes
btnUpdate Update Record

22
Form Controls

Button Name Button Text

btnDelete Delete Record


btnClear Clear/Cancel

Your form should now look something like the illustration below (use the formatting facilities to tidy the form up
if necessary).

The form with its additional controls

27. Add the following code to the Update Record button:

ds.Tables ("Contacts").Rows(inc).Item(1) = txtFirstName.Text.ToString()


ds.Tables ("Contacts").Rows(inc).Item(2) = txtLastName.Text.ToString()
MsgBox ("Record updated")

To test that the code is working:

28. Run the application and go to the first record.


29. Change the first and last names inside the text boxes to something different, and click on the Update
Record button.

23
30. Move to the next record (by clicking on the Next Record button), and then move back to the first record
(by clicking on either the Previous Record button or the First Record button). You should see that the
changes you made to the first record have been preserved.
31. Now close the application, run it again, and go to the first record. You should see the original name
information appear, rather than the amended version you so recently entered.

As stated earlier, any changes to the dataset object are not written to the database itself (committed) unless
you explicitly instruct the application to carry out this step.

32. Edit the code for the Update Record button to read as follows:

Dim cb As New OleDb.OleDbCommandBuilder(da)


ds.Tables ("Contacts").Rows(inc).Item(1) = txtFirstName.Text.ToString()
ds.Tables ("Contacts").Rows(inc).Item(2) = txtLastName.Text.ToString()
da.Update (ds, "Contacts")
MsgBox ("Record updated")

The first line of this amended code (the first addition to the procedure) creates an object called a command
builder, the purpose of which is to build an SQL command string for you. The only parameter it requires is the
name of a variable that holds a reference to a data adapter (in this case da). A reference to the command
builder itself is stored in another variable called cb.

The fourth line of code (which is the other addition to the procedure) uses the data adapter's Update()method
to send the changes we have made in the dataset (ds) to the database itself. The parameters passed to
the update() method are the name of a variable that holds a reference to a dataset (in this case ds), and the
meaningful name that we gave to the dataset (in this case "Contacts"). This second parameter is optional, but
may make the code more readable.

You can test the code to ensure that it functions correctly by running the program again, making some changes
to the first and last names in the first record, and closing the application down again. When you re-open the
application, you should find that the changes you made have been saved.

There is one minor problem, which occurs if you click on the Update Record button before the first record has
been displayed in the form (try this for yourself to see what happens). As an exercise, think about how to the
program's code could be amended to prevent the possibility of this occurring.

We will now look at the code needed to add a new record.

24
32. Add the following code to the Add Record button:

btnAdd.Enabled = False
btnUpdate.Enabled = False
btnDelete.Enabled = False
btnCommit.Enabled = True
txtFirstName.Clear()
txtLastName.Clear()
inc = maxRows
maxRows = maxRows + 1

This code switches off (disables) the Add Record button, together with the Update Record button and Delete
Record button. At the same time, it switches on (enables) the Commit Changes button. It also clears the
textboxes to enable the user to enter the details of the new record, and increments the
variable maxRows (since we will be adding a record to the database).

The variable inc is set to the old value of maxRows (if we subsequently click on the Clear/Cancel button, we
will check for a difference between these two values to determine whether or not we are cancelling the addition
of a record to the database). Note that the Commit Changes button is actually enabled by default when the
program starts, although it would obviously be better if it were disabled until needed. With this in mind, do the
following:

33. Switch to the form design window and click on the Commit Changes button.
34. Find the button's Enabled property in the Properties list.
35. Set this property to False.

Once you have entered the details for the new record, you will want to save it (commit it) to the database. Just
supposing you change your mind before doing so, however, you should be able to cancel the operation.

36. Switch to the form design window, double-click on the Clear/Cancel button, and add the following code
to the event procedure that has been created for you:

btnCommit.Enabled = False
btnAdd.Enabled = True
btnUpdate.Enabled = True
btnDelete.Enabled = True
If maxRows > inc Then maxRows = inc

25
inc = 0
NavigateRecords()

This code switches the Commit Changes button off and the other three buttons back on, after which the first
record is once again displayed (essentially, by setting the value of inc to 0, and then calling
the NavigateRecords() procedure). Before resetting inc, the code sets the value of maxRows back to its
previous value (the current value of inc), since we will not now be adding a record to the database. Assuming
we do not change our minds, however, we will use the Commit Changes button to save the new record to the
database.

37. Switch to the form design window, double-click on the Commit Changes button, and add the following
code to the event procedure that has been created for you:

If inc <> -1 Then


Dim cb As New OleDb.OleDbCommandBuilder(da)
Dim dsNewRow As DataRow
dsNewRow = ds.Tables("Contacts").NewRow()
dsNewRow.Item("FirstName") = txtFirstName.Text
dsNewRow.Item("LastName") = txtLastName.Text
ds.Tables("Contacts").Rows.Add(dsNewRow)
da.Update(ds, "Contacts")
MsgBox("New Record added to the database")
btnCommit.Enabled = False
btnAdd.Enabled = True
btnUpdate.Enabled = True
btnDelete.Enabled = True
End If

An If . . . statement is used to allow us to check whether or not there is actually a valid record to add. If
there is (i.e. inc is not equal to -1), the next two lines of code set up a command builder, and create
a DataRow object variable called dsNewRow. The next line of code actually creates the new record.

The next two lines set the values of FirstName and LastName in the new record to the values stored
in txtFirstName.Text and txtLastName.Text respectively. The next two lines after that add the new record to
the dataset and update the database.

26
The remaining code simply displays a message that informs the user that the new record has been added to
the database, and restores the buttons on the form to the state they were in before the user clicked on the Add
Record button.

You can test the Add Record button by running the program and trying it out. You may note certain
circumstances under which the program generates an error when you click on the Commit Changesbutton. As
an exercise, try to identify what circumstances cause an error, and try and amend your code to eliminate any
problems.

We will now look at the code needed to delete a record.

38. Add the following code to the Delete Record button:

Dim cb As New OleDb.OleDbCommandBuilder(da)


ds.Tables("Contacts").Rows(inc).Delete()
maxRows = maxRows - 1
inc = 0
NavigateRecords()
da.Update (ds, "Contacts")
NavigateRecords()

Most of the code here should by now be fairly self-explanatory. The only new element is the Delete()method,
the purpose of which is to remove the specified row from the dataset. The inc variable points to the currently
selected row, and this is the row that will be deleted from the dataset. The data adapter's Update() method is
again called upon to send the changes to the database itself.

The maxRows variable is decremented by one to reflect the fact that we now have one less record in the
dataset. The code then displays the first record once more by setting inc to zero, and calling
the NavigateRecords() subroutine.

Test the code by running the application again and deleting a record. As with the previous code we have
written, you will probably find that under certain circumstances, use of the Delete Record button causes a
problem. As an exercise, see if you can amend the code to eliminate any potential problems.

39. You may also wish to prompt the user for confirmation before the record is deleted. To achieve this, add
the following code to the Delete Record button, before the code already in there:

27
If MessageBox.Show ("Are you sure?", "Delete", _
MessageBoxButtons.YesNo, MessageBoxIcon.Warning) = DialogResult.No Then
MsgBox ("Operation Cancelled")
Exit Sub
End If

Without going into too much detail, the message box displays two buttons (Yes and No). If the user clicks on
the Yes button, the code that follows the If . . . statement will be executed (i.e. the record will be deleted). If the
user clicks on the No button, the delete operation will be cancelled. The code exits the subroutine after
displaying an appropriate message, without executing the code that follows the If . . .statement.

The application is obviously far from complete, since at the moment it displays only the first and last names of
the contact. As a final exercise, you may wish to complete the program by adding the requisite text boxes and
labels, and generally improving the look and feel of the user interface. You will also need to write some
additional code (for example to clear the remainder of the text boxes when the Add Record button is pressed,
and extend the NavigateRecords() procedure). Your final application might look something like the illustration
below.

A possible layout for the application

28
Create a simple data application by using
ADO.NET
When you create an application that manipulates data in a database, you perform basic tasks such as
defining connection strings, inserting data, and running stored procedures. By following this topic,
you can discover how to interact with a database from within a simple Windows Forms "forms over
data" application by using Visual C# or Visual Basic and ADO.NET. All .NET data technologies—
including datasets, LINQ to SQL, and Entity Framework—ultimately perform steps that are very similar
to those shown in this article.

This article demonstrates a simple way to get data out of a database in a fast manner. If your
application needs to modify data in non-trivial ways and update the database, you should consider
using Entity Framework and using data binding to automatically sync user interface controls to
changes in the underlying data.
Important

To keep the code simple, it doesn't include production-ready exception handling.


Prerequisites

To create the application, you'll need:

 Visual Studio.
 SQL Server Express LocalDB. If you don't have SQL Server Express LocalDB, you can install it from
the SQL Server Express download page.

This topic assumes that you're familiar with the basic functionality of the Visual Studio IDE and can
create a Windows Forms application, add forms to the project, put buttons and other controls on the
forms, set properties of the controls, and code simple events. If you aren't comfortable with these
tasks, we suggest that you complete the Getting started with Visual C# and Visual Basic topic before
you start this walkthrough.
Set up the sample database

Create the sample database by following these steps:

1. In Visual Studio, open the Server Explorer window.


2. Right-click on Data Connections and choose Create New SQL Server Database.
3. In the Server name text box, enter (localdb)\mssqllocaldb.
4. In the New database name text box, enter Sales, then choose OK.

The empty Sales database is created and added to the Data Connections node in Server
Explorer.
29
5. Right-click on the Sales data connection and select New Query.

A query editor window opens.

6. Copy the Sales Transact-SQL script to your clipboard.


7. Paste the T-SQL script into the query editor, and then choose the Execute button.

After a short time, the query finishes running and the database objects are created. The
database contains two tables: Customer and Orders. These tables contain no data initially, but
you can add data when you run the application that you'll create. The database also contains
four simple stored procedures.
Create the forms and add controls

1. Create a project for a Windows Forms application, and then name it SimpleDataApp.

Visual Studio creates the project and several files, including an empty Windows form that's
named Form1.

2. Add two Windows forms to your project so that it has three forms, and then give them the
following names:
 Navigation
 NewCustomer
 FillOrCancel
3. For each form, add the text boxes, buttons, and other controls that appear in the following
illustrations. For each control, set the properties that the tables describe.
Note

The group box and the label controls add clarity but aren't used in the code.

Navigation form

Controls for the Navigation form Properties

Button Name = btnGoToAdd

Button Name = btnGoToFillOrCancel

Button Name = btnExit

30
NewCustomer form

Controls for the NewCustomer form Properties

TextBox Name = txtCustomerName

TextBox Name = txtCustomerID

Readonly = True

Button Name = btnCreateAccount

NumericUpdown DecimalPlaces = 0

Maximum = 5000

Name = numOrderAmount

DateTimePicker Format = Short

Name = dtpOrderDate

Button Name = btnPlaceOrder

Button Name = btnAddAnotherAccount

Button Name = btnAddFinish

FillOrCancel form

Controls for the FillOrCancel form Properties

TextBox Name = txtOrderID

31
Controls for the FillOrCancel form Properties

Button Name = btnFindByOrderID

DateTimePicker Format = Short

Name = dtpFillDate

DataGridView Name = dgvCustomerOrders

Readonly = True

RowHeadersVisible = False

Button Name = btnCancelOrder

Button Name = btnFillOrder

Button Name = btnFinishUpdates

Store the connection string

When your application tries to open a connection to the database, your application must have access
to the connection string. To avoid entering the string manually on each form, store the string in
the App.config file in your project, and create a method that returns the string when the method is
called from any form in your application.

You can find the connection string by right-clicking on the Sales data connection in Server
Explorer and choosing Properties. Locate the ConnectionString property, then
use Ctrl+A, Ctrl+C to select and copy the string to the clipboard.

1. If you're using C#, in Solution Explorer, expand the Properties node under the project, and
then open the Settings.settings file. If you're using Visual Basic, in Solution Explorer,
click Show All Files, expand the My Project node, and then open the Settings.settings file.
2. In the Name column, enter connString.
3. In the Type list, select (Connection String).
4. In the Scope list, select Application.
5. In the Value column, enter your connection string (without any outside quotes), and then save
your changes.
Note
32
In a real application, you should store the connection string securely, as described in Connection
strings and configuration files.
Write the code for the forms

This section contains brief overviews of what each form does. It also provides the code that defines
the underlying logic when a button on the form is clicked.
Navigation form

The Navigation form opens when you run the application. The Add an account button opens the
NewCustomer form. The Fill or cancel orders button opens the FillOrCancel form. The Exit button
closes the application.
Make the Navigation form the startup form

If you're using C#, in Solution Explorer, open Program.cs, and then change
the Application.Run line to this: Application.Run(new Navigation());

If you're using Visual Basic, in Solution Explorer, open the Properties window, select
the Application tab, and then select SimpleDataApp.Navigation in the Startup form list.
Create auto-generated event handlers

Double-click the three buttons on the Navigation form to create empty event handler methods.
Double-clicking the buttons also adds auto-generated code in the Designer code file that enables a
button click to raise an event.
Add code for the Navigation form logic

In the code page for the Navigation form, complete the method bodies for the three button click
event handlers as shown in the following code.
C#Copy

/// <summary>
/// Opens the NewCustomer form as a dialog box,
/// which returns focus to the calling form when it is closed.
/// </summary>
private void btnGoToAdd_Click(object sender, EventArgs e)
{
Form frm = new NewCustomer();
frm.Show();
}

/// <summary>
/// Opens the FillorCancel form as a dialog box.
/// </summary>
private void btnGoToFillOrCancel_Click(object sender, EventArgs e)
{
Form frm = new FillOrCancel();
frm.ShowDialog();
33
}

/// <summary>
/// Closes the application (not just the Navigation form).
/// </summary>
private void btnExit_Click(object sender, EventArgs e)
{
this.Close();
}
NewCustomer form

When you enter a customer name and then select the Create Account button, the NewCustomer
form creates a customer account, and SQL Server returns an IDENTITY value as the new customer ID.
You can then place an order for the new account by specifying an amount and an order date and
selecting the Place Order button.
Create auto-generated event handlers

Create an empty Click event handler for each button on the NewCustomer form by double-clicking on
each of the four buttons. Double-clicking the buttons also adds auto-generated code in the Designer
code file that enables a button click to raise an event.
Add code for the NewCustomer form logic

To complete the NewCustomer form logic, follow these steps.

1. Bring the System.Data.SqlClient namespace into scope so that you don't have to fully qualify
the names of its members.

C#Copy

using System.Data.SqlClient;

2. Add some variables and helper methods to the class as shown in the following code.

C#Copy

// Storage for IDENTITY values returned from database.


private int parsedCustomerID;
private int orderID;

/// <summary>
/// Verifies that the customer name text box is not empty.
/// </summary>
private bool IsCustomerNameValid()
{
if (txtCustomerName.Text == "")
{

34
MessageBox.Show("Please enter a name.");
return false;
}
else
{
return true;
}
}

/// <summary>
/// Verifies that a customer ID and order amount have been provided.
/// </summary>
private bool IsOrderDataValid()
{
// Verify that CustomerID is present.
if (txtCustomerID.Text == "")
{
MessageBox.Show("Please create customer account before placing order.");
return false;
}
// Verify that Amount isn't 0.
else if ((numOrderAmount.Value < 1))
{
MessageBox.Show("Please specify an order amount.");
return false;
}
else
{
// Order can be submitted.
return true;
}
}

/// <summary>
/// Clears the form data.
/// </summary>
private void ClearForm()
{
txtCustomerName.Clear();
txtCustomerID.Clear();
dtpOrderDate.Value = DateTime.Now;
numOrderAmount.Value = 0;
this.parsedCustomerID = 0;
}

3. Complete the method bodies for the four button click event handlers as shown in the following
code.

35
C#Copy

/// <summary>
/// Creates a new customer by calling the Sales.uspNewCustomer stored procedure.
/// </summary>
private void btnCreateAccount_Click(object sender, EventArgs e)
{
if (IsCustomerNameValid())
{
// Create the connection.
using (SqlConnection connection = new
SqlConnection(Properties.Settings.Default.connString))
{
// Create a SqlCommand, and identify it as a stored procedure.
using (SqlCommand sqlCommand = new SqlCommand("Sales.uspNewCustomer",
connection))
{
sqlCommand.CommandType = CommandType.StoredProcedure;

// Add input parameter for the stored procedure and specify what to
use as its value.
sqlCommand.Parameters.Add(new SqlParameter("@CustomerName",
SqlDbType.NVarChar, 40));
sqlCommand.Parameters["@CustomerName"].Value = txtCustomerName.Text;

// Add the output parameter.


sqlCommand.Parameters.Add(new SqlParameter("@CustomerID",
SqlDbType.Int));
sqlCommand.Parameters["@CustomerID"].Direction =
ParameterDirection.Output;

try
{
connection.Open();

// Run the stored procedure.


sqlCommand.ExecuteNonQuery();

// Customer ID is an IDENTITY value from the database.


this.parsedCustomerID =
(int)sqlCommand.Parameters["@CustomerID"].Value;

// Put the Customer ID value into the read-only text box.


this.txtCustomerID.Text = Convert.ToString(parsedCustomerID);
}
catch
{

36
MessageBox.Show("Customer ID was not returned. Account could not
be created.");
}
finally
{
connection.Close();
}
}
}
}
}

/// <summary>
/// Calls the Sales.uspPlaceNewOrder stored procedure to place an order.
/// </summary>
private void btnPlaceOrder_Click(object sender, EventArgs e)
{
// Ensure the required input is present.
if (IsOrderDataValid())
{
// Create the connection.
using (SqlConnection connection = new
SqlConnection(Properties.Settings.Default.connString))
{
// Create SqlCommand and identify it as a stored procedure.
using (SqlCommand sqlCommand = new SqlCommand("Sales.uspPlaceNewOrder",
connection))
{
sqlCommand.CommandType = CommandType.StoredProcedure;

// Add the @CustomerID input parameter, which was obtained from


uspNewCustomer.
sqlCommand.Parameters.Add(new SqlParameter("@CustomerID",
SqlDbType.Int));
sqlCommand.Parameters["@CustomerID"].Value = this.parsedCustomerID;

// Add the @OrderDate input parameter.


sqlCommand.Parameters.Add(new SqlParameter("@OrderDate",
SqlDbType.DateTime, 8));
sqlCommand.Parameters["@OrderDate"].Value = dtpOrderDate.Value;

// Add the @Amount order amount input parameter.


sqlCommand.Parameters.Add(new SqlParameter("@Amount",
SqlDbType.Int));
sqlCommand.Parameters["@Amount"].Value = numOrderAmount.Value;

// Add the @Status order status input parameter.


// For a new order, the status is always O (open).
37
sqlCommand.Parameters.Add(new SqlParameter("@Status", SqlDbType.Char,
1));
sqlCommand.Parameters["@Status"].Value = "O";

// Add the return value for the stored procedure, which is the order
ID.
sqlCommand.Parameters.Add(new SqlParameter("@RC", SqlDbType.Int));
sqlCommand.Parameters["@RC"].Direction =
ParameterDirection.ReturnValue;

try
{
//Open connection.
connection.Open();

// Run the stored procedure.


sqlCommand.ExecuteNonQuery();

// Display the order number.


this.orderID = (int)sqlCommand.Parameters["@RC"].Value;
MessageBox.Show("Order number " + this.orderID + " has been
submitted.");
}
catch
{
MessageBox.Show("Order could not be placed.");
}
finally
{
connection.Close();
}
}
}
}
}

/// <summary>
/// Clears the form data so another new account can be created.
/// </summary>
private void btnAddAnotherAccount_Click(object sender, EventArgs e)
{
this.ClearForm();
}

/// <summary>
/// Closes the form/dialog box.
/// </summary>
private void btnAddFinish_Click(object sender, EventArgs e)
38
{
this.Close();
}
FillOrCancel form

The FillOrCancel form runs a query to return an order when you enter an order ID and then click
the Find Order button. The returned row appears in a read-only data grid. You can mark the order as
canceled (X) if you select the Cancel Order button, or you can mark the order as filled (F) if you select
the Fill Order button. If you select the Find Order button again, the updated row appears.
Create auto-generated event handlers

Create empty Click event handlers for the four buttons on the FillOrCancel form by double-clicking
the buttons. Double-clicking the buttons also adds auto-generated code in the Designer code file that
enables a button click to raise an event.
Add code for the FillOrCancel form logic

To complete the FillOrCancel form logic, follow these steps.

1. Bring the following two namespaces into scope so that you don't have to fully qualify the names
of their members.

C#Copy

using System.Data.SqlClient;
using System.Text.RegularExpressions;

2. Add a variable and helper method to the class as shown in the following code.

C#Copy

// Storage for the order ID value.


private int parsedOrderID;

/// <summary>
/// Verifies that an order ID is present and contains valid characters.
/// </summary>
private bool IsOrderIDValid()
{
// Check for input in the Order ID text box.
if (txtOrderID.Text == "")
{
MessageBox.Show("Please specify the Order ID.");
return false;
}

// Check for characters other than integers.

39
else if (Regex.IsMatch(txtOrderID.Text, @"^\D*$"))
{
// Show message and clear input.
MessageBox.Show("Customer ID must contain only numbers.");
txtOrderID.Clear();
return false;
}
else
{
// Convert the text in the text box to an integer to send to the database.
parsedOrderID = Int32.Parse(txtOrderID.Text);
return true;
}
}

3. Complete the method bodies for the four button click event handlers as shown in the following
code.

C#Copy

/// <summary>
/// Executes a t-SQL SELECT statement to obtain order data for a specified
/// order ID, then displays it in the DataGridView on the form.
/// </summary>
private void btnFindByOrderID_Click(object sender, EventArgs e)
{
if (IsOrderIDValid())
{
using (SqlConnection connection = new
SqlConnection(Properties.Settings.Default.connString))
{
// Define a t-SQL query string that has a parameter for orderID.
const string sql = "SELECT * FROM Sales.Orders WHERE orderID = @orderID";

// Create a SqlCommand object.


using (SqlCommand sqlCommand = new SqlCommand(sql, connection))
{
// Define the @orderID parameter and set its value.
sqlCommand.Parameters.Add(new SqlParameter("@orderID",
SqlDbType.Int));
sqlCommand.Parameters["@orderID"].Value = parsedOrderID;

try
{
connection.Open();

// Run the query by calling ExecuteReader().


using (SqlDataReader dataReader = sqlCommand.ExecuteReader())
40
{
// Create a data table to hold the retrieved data.
DataTable dataTable = new DataTable();

// Load the data from SqlDataReader into the data table.


dataTable.Load(dataReader);

// Display the data from the data table in the data grid
view.
this.dgvCustomerOrders.DataSource = dataTable;

// Close the SqlDataReader.


dataReader.Close();
}
}
catch
{
MessageBox.Show("The requested order could not be loaded into the
form.");
}
finally
{
// Close the connection.
connection.Close();
}
}
}
}
}

/// <summary>
/// Cancels an order by calling the Sales.uspCancelOrder
/// stored procedure on the database.
/// </summary>
private void btnCancelOrder_Click(object sender, EventArgs e)
{
if (IsOrderIDValid())
{
// Create the connection.
using (SqlConnection connection = new
SqlConnection(Properties.Settings.Default.connString))
{
// Create the SqlCommand object and identify it as a stored procedure.
using (SqlCommand sqlCommand = new SqlCommand("Sales.uspCancelOrder",
connection))
{
sqlCommand.CommandType = CommandType.StoredProcedure;

41
// Add the order ID input parameter for the stored procedure.
sqlCommand.Parameters.Add(new SqlParameter("@orderID",
SqlDbType.Int));
sqlCommand.Parameters["@orderID"].Value = parsedOrderID;

try
{
// Open the connection.
connection.Open();

// Run the command to execute the stored procedure.


sqlCommand.ExecuteNonQuery();
}
catch
{
MessageBox.Show("The cancel operation was not completed.");
}
finally
{
// Close connection.
connection.Close();
}
}
}
}
}

/// <summary>
/// Fills an order by calling the Sales.uspFillOrder stored
/// procedure on the database.
/// </summary>
private void btnFillOrder_Click(object sender, EventArgs e)
{
if (IsOrderIDValid())
{
// Create the connection.
using (SqlConnection connection = new
SqlConnection(Properties.Settings.Default.connString))
{
// Create command and identify it as a stored procedure.
using (SqlCommand sqlCommand = new SqlCommand("Sales.uspFillOrder",
connection))
{
sqlCommand.CommandType = CommandType.StoredProcedure;

// Add the order ID input parameter for the stored procedure.


sqlCommand.Parameters.Add(new SqlParameter("@orderID",
SqlDbType.Int));
42
sqlCommand.Parameters["@orderID"].Value = parsedOrderID;

// Add the filled date input parameter for the stored procedure.
sqlCommand.Parameters.Add(new SqlParameter("@FilledDate",
SqlDbType.DateTime, 8));
sqlCommand.Parameters["@FilledDate"].Value = dtpFillDate.Value;

try
{
connection.Open();

// Execute the stored procedure.


sqlCommand.ExecuteNonQuery();
}
catch
{
MessageBox.Show("The fill operation was not completed.");
}
finally
{
// Close the connection.
connection.Close();
}
}
}
}
}

/// <summary>
/// Closes the form.
/// </summary>
private void btnFinishUpdates_Click(object sender, EventArgs e)
{
this.Close();
}

43
23.1 Creating a Simple Database Application
Visual Basic allows us to manage databases created with different database programs such as MS
Access, Oracle, MySQL and more. In this lesson, we are not dealing with how to create database
files but we will see how we can access database files in the VB environment. In the following
example, we will create a simple database application which enables one to browse customers'
names. To create this application, select the data control on the toolbox(as shown in Figure 23.1)
and insert it into the new form. Place the data control somewhere at the bottom of the form. Name the
data control as data_navigator. To be able to use the data control, we need to connect it to any
database. We can create a database file using any database application but I suggest we use the
database files that come with VB6. Let's select NWIND.MDB as our database file.

Figure 23.1

23.2 Connecting Data Control to Database


To connect the data control to this database, double-click the DatabaseName property in the
Properties window and then click on the button with three dots on the right(as shown in Figure 23.2)
to open a file selection dialog as shown in Figure 23.3. From the dialog, search the folders of your
hard drive to locate the database file NWIND.MDB. It is usually placed under Microsoft Visual
Studio\VB98\ folder, Select the aforementioned file and now your data control is connected to this
database file.

Figure 23.2

44
Figure 23.3

The next step is to double-click on the RecordSource property to select the customers table from the
database file NWIND.MDB, as shown in Figure 23.4. You can also change the caption of the data
control to anything, we use Click to browse Customers. After that, we will place a label and change its
caption to Customer Name. In addition, insert another label and name it as cus_name and leave the
label empty as customers' names will appear here when we click the arrows on the data control. We
need to bind this label to the data control for the application to work. To do this, open the
label's DataSource and select data_navigator that will appear automatically. One more thing that we
need to do is to bind the label to the correct field so that data in this field will appear on this label. To
do this, open the DataField property and select ContactName, as shown in Figure 23.5.

Figure 23.4

45
Figure 23.5

Now, press F5 and run the program. You should be able to browse all the customers' names by
clicking the arrows on the data control, as shown in Figure 23.7.

46
Figure 23.7 The Runtime Interface

You can also add other fields using exactly the same method. For example, you can add title,
company, adress, City, postcode ,telephone number and more to the database browser. Besides, you
can design a more professional interface, as shown in Figure 23.8.

Figure 23.8

47
you have learned how to create a simple database application using data control. However, you could
only browse the database using the data control. In this lesson, you shall learn how to create your
own navigation buttons for browsing the database. Besides that, we shall also learn how to add, save
and delete data.The data control supports some methods that allow manipulation of a database, for
example, to move the pointer to a certain location.

The following are some of the commands that you can use to move the pointer around:

' Move to the first record

data_navigator.RecordSet.MoveFirst

' Move to the last record

data_navigator.RecordSet.MoveLast

' Move to the next record

data_navigator.RecordSet.MoveNext

' Move to the first record>

data_navigator.RecordSet.Previous

You can also add, save and delete records using the following commands:

data_navigator.RecordSet.AddNewp ' Adds a new record

data_navigator.RecordSet.Updatep ' Updates and saves the new record

data_navigator.RecordSet.Delete ' Deletes a current record

*note: data_navigator is the name of data control

In the following example, you shall insert four commands and label them as First Record, Next
Record, Previous Record and Last Record . They will be used to navigator around the database
without using the data control. You still need to retain the same data control (from example in lesson
19) but set the property Visible to no so that users will not see the data control but use the button to
browse through the database instead. Now, double-click on the command button and key in the
codes according to the labels.

Private Sub cmdFirst()

data_navigator.Recordset.MoveFirst
End Sub

Private Sub cmdFirst_Click()


data_navigator.Recordset.MoveNext
End Sub
48
Private Sub cmdPrevious_Click()
data_navigator.Recordset.MovePrevious
End Sub

Private Sub cmdLast_Click()


data_navigator.Recordset.MoveLast
End Sub

Run the application and you shall obtain the interface as shown in Figure 24.1 below and you will be
able to browse the database using the four navigation buttons.

Figure 24.1 The


Runtime Interface

49
Working with Images in Visual Basic

The application we are going to build here can manipulate images in various ways. It is not intended as a
serious piece of image editing software (we will leave that to the professionals) but despite its limitations can
produce some interesting effects. It also demonstrates quite well how images can be manipulated on a pixel by
pixel basis, and how we can resize images to suit our particular needs.

The application has a display area of 640 × 480 pixels - relatively small by modern standards. Images
produced by modern digital cameras tend to be much bigger. Nevertheless, the application can handle any
image you care to throw at it provided it is stored as a bitmap, JPEG or GIF file. Larger images will simply be
scaled down to fit the display area. Nevertheless, this is a quite powerful application considering the relatively
small amount of code involved, and there is plenty of scope to extend the program's functionality.

1. Open a new project called "ImageStudio" and create an interface like the one illustrated below. Note
that you will also need two file dialog controls (see the table below for details) .

The ImageStudio program interface

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

ImageStudio Form Controls

Control Name Additional Properties

Form frmImageStudio Size: 960, 540 Text: "ImageStudio"


PictureBox picDisplay BackColor: ControlDark
BorderStyle: Fixed3D
Location: 12, 12
Size: 640, 480
Button cmdLoad Location: 658, 12
Text: "Load Image"
Button cmdRestore Location: 658, 41
Text: "Restore"
Button cmdSave Location: 658, 70
Text: "Save Image"
Button cmdRed Location: 658, 206
Text: "Red"
Button cmdGreen Location: 658, 235
Text: "Green"
Button cmdBlue Location: 658, 264
Text: "Blue"
Button cmdGrey Location: 658, 293
Text: "Grey Scale"
Button cmdNeg Location: 658, 322
Text: "Negative"
Button cmdSepia Location: 658, 351
Text: "Sepia"
Button cmdMono Location: 658, 380
Text: "Mono"
Button cmdMirror Location: 658, 409
Text: "Mirror"
Button cmdFlip Location: 658, 438
Text: "Flip"
Button cmdRotate180 Location: 658, 467
Text: "Rotate 180"
Button cmdExit ForeColor: Red
Location: 857, 467
Text: "Exit"
TrackBar trkBrightness Location: 780, 12
Maximum: 20
Orientation: Vertical
Size: 32, 480
Value: 10
Label lblTrackbar Location: 818, 246
Text: "Brightness"

51
ImageStudio Form Controls

Control Name Additional Properties

Label lbl_xyR AutoSize: False


BackColor: White
BorderStyle: FixedSingle
Location: 844, 12
Size: 27, 18
Text: None
Label lbl_xyG AutoSize: False
BackColor: White
BorderStyle: FixedSingle
Location: 877, 12
Size: 27, 18
Text: None
Label lbl_xyB AutoSize: False
BackColor: White
BorderStyle: FixedSingle
Location: 910, 12
Size: 27, 18
Text: None
Label lblR AutoSize: False
Location: 844, 30
Size: 27, 18
Text: "R"
TextAlign: MiddleCenter
Label lblG AutoSize: False
Location: 877, 30
Size: 27, 18
Text: "G"
Label lblB AutoSize: False
Location: 910, 30
Size: 27, 18
Text: "B"
OpenFileDialog ofdLoadImage -
SaveFileDialog sfdSaveImage -

3. Once you have created the application's interface, switch to the code editor and add the following
variable declarations to the form's class definition:

Dim bmp01, bmp02, bmpTemp As Bitmap


Dim clr, clrTemp As Color
Dim intR, intG, intB, intTemp As Integer

The first line of code declares three variables of type Bitmap. One of these variables (bmp01) will hold a
reference copy of the image being worked on, while the second (bmp02) will be used to implement changes.
The reason for the third variable will become apparent later.

52
The second line of code declares two variables of type Color, one of which (clr) will be used to hold the result
of changes to the colour of a pixel, while the second (clrTemp) will be used to hold the results of any
intermediate calculations. Note that the Color data type stores each colour as a structure in which the Red(R)
, Green (G) and Blue (B) values that make up the colour are represented as member variables. Each of these
member variables can be individually accessed, and can be read from or written to.

The last line of code declares three integer variables that will be used to store the values of the red (intR) ,
green (intG) and blue (intB) colour components for a given pixel. A fourth integer variable (intTemp) will be
used to hold the results of various intermediate calculations.

The next piece of code we are going to write (and the longest single procedure in the entire program) is the
code to load an image.

4. In the form design window, double click on the Load Image button and add the following code at the
cursor:

Dim intResponse As Integer


Dim sngMul As Single

ofdLoadImage.Filter = "Bitmap Files|*.bmp|JPEG Files|*.jpg|GIF Files|*.gif"


If ofdLoadImage.ShowDialog = DialogResult.OK Then
bmp01 = New Bitmap (Image.FromFile (ofdLoadImage.FileName))
If bmp01.Width > 640 Or bmp01.Height > 480 Then
intResponse = MsgBox _
("Image is larger than display area. Do you want to resize image to fit?", _
vbYesNo + vbQuestion)
End If
If intResponse = vbNo Then
Exit Sub
ElseIf intResponse = vbYes Then
If(bmp01.Width / 4) >= (bmp01.Height / 3) Then
sngMul = 1 / (bmp01.Width / 640)
Else
sngMul = 1 / (bmp01.Height / 480)
End If
bmpTemp = New Bitmap(bmp01, (bmp01.Width * sngMul), (bmp01.Height *
sngMul))
bmpTemp.SetResolution(bmp01.HorizontalResolution,
53
bmp01.VerticalResolution)
bmp01 = bmpTemp
End If
bmp02 = New Bitmap(bmp01)
bmpTemp = New Bitmap(bmp01)
picDisplay.Image = bmp01
Me.Text = "ImageStudio(" & ofdLoadImage.FileName & ")"
End If

This is a rather complex-looking piece of code, but that complexity reflects the fact that it does quite a lot of
work. The first line declares an integer variable (intResponse) that will receive the user's response to the
message box that will be displayed if the target image is too big for the display area (more about this shortly).
The second local variable (sngMul) is a floating-point variable that will be used to hold a calculated value.
Essentially it is a multiplier that reflects the factor by which an oversized image would need to be reduced in
order for the display area to accommodate it.

The next line of code sets up the file filter for the OpenFileDialog control (ofdLoadImage), which as a result
will only list image files of the allowed types (Bitmap, JPEG or GIF). The rest of the code is taken up by an If .
. . End If statement that only executes if the user clicks the Open button of the File Open dialog box, having of
course first selected a file. That being the case, the next line of code creates a new object of type Bitmap from
the image contained in the selected file, and assigns it to the variable bmp01.

The next block of code is a nested If . . . End If statement that executes if the image is too big to fit into the
display area, in which case the user is given the option to resize the image to fit.

If the user answers "No", the code exits the subroutine without further ado. Otherwise, it checks the aspect
ratio of the image to determine whether the image has a 4:3 (or better) width to height ratio, in which case the
multiplier variable sngMul is calculated as the fraction by which the image's dimensions must be multiplied in
order for it to fit into the display area. If the aspect ratio is less than 4:3, then the height is used to calculate the
value for sngMul instead.

The next two lines create a new Bitmap object using bmp01, but with the new (reduced) dimensions. The
result is assigned to bmpTemp, but the Visual Basic SetResolution() method is called to scale down the
contents of the original image to fit into the new one. Initially, bmp01 contains the original image unchanged.
Then, bmpTemp gets a scaled down version of the image, and this resized version is assigned back
to bmp01.

54
The last four lines of code before the End If are much more straightforward. Two new bitmap objects are
created from bmp01 and assigned to the variables bmp02 and bmpTemp.
The picDisplay control's Image property is also set to bmp01.

Finally, the form's Text property is amended to reflect the fact that the application has now loaded, and is
displaying, the contents of an image file. Run the program and load an image to see how this works (if you
don't have a suitable image to hand, you can download the zipped file image_studio.zip, which contains the
images featured in this page, from the following link:

image_studio.zip.

The illustration below shows the application with an image loaded into the display area.

The ImageStudio program can now load an image

5. Before we start writing procedures that change the image, it would be useful to be able to restore the
original image if we don't like the changes we have made. In the form design window, double click on
the Restore button and add the following code at the cursor:
55
picDisplay.Image = bmp01
bmp02 = New Bitmap(bmp01)
bmpTemp = New Bitmap(bmp01)
trkBrightness.Value = 10

This code simply sets the displayed image back to the original image which is safely stored in bmp01. It then
assigns fresh bitmap images to bmp02 and bmpTemp using the image stored in bmp01 (you will not really be
able to see the effect of this code until we have done something to change the appearance of an image).
Meanwhile, it might be interesting to be able to look at the Red, Green and Blue values (RGB values) of the
pixels in our images . . . .

6. In the code editor window, add the following procedure somewhere within the body of the form's class
definition:

Private Sub picOriginal_MouseMove(sender As Object, e As _


Windows.Forms.MouseEventArgs) Handles picDisplay.MouseMove
If picDisplay.Image Is Nothing Then
Exit Sub
End If
If e.X < bmp02.Width And e.Y < bmp02.Height Then
clr = bmp02.GetPixel(e.X, e.Y)
lbl_xyR.Text = clr.R
lbl_xyG.Text = clr.G
lbl_xyB.Text = clr.B
Else
lbl_xyR.Text = ""
lbl_xyG.Text = ""
lbl_xyB.Text = ""
End If
End Sub

This procedure executes if the mouse moves over the picDisplay control. The first If . . . End If statement
checks to see whether there is an image in the PictureBox control, and if not, exits the subroutine
immediately. The first part of the subsequent If . . . Else . . . End If statement is executed if the mouse is
actually over part of the image (which does not necessarily have to occupy the entire display area). It gets the
colour value for the pixel under the mouse and assigns that value to the variable clr.

56
The values of each of the R, G and B components of clr are assigned to the Text properties
of lbl_xyR, lbl_xyG and lbl_xyB respectively. If the mouse is over an area that is not occupied by part of the
image, all three of these Label controls have their Text property set to a blank string (""). Run the application,
load an image, and move the mouse pointer over the display area to see how this code affects the program's
output.

The R, G and B values for the pixel under the mouse pointer are now displayed (top right)

As you move the mouse over the image, you should see rapid changes in the values displayed for
the R, Gand B values of the pixel under the mouse pointer (unless you have chosen an image with a large
area of one colour, of course!). There is one minor annoyance, however. When you move the mouse pointer
off the image and out of the PictureBox control, the last recorded values remain displayed. In order to clear
these values when the mouse is not hovering over the PictureBox control, we need a small addition to our
code.

7. In the code editor window, add the following procedure somewhere within the body of the form's class
definition:

57
Private Sub picOriginal_MouseLeave(sender As Object, e As EventArgs) _
Handles picDisplay.MouseLeave
lbl_xyR.Text = ""
lbl_xyG.Text = ""
lbl_xyB.Text = ""
End Sub

This relatively trivial bit of code does not really need much explanation. It simply sets the Text property of the
three label controls lbl_xyR, lbl_xyG and lbl_xyB to the empty string when the mouse leaves picDisplay.

We now come to the main purpose of the application which is to create interesting (if not particularly useful)
graphic effects using the images we have chosen. For example, we know that the colour displayed by each
pixel is the result of combining three primary colours (red, green and blue), each of which can have an intensity
of between 0 and 255. We can manipulate these RGB values individually for every pixel in an image. If we
want to, we can (for example) turn off the green and blue components and just leave the red component. Let's
do just that!

8. In the form design window, double click on the Red button and add the following code at the cursor:

If picDisplay.Image Is Nothing Then


Exit Sub
End If
For x = 0 To bmp02.Width - 1
For y = 0 To bmp02.Height - 1
clr = bmp02.GetPixel(x, y)
clrTemp = Color.FromArgb(clr.R, 0, 0)
bmp02.SetPixel(x, y, clrTemp)
bmpTemp.SetPixel(x, y, clrTemp)
Next
Next
picDisplay.Image = bmp02
trkBrightness.Value = 10

A common feature of many of the procedures we will write from now on is the If . . . End If statement at the
beginning of the code that exits the procedure immediately if there is no image to process. The remainder of
the code in this procedure loops through every pixel in the image, one column at a time, using nested For . . .

58
Next loops. The colour value for each pixel is retrieved and stored in the variable clr using GetPixel() (one of
the methods available to a Visual Basic object of type Bitmap).

The variable clrTemp is then passed the value of just the red component of clr using the method Color.From
Argb(), in which the green and blue arguments have been set to zero. The SetPixel() method of
the Bitmap object is then called upon twice, once to set the value of the pixel in bmp02 to the new colour (a
shade of pure red), and once to perform the same operation on the corresponding pixel
of bmpTemp (essentially making bmpTemp a copy of bmp02).

The penultimate line sets the displayed image to the new version of bmp02, while the last line resets the
position of the Trackbar control's slider (in case we have moved it meanwhile). Run the program again, load
an image, and click on the Red button to see the result. The screenshots below show "before and after" views
for this feature of the application. If you look carefully at the second picture, you will notice that the RGB values
displayed in the top right hand corner show zero values for the green and blue components (the intensity value
displayed for the red component varies as you move the mouse around over the image) .

An image with no transformations

59
The same image after the red filter has been applied

9. The code for the Green button is almost identical. In the form design window, double click on the
button and add the following code at the cursor:

If picDisplay.Image Is Nothing Then


Exit Sub
End If
For x = 0 To bmp02.Width - 1
For y = 0 To bmp02.Height - 1
clr = bmp02.GetPixel(x, y)
clrTemp = Color.FromArgb(0, clr.G, 0)
bmp02.SetPixel(x, y, clrTemp)
bmpTemp.SetPixel(x, y, clrTemp)
Next
Next
picDisplay.Image = bmp02
trkBrightness.Value = 10

10. Use the same procedure to add the code for the Blue button:
60
If picDisplay.Image Is Nothing Then
Exit Sub
End If
For x = 0 To bmp02.Width - 1
For y = 0 To bmp02.Height - 1
clr = bmp02.GetPixel(x, y)
clrTemp = Color.FromArgb(0, 0, clr.B)
bmp02.SetPixel(x, y, clrTemp)
bmpTemp.SetPixel(x, y, clrTemp)
Next
Next
picDisplay.Image = bmp02
trkBrightness.Value = 10

The purpose of the Grey Scale button is to transform the image from a colour image to its greyscale equivalent
(sometimes greyscale versions of photographs can be used quite effectively). In computer graphics, shades of
grey can easily be created by setting each of the R, G and B components to the same value. At the extreme
ends of this scale you have black (R=0, G=0 and B=0) and white (R=255, G=255 and B=255) Anything in
between is some shade of grey, although as you approach values that are close to either end of the scale, it
becomes quite difficult for the human eye to differentiate.

The main problem here is that the colours in the image will have different values for the red, green and blue
elements (otherwise they would already be grey!). What value do we therefore use for
our R, G and Bcomponents to create the correct shade of grey? The solution we have come up with (which is
not necessarily the only method that could be used) is to take the average intensity of the three colour
components and then apply it to all of them to create a grey shade. The code is shown below and is fairly self
explanatory, providing you understood the code we used to filter out colours in the procedures already
implemented for the Red, Green and Blue buttons.

11. In the form design window, double click on the Grey Scale button and add the following code at the
cursor:

If picDisplay.Image Is Nothing Then


Exit Sub
End If
For x = 0 To bmp02.Width - 1
For y = 0 To bmp02.Height - 1
clr = bmp02.GetPixel(x, y)
61
intR = clr.R
intG = clr.G
intB = clr.B
intTemp = (intR + intG + intB) / 3
clrTemp = Color.FromArgb(intTemp, intTemp, intTemp)
bmp02.SetPixel(x, y, clrTemp)
bmpTemp.SetPixel(x, y, clrTemp)
Next
Next
picDisplay.Image = bmp02
trkBrightness.Value = 10

The main difference in coding here is that we are extracting all of the colour values, adding them together and
dividing by three in order to get the average value. The value thus obtained is then used to create a new colour
that is a shade of grey (by assigning the same value to each of the R, G and B components) and then writing
that new colour back to the corresponding pixels in bmp02 and bmpTemp.

Run the program, load an image, and click on the Grey Scale button to see the result. The screenshots below
show "before and after" views as before. If you look carefully at the second picture, you will notice that the
RGB values displayed in the top right hand corner show identical values for the red, green and blue
components.

62
An image with no transformations

63
The same image after the greyscale filter has been applied

The next procedure we shall write will create a negative image (you may have seen photographic film
negatives of colour or monochrome photographs). This procedure alters the value of each colour component
within each pixel by subtracting the value of each element from its maximum possible value (255) in order to
effectively shift it to the opposite end of the spectrum for that element. Apart from that - if you understood the
previous procedure - little further explanation is necessary.

12. In the form design window, double click on the Negative button and add the following code at the
cursor:

If picDisplay.Image Is Nothing Then


Exit Sub
End If
For x = 0 To bmp02.Width - 1
For y = 0 To bmp02.Height - 1
clr = bmp02.GetPixel(x, y)
clrTemp = Color.FromArgb(255 - clr.R, 255 - clr.G, 255 - clr.B)
64
bmp02.SetPixel(x, y, clrTemp)
bmpTemp.SetPixel(x, y, clrTemp)
Next
Next
picDisplay.Image = bmp02
trkBrightness.Value = 10

13. Run the program, load an image, and click on the Negative button to see the result. The screenshots
below show "before and after" views as before.

An image with no transformations

65
The same image after the negative filter has been applied

Note that again, we make no claims as to the correctness of this procedure in producing a true negative image,
but it seems to work well enough. Note also that the various transformations can be applied in series - the
illustration below shows the effect of first transforming the image to greyscale and then applying the negative
filter to the result. Combining transformations in this way can produce some very interesting results!

66
The same image again, after both greyscale and negative filters have been applied

The next button we come to is Sepia. This time we are definitely walking on disputed territory, as the proffered
definitions of what constitutes "sepia" vary widely. The overall effect is intended to produce (artificially) the
same kind of effect as is seen in very old "black and white" (read greyscale) photographs that have been
affected over time by exposure to light. You may well have seen examples, in which the image has turned from
shades of grey to shades of a sort of orange-brownish colour (again, my description is based on my own
personal perceptions).

The algorithm used in the code offered here is based on "colour degradation" values found on various
graphics-oriented websites and forums that occasionally discuss this issue, and the basis for it is somewhat
abstract. It does, however, appear to work quite effectively. Essentially, the code has the effect of lightening
the image slightly overall, while at the same time adjusting the value of each of the red, green and blue
components based on a fixed ratio of their pre-existing values and that of the other components.

67
As far as I have been able to work out, the effect is to reduce the available colour spectrum (a bit like the
greyscale algorithm) while favouring red and green (in that order) over blue (if anyone has a concise, simple
explanation for how it works, I would be happy to hear from them!)

14. In the form design window, double click on the Sepia button and add the following code at the cursor:

If picDisplay.Image Is Nothing Then


Exit Sub
End If
For x = 0 To bmp02.Width - 1
For y = 0 To bmp02.Height - 1
clr = bmp02.GetPixel(x, y)
intR = (clr.R * 0.393) + (clr.G * 0.769) + (clr.B * 0.189)
intG = (clr.R * 0.349) + (clr.G * 0.686) + (clr.B * 0.168)
intB = (clr.R * 0.272) + (clr.G * 0.534) + (clr.B * 0.131)
If intR > 255.0 Then intR = 255.0
If intG > 255.0 Then intG = 255.0
If intB > 255.0 Then intB = 255.0
clrTemp = Color.FromArgb(Int (intR), Int (intG), Int (intB))
bmp02.SetPixel(x, y, clrTemp)
bmpTemp.SetPixel(x, y, clrTemp)
Next
Next
picDisplay.Image = bmp02
trkBrightness.Value = 10

Because some of the transforms may leave individual colour components with values in excess of 255, the
code includes statements that will limit the values of the R, G and B components to a maximum of 255. Run
the program, load an image, and click on the Sepia button to see the result. The screenshots below show
"before and after" views as before. You can make your own mind up as to the effectiveness of this procedure.

68
An image with no transformations

69
The same image after the sepia filter has been applied

We are back on slightly firmer ground with the Mono button, which produces a genuinely black and white
image in the sense that the resulting images only has black and white pixels. The code is very similar to the
greyscale code except that this time, if the average of the colour component values equates to less than 64 the
pixel is set to black; otherwise it is set to white. Again, we do not proffer this as the only solution, but it seems
to work reasonably well.

15. In the form design window, double click on the Mono button and add the following code at the cursor:

If picDisplay.Image Is Nothing Then


Exit Sub
End If
For x = 0 To bmp02.Width - 1
For y = 0 To bmp02.Height - 1
clr = bmp02.GetPixel(x, y)
intR = clr.R
intG = clr.G
intB = clr.B
intTemp = (intR + intG + intB) / 3
If intTemp < 64 Then
clrTemp = Color.Black
bmp02.SetPixel(x, y, clrTemp)
bmpTemp.SetPixel(x, y, clrTemp)
Else
clrTemp = Color.White
bmp02.SetPixel(x, y, clrTemp)
bmpTemp.SetPixel(x, y, clrTemp)
End If
Next
Next
picDisplay.Image = bmp02
trkBrightness.Value = 10

16. Run the program, load an image, and click on the Mono button to see the result. The screenshots
below show "before and after" views as before.

70
An image with no transformations

The same image after the mono filter has been applied

71
The next procedure (invoked by the Mirror button) creates a mirror image of the image loaded into the display
area. The procedure again loops through the pixels column by column, but this time it swaps the colours in the
pixels in the left and right halves of the screen, starting from the outside edges and working towards the centre
until all of the pixels have had their colours swapped. The algorithm itself is actually relatively straightforward,
although it looks complicated at first glance.

17. In the form design window, double click on the Mirror button and add the following code at the
cursor:

If picDisplay.Image Is Nothing Then


Exit Sub
End If
For x = 0 To (bmp02.Width / 2) - 1
For y = 0 To bmp02.Height - 1
clrTemp = bmp02.GetPixel(x, y)
clr = bmp02.GetPixel((bmp02.Width - x) - 1, y)
bmp02.SetPixel(x, y, clr)
bmp02.SetPixel((bmp02.Width - x) - 1, y, clrTemp)
bmpTemp.SetPixel(x, y, clr)
bmpTemp.SetPixel((bmp02.Width - x) - 1, y, clrTemp)
Next
Next
picDisplay.Image = bmp02
trkBrightness.Value = 10

The main For . . . Next loop uses a counter with a range of 0 to (image width/2)-1. If the image has an odd
number of pixels in the horizontal dimension, the centre column is left unchanged. The program has been
tested on images that have both an odd and an even number of pixels in the horizontal dimension, and it
seems to work correctly in both cases. Run the program, load an image, and click on the Mirror button to see
the result. The screenshots below show "before and after" views as before.

72
An image with no transformations

The same image after the mirror procedure has been applied

73
The penultimate transformation that we will code here is invoked by the Flip button. It is very similar to the
mirror procedure, except that this time the image is inverted rather than mirrored. There is still a symmetrical
exchange of colour values between pixels, but now it takes place across the horizontal centreline between the
top and bottom halves of the image rather than between the left and right halves. The code is therefore very
similar, and works in pretty much the same way.

18. In the form design window, double click on the Flip button and add the following code at the cursor:

If picDisplay.Image Is Nothing Then


Exit Sub
End If
For x = 0 To bmp02.Width - 1
For y = 0 To (bmp02.Height / 2) - 1
clrTemp = bmp02.GetPixel(x, y)
clr = bmp02.GetPixel(x, (bmp02.Height - y) - 1)
bmp02.SetPixel(x, y, clr)
bmp02.SetPixel(x, (bmp02.Height - y) - 1, clrTemp)
bmpTemp.SetPixel(x, y, clr)
bmpTemp.SetPixel(x, (bmp02.Height - y) - 1, clrTemp)
Next
Next
picDisplay.Image = bmp02
trkBrightness.Value = 10

19. Run the program, load an image, and click on the Flip button to see the result. The screenshots
below show the "before and after" views.

74
An image with no transformations

75
The same image after the flip procedure has been applied

The final transformation is triggered when the user clicks on the Rotate 180 button, and is a combination of the
mirror and flip transformations in the sense that it rotates the image through 180 degrees. It essentially
achieves, in a single operation, what applying the mirror and flip operations would achieve if applied separately
one after the other. Applying the rotate 180 procedure twice (or any even number of times for that matter)
restores the image to its original state.

20. In the form design window, double click on the Rotate 180 button and add the following code at the
cursor:

If picDisplay.Image Is Nothing Then


Exit Sub
End If
For x = 0 To bmp02.Width - 1
For y = 0 To (bmp02.Height / 2) - 1
clrTemp = bmp02.GetPixel(x, y)
clr = bmp02.GetPixel((bmp02.Width - x) - 1, (bmp02.Height - y) - 1)

76
bmp02.SetPixel(x, y, clr)
bmp02.SetPixel((bmp02.Width - x) - 1, (bmp02.Height - y) - 1, clrTemp)
bmpTemp.SetPixel(x, y, clr)
bmpTemp.SetPixel((bmp02.Width - x) - 1, (bmp02.Height - y) - 1, clrTemp)
Next
Next
picDisplay.Image = bmp02
trkBrightness.Value = 10

21. Run the program, load an image, and click on the Rotate 180 button to see the result. The
screenshot below shows what happens, using the same image as before.

Using the same image as before, this is the effect when the rotate 180 procedure is applied

The last procedure we will write that has an effect on the image loaded into the display area is not actually a
transformation as such, but it does alter the brightness of the image by increasing or decreasing the intensity of
each of the RGB values in each pixel. It can also be applied to any of the effects we have used so far, at any
time. The effect is intended to be temporary in the sense that the user should be able to move the slider up and

77
down to increase or decrease brightness, but should also be able to return the image to its original state by
setting the slider back to its starting position.

There could be literally hundreds of thousands of pixels in an image, each with a different colour, and each
colour potentially having different values for the red, green and blue components. It is inevitable that uniformly
increasing or decreasing the value of each colour component of each pixel will see some of those values
exceeding 255 or at the upper end of the scale or becoming negative values at the lower end. Either situation
causes a problem, so we need to impose upper and lower limits for pixel RGB component values of 255 and 0
respectively.

Although it should be possible to write a more efficient procedure to handle this situation without resorting to
the use of an otherwise redundant copy of the bitmap, we have opted for an approach that is easier to code.
To that end, bmpTemp (as you may have noticed) is constantly maintained as a copy of bmp02. When the
brightness is increased or decreased, the pixel RGB values in bmp02 are changed with reference to their
counterparts in bmpTemp, which is itself not affected by changes in brightness. Note however that any
transformation applied to an image after its brightness has been changed will be applied to the current state
of bmp02, and will subsequently be copied into bmpTemp) .

22. In the form design window, double click on the Brightness Trackbar control to create the
control's Scroll event handler, and add the following code at the cursor:

If picDisplay.Image Is Nothing Then


Exit Sub
End If
intTemp = trkBrightness.Value * 10
For x = 0 To bmpTemp.Width - 1
For y = 0 To bmpTemp.Height - 1
clr = bmpTemp.GetPixel(x, y)
intR = clr.R * intTemp / 100
intG = clr.G * intTemp / 100
intB = clr.B * intTemp / 100
If intR > 255 Then intR = 255
If intG > 255 Then intG = 255
If intB > 255 Then intB = 255
If intR < 0 Then intR = 0
If intG < 0 Then intG = 0
If intB < 0 Then intB = 0
clrTemp = Color.FromArgb(Int (intR) , Int (intG) , Int (intB))
78
bmp02.SetPixel(x, y, clrTemp)
Next
Next
picDisplay.Image = bmp02

The code loops through the image pixel by pixel, one column at a time, as before. It uses the current value of
the TrackBar control to determine the factor by which to increase or decrease the intensity of each pixel's
RGB components. Run the program one more time, load an image, and experiment with the trackbar control to
see how it affects the image. The screenshots below show an image at full brightness, and the same image
with the trackbar slider set to a value of 3 (the trackbar settings range from 0 to 20, with 10 representing the
original brightness level for the image when it is loaded) .

An image at full brightness

79
The same image at brightness level 3

Since it would be nice to be able to save the results of the changes we have made to an image in a separate
file, the last significant piece of code we will write will be the code for the Save Image button, which allows us
to save the currently displayed image in a Bitmap, JPEG or GIF image file. The code calls on
the SaveFileDialog control sfdSaveImage, and requires little explanation. If the user clicks OK, the code uses
an If . . . ElseIf . . . End If statement to determine which file type the user has chosen, and saves the file in the
appropriate format with the filename given by the user (if the user does not enter a filename, clicking
on Save in the dialog box doesn't actually do anything) .

23. In the form design window, double click on the Save Image button, and add the following code at the
cursor:

sfdSaveImage.Filter = "Bitmap Files|*.bmp|JPEG Files|*.jpg|GIF Files|*.gif"


If sfdSaveImage.ShowDialog = DialogResult.OK Then
If sfdSaveImage.FilterIndex = 1 Then
picDisplay.Image.Save(sfdSaveImage.FileName, _
Drawing.Imaging.ImageFormat.Bmp)

80
ElseIf sfdSaveImage.FilterIndex = 2 Then
picDisplay.Image.Save(sfdSaveImage.FileName, _
Drawing.Imaging.ImageFormat.Jpeg)
ElseIf sfdSaveImage.FilterIndex = 3 Then
picDisplay.Image.Save(sfdSaveImage.FileName, _
Drawing.Imaging.ImageFormat.Gif)
End If
End If

24. To complete the application, double click on the Exit button, and add the following command at the
cursor:

End

The application could be extended to do much more, and probably more efficiently. It nevertheless serves to
demonstrate some of the ways in which Visual Basic can be used to manipulate graphic images, and provides
a starting point for those interested in creating more sophisticated image editing applications.

Graphic Encryption with Visual Basic

Elsewhere in this section we look at a simple encryption program that encodes text messages by substituting
each printable character in the ASCII character set with another character, chosen from the first 256 characters
of the Unicode character encoding scheme. In this program we are going to encode a text message into a
bitmapped image using a very subtle encoding scheme that will make small changes to the image that cannot
be detected by the human eye. In fact, the coding is so subtle that providing you choose the right kind of
image, it would be extremely difficult for a third party to extract the encoded message without a copy of the
program, even if they suspected that the image might contain a hidden message.

The program, as written, does make it rather obvious that the images contain encoded messages, because we
have given the encoded image files their own special file extension (.ebm for encoded bitmap). You could just
save the image with the normal extension for a bitmap file (.bmp), and no-one would be any the wiser. In fact,
the only way to tell the difference between the original bitmap image and the encoded version would be to
compare the byte values at the start of the file to see if there are any differences. Note that on average, only

81
about half of the bytes used to encode the message will actually change from their original value, for reasons
that will become apparent as you begin to understand how the code works.

Before we proceed, it is probably worth mentioning that what we are doing here is not, strictly speaking,
"encryption". The correct term is steganography. Steganography differs from encryption, because the way in
which the message is hidden means that only the sender and receiver of the message will be aware that a
hidden message exists. By contrast an encrypted message is usually pretty obviously just that, because it
consists of a message that is unintelligible to anyone not possessing the necessary key and decryption
algorithm.

Stenography encompasses a broad range of techniques used to hide messages, some of which go back
several thousand years. Such techniques have included, for example, tattooing a message on a courier's
shaved head and allowing the hair to grow again in order to hide the message (slow but effective); the use of
invisible ink; and making small changes to the size, spacing and typeface of printed matter in order to convey a
hidden subtext.

The computer age has provided countless additional ways in which to hide messages. Digital media files
provide excellent hiding places for covert information because of their size. The approach we are taking here is
a relatively simple one; we are hiding our messages in bitmapped image files because each pixel in a
bitmapped image is preserved in its original state when the image is saved, and does not undergo further
encoding of the kind used in other image file formats. There are probably many far more ingenious methods
that could be used to hide information in electronic media files.

1. Open a new project called "GraphicEncryption" and create an interface like the one illustrated below.
The images used in the sample screenshots are contained in a zipped file called
"graphic_encryption.zip", which can be downloaded here:
graphic_encryption.zip

82
The GraphicEncryption program interface

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

frmGraphicEncryption Form Controls

Control Name Additional Properties

Form frmGraphicEncryption Size: 520, 600


Text: "GraphicEncryption"
PictureBox picImage BorderStyle: Fixed3D
Location: 10, 10
Size: 320, 240
Label lblMessage Location: 12, 262
Text: None
83
frmGraphicEncryption Form Controls

Control Name Additional Properties

TextBox txtMessage Enabled: False


Location: 10, 280
Multiline: True
ScrollBars: Vertical
Size: 320, 240
Label lblMessageLength Location: 10, 534
Text: "Message length: 0 characters"
Button cmdLoadBmp Location: 345, 12
Size: 140, 23
Text: "Load Bitmap Image"
Button cmdEncrypt Enabled: False
Location: 345, 41
ize: 140, 23
Text: "Encrypt Data"
Button cmdSaveImage Enabled: False
Location: 345, 70
Size: 140, 23
Text: "Save Encrypted Image"
Button cmdLoadEnc Location: 345, 99
Size: 140, 23
Text: "Load Encrypted Image"
Button cmdDecrypt Enabled: False
Location: 345, 128
Size: 140, 23
Text: "Decrypt Data"
Button cmdSaveText Enabled: False
Location: 345, 157
Size: 140, 23
Text: "Save Plain Text"
Button cmdCancel Enabled: False
Location: 345, 186
Size: 140, 23
Text: "Cancel"
Button cmdExit ForeColor: Red
Location: 410, 529
ext: "Exit"
OpenFileDialog ofdLoadBitmap -
SaveFileDialog sfdSaveBitmap -
SaveFileDialog sfdSavePlainText -

84
The program code for the form's class definition is presented below in its entirety. There are no lengthy
explanations provided; the code is annotated with numerous comments that should be sufficient to allow the
reader to understand how the program works. At the bottom of this page, however, there is a detailed
explanation of how message data is encoded into the images, since this is something that may not be entirely
obvious from either the code or the accompanying comments - especially if the reader is not too well
acquainted with binary logic.

The program code:

Public Class frmGraphicEncryption


'Bitmap variables to hold image data
Dim bmp01, bmp02, bmpTemp As Bitmap
'Variable to temporarily store pixel colour values
Dim clr As Color
'Variables to hold screen coordinates and row and column values
Dim x, y, rows, cols As Integer
'Variable to hold modified colour component values
Dim ORcol As Integer
'Variables to hold actual and maximum permitted message lengths
Dim intMessageLength, intMaxLength As Integer
'Variable to keep track of whether message exists or not
Dim boolMsgFlag As Boolean

Private Sub cmdLoadBmp_Click(sender As Object, e As _


EventArgs) Handles cmdLoadBmp.Click

'Multiplier value used for resizing image if needed


Dim sngMul As Single

'Set OpenFileDialog control to see bitmap files only


ofdLoadBitmap.Filter = "Windows Bitmap Files|*.bmp"

'This code executes if user selects (or types) a valid file name and clicks
OK

85
If ofdLoadBitmap.ShowDialog = DialogResult.OK Then
'Assign image from file to bmp01
bmp01 = New Bitmap(Image.FromFile(ofdLoadBitmap.FileName))
'Display image file's name at top of form
Me.Text = "GraphicEncryption(" & ofdLoadBitmap.FileName & ")"
'Assign a copy of the image to bmp02
bmp02 = New Bitmap(bmp01)
'If the image in bmp01 is too big for the picture box to display,
'scale it down as required
If bmp01.Width > 320 Or bmp01.Height > 240 Then
If(bmp01.Width / 4) >= (bmp01.Height / 3) Then
sngMul = 1 / (bmp01.Width / 320)
Else
sngMul = 1 / (bmp01.Height / 240)
End If
bmpTemp = New Bitmap(bmp01, (bmp01.Width * sngMul), _
(bmp01.Height * sngMul))
bmpTemp.SetResolution(bmp01.HorizontalResolution, _
bmp01.VerticalResolution)
bmp01 = bmpTemp
End If
'Load the [resized] image into the picture box
picImage.Image = bmp01
'Get the height of the original version of the image (as rows)
rows = bmp02.Height
'Get the width of the original version of the image (as cols)
cols = bmp02.Width
'Calculate the maximum number of characters that can be encoded
intMaxLength = rows * cols / 8
'Limit message length to largest 16-bit value
If intMaxLength > 65535 Then intMaxLength = 65535
'Set the text box's maximum length property
txtMessage.MaxLength = intMaxLength
'Display message and inform user of maximum message length allowed
lblMessage.Text = "Type your message here (maximum " _
& intMaxLength & " characters) :"
'Initialise message-related variables and set up display
'to allow user to input message
86
intMessageLength = 0
x = 0
y = 0
txtMessage.Enabled = True
txtMessage.Focus()
lblMessageLength.Visible = True
cmdLoadEnc.Enabled = False
cmdLoadBmp.Enabled = False
cmdCancel.Enabled = True
End If
End Sub

Private Sub cmdLoadEnc_Click(sender As Object, e As _


EventArgs) Handles cmdLoadEnc.Click

'Multiplier value used for resizing image if needed


Dim sngMul As Single

'Set OpenFileDialog control to see encoded bitmap files only


ofdLoadBitmap.Filter = "Encrypted Bitmap Files|*.ebm"
'This code executes if user selects (or types) a valid file name and clicks
OK
If ofdLoadBitmap.ShowDialog = DialogResult.OK Then
'Assign image from file to bmp01
bmp01 = New Bitmap(Image.FromFile(ofdLoadBitmap.FileName))
'Display encoded image file's name at top of form
Me.Text = "GraphicEncryption(" & ofdLoadBitmap.FileName & ")"
'Assign a copy of the image to bmp02
bmp02 = New Bitmap(bmp01)
'If the image in bmp01 is too big for the picture box to display,
'scale it down as required
If bmp01.Width > 320 Or bmp01.Height > 240 Then
If(bmp01.Width / 4) >= (bmp01.Height / 3) Then
sngMul = 1 / (bmp01.Width / 320)
Else
sngMul = 1 / (bmp01.Height / 240)
End If
bmpTemp = New Bitmap(bmp01, (bmp01.Width * sngMul), _
87
(bmp01.Height * sngMul))
bmpTemp.SetResolution(bmp01.HorizontalResolution, _
bmp01.VerticalResolution)
bmp01 = bmpTemp
End If
'Load the [resized] image into the picture box
picImage.Image = bmp01
'Get the height of the original version of the image(as rows)
rows = bmp02.Height
'Get the width of the original version of the image(as cols)
cols = bmp02.Width
lblMessage.Text = "Encrypted message:"
'Initialise message-related variables and set up display
'to allow user to decrypt message
intMessageLength = 0
x = 0
y = 0
lblMessageLength.Visible = True
cmdLoadEnc.Enabled = False
cmdLoadBmp.Enabled = False
cmdDecrypt.Enabled = True
cmdCancel.Enabled = True
End If
End Sub

Private Sub txtMessage_TextChanged(sender As Object, e As _


EventArgs) Handles txtMessage.TextChanged
'This code executes each time the message text input by the user changes

'Get the current message length


intMessageLength = Len(txtMessage.Text)
'If the message is not zero-length and this is the first character
' (boolMsgFlag = False), set boolMsgFlag to True to indicate message
'is in progress and enable Encrypt Data button
If Len(txtMessage.Text) > 0 And txtMessage.ReadOnly = False Then
If boolMsgFlag = False Then
boolMsgFlag = True
cmdEncrypt.Enabled = True
88
End If
'If message is zero length (user has deleted all characters)
'set boolMsgFlag to False and disable Encrypt Data button
Else
boolMsgFlag = False
cmdEncrypt.Enabled = False
End If
'Update displayed message length
lblMessageLength.Text = "Message length: " & intMessageLength & "
characters"
End Sub

Private Sub cmdEncrypt_Click(sender As Object, e As _


EventArgs) Handles cmdEncrypt.Click

'ByteArray() holds the values for each bit in the ASCII code
Dim ByteArray(0 To 7) As Integer
'ByteMask() holds the binary masks used to extract the individual bit
values
Dim ByteMask() As Integer = {128, 64, 32, 16, 8, 4, 2, 1}
'LengthArray() Holds the values for each bit in the message length value
Dim LengthArray(0 To 15) As Integer
'LengthMask() holds the binary masks used to extract the individual bit
values
Dim LengthMask() As Integer = _
{32768, 16384, 8192, 4096, 2048, 1024, 512, 256, 128, 64, 32, 16, 8, 4,
2, 1}
'Encode the message length into the red pixel component of the first 16
pixels
'This loop will assign either a one or a zero to each element of
LengthArray
'The integer value MessageLength is ANDed with each LengthMask element in
turn,
'resulting in integer values that are either zero or a binary power within
the
'range: [1,2,4,8,16,32,64,128,256,512,1024,2048,4096,8192,16,384,32768]
'The resulting integer value is right-shifted in order to leave 0 or 1
For i = 0 To 15
89
LengthArray(i) = (intMessageLength And LengthMask(i)) >> (15 - i)
'Get the colour of the pixel
clr = bmp02.GetPixel(i, 0)
'Set ORcol to the value of the pixel's red component,
'with the least significant bit is set to 1
ORcol = clr.R Or 1
'Modify the pixel's colour so that the least significant bit of the red
'component is set to the same value (0 or 1) as bit i in the
'intMessageLength variable
clr = Color.FromArgb((ORcol And (254 + LengthArray(i))), clr.G, clr.B)
'Set the pixel to the modified colour
bmp02.SetPixel(i, 0, clr)
Next
'Encode the bits from one character's ASCII code into 8 pixels
'This loop will assign either a one or a zero to each element of ByteArray
For i = 0 To (intMessageLength - 1)
For j = 0 To 7
'The ASCII code is ANDed with each ByteMask element in turn, resulting
in
'integer values that are either zero or a binary power within the
range:
'[1,2,4,8,16,32,64,128] - the resulting integer value is right-shifted
'in order to leave 0 or 1
ByteArray(j) = (Asc(txtMessage.Text(i)) And ByteMask(j)) >> (7 - j)
'Get the colour of the pixel
clr = bmp02.GetPixel(x, y)
'Set ORcol to the value of the pixel's blue component,
'with the least significant bit set to 1
ORcol = clr.B Or 1
'Modify the pixel's colour so that the least significant bit of the
blue
'component is set to the same value (0 or 1) as bit j in the ASCII
'character code
clr = Color.FromArgb(clr.R, clr.G, (ORcol And (254 + ByteArray(j))))
'Set the pixel to the modified colour
bmp02.SetPixel(x, y, clr) 'If this is the last pixel in the
row, go to the start of the next row,
'otherwise go to the next pixel in the row
90
If x = cols - 1 Then
x = 0
y += 1
Else
x += 1
End If
Next
Next
'Set up the user interface to allow the user to save the encoded image file
txtMessage.Enabled = False
cmdEncrypt.Enabled = False
cmdSaveImage.Enabled = True
End Sub

Private Sub cmdDecrypt_Click(sender As Object, e As _


EventArgs) Handles cmdDecrypt.Click

'ByteArray() holds the values for each bit in the ASCII code
Dim ByteArray(0 To 7) As Integer
'LengthArray() Holds the values for each bit in the message length value
Dim LengthArray(0 To 15) As Integer
'CodedChar holds the result of each decoding operation
Dim CodedChar As Char
'CodedASCII holds the ASCII character code of each character exctracted
Dim CodedASCII As Integer

'Set up the text box for read-only output


txtMessage.Enabled = True
txtMessage.ReadOnly = True

'Get the message length from the red pixel value using the first 16 pixels
For i = 0 To 15
'Get the colour of the current pixel
clr = bmp02.GetPixel(i, 0)
'Get the LSB value for the pixel's red component
LengthArray(i) = (clr.R And 1)
'Add the binary component to the length variable
intMessageLength += 65535 And LengthArray(i) << (15 - i)
91
Next

For i = 0 To (intMessageLength - 1)
'Initialise the ASCII character code variable to zero
CodedASCII = 0
'extract the eight bits that make up the ASCII character code
For j = 0 To 7
'Get the colour of the current pixel
clr = bmp02.GetPixel(x, y)
'Get the LSB value for the pixel's blue component
ByteArray(j) = (clr.B And 1)
'Add the binary component to the character code
CodedASCII += 255 And ByteArray(j) << (7 - j)
'If this is the last pixel in the row, go to the start of the next row,
'otherwise go to the next pixel in the row
If x = cols - 1 Then
x = 0
y += 1
Else
x += 1
End If
Next
'Output the message to the text box
CodedChar = Chr(CodedASCII)
txtMessage.Text &= CodedChar
Next
'Set up the user interface to allow the user to
'save the decoded message as a text file
cmdDecrypt.Enabled = False
cmdSaveText.Enabled = True
lblMessageLength.Text = "Message length: " & intMessageLength & "
characters"
End Sub

Private Sub cmdSaveImage_Click(sender As Object, e As _


EventArgs) Handles cmdSaveImage.Click
'This procedure saves the image with its encrypted message
'as a bitmap file with the extension .ebm
92
sfdSaveBitmap.Filter = "Encrypted Bitmaps |*.ebm"
If sfdSaveBitmap.ShowDialog = DialogResult.OK Then
bmp02.Save(sfdSaveBitmap.FileName, Drawing.Imaging.ImageFormat.Bmp)
End If
'Set the program back to its initial state
resetProgram()
End Sub

Private Sub cmdSaveText_Click(sender As Object, e As _


EventArgs) Handles cmdSaveText.Click
'This procedure saves the decoded message as a text file
Dim FileWriter As IO.StreamWriter

sfdSavePlainText.Filter = "Text |*.txt"


If sfdSavePlainText.ShowDialog = DialogResult.OK Then
FileWriter = New IO.StreamWriter(sfdSavePlainText.FileName, False)
FileWriter.Write(txtMessage.Text)
FileWriter.Close()
End If
'Set the program back to its initial state
resetProgram()
End Sub

Private Sub cmdCancel_Click(sender As Object, e As _


EventArgs) Handles cmdCancel.Click
'Set the program back to its initial state
resetProgram()
End Sub

Sub resetProgram()
'This procedure resets the program variables and
'restores the user interface to its original state
bmp01 = Nothing
bmp02 = Nothing
bmpTemp = Nothing
Me.Text = "GraphicEncryption"
picImage.Image = Nothing
txtMessage.Clear()
93
txtMessage.Enabled = False
txtMessage.ReadOnly = False
lblMessage.Text = ""
lblMessageLength.Visible = False
cmdLoadEnc.Enabled = True
cmdLoadBmp.Enabled = True
cmdEncrypt.Enabled = False
cmdSaveImage.Enabled = False
cmdDecrypt.Enabled = False
cmdSaveText.Enabled = False
cmdCancel.Enabled = False
End Sub

Private Sub cmdExit_Click _


(sender As Object, e As EventArgs) Handles cmdExit.Click
'This command exits the program unconditionally
End
End Sub
End Class

Notes on the encoding/decoding algorithms

The program can encode an ASCII text-message (i.e. a message containing only eight-bit ASCII characters)
into a bitmapped image. It does this by setting the value of the least significant bit (LSB) of the blue colour
component in each pixel used. Each character in the message is encoded using eight contiguous pixels, and
the characters in the message are encoded sequentially into eight-byte blocks within the bitmap, in the same
order in which they appear in the original message. In order to encode onecharacter, eight bytes of image data
are required.

The possibilities for encoding data into a bitmap are almost endless. You could, for example, use all three
colour components to encode the hidden information without noticably changing the image. This would
increasing the amount of information that could be hidden (i.e. the encoding density) by a factor of three for
any given image. Alternatively, you could increase the number of bytes used for the character encoding in
order to allow the full range of Unicode characters to be encoded. This would reduce the encoding density, but
would allow you to encode messages containing virtually any kind of character or symbol.

94
We leave more ambitious implementations to those with the enthusiasm to pursue them. In the meantime, a
more detailed look how the existing program encodes an ASCII text message might be illuminating. Let's
assume that the message we want to encode is:

"The boy stood on the burning deck ..."

Let us further assume that the bitmap image file "Tigger.bmp" (one of the files in graphic_encryption.zip) will
be used as the carrier. By carrier, we mean the medium within which we will hide the information. The following
screenshots outline the steps the user must follow in order to end up with an encoded bitmap file containing
this information:

Click on the Load Bitmap Image button

95
Browse to and select the file "Tigger.bmp", then click the Open button

96
Type the message into the text box and click on the Encrypt Data button

97
Click on the Save Encrypted Data button

98
Save the encoded file with the .ebm file extension

The encoding of the message is done within a loop that iterates through the characters in the message one at
a time. A second loop, nested within the first, is used to extract the eight bits of the ASCII character code for
each character and encode it onto eight contiguous pixels within the image. Let's look at how this works for the
first character in the message (in this example, the upper-case letter "T") :

1. First of all, we get the ASCII character code for "T", which is 84

2. To get the ByteArray() elements for the character, we use each ByteMask() element in turn, ANDing it
with the character code to isolate bits 0 through 7 (starting with bit 7 and working from left to right). We
then right-shift them the requisite number of places to move them into the least significant bit (LSB)
position:

84 AND 128
Binary: 01010100 AND 10000000 = 00000000

99
Right-shifting 00000000 by 7 places = 0

84 AND 64
Binary: 01010100 AND 01000000 = 01000000
Right-shifting 01000000 by 6 places = 1

84 AND 32
Binary: 01010100 AND 00100000 = 00000000
Right-shifting 00000000 by 5 places = 0

84 AND 16
Binary: 01010100 AND 00010000 = 00010000
Right-shifting 00010000 by 4 places = 1

84 AND 8
Binary: 01010100 AND 00001000 = 00000000
Right-shifting 00000000 by 3 places = 0

84 AND 4
Binary: 01010100 AND 00000100 = 00000100
Right-shifting 00000100 by 2 places = 1

84 AND 2
Binary: 01010100 AND 00000010 = 00000000
Right-shifting 00000000 by 1 place = 0

84 AND 1
Binary: 01010100 AND 00000001 = 00000000
Right-shifting 00000000 by 0 places = 0

To (hopefully) clarify things somewhat, the process is summarised in the following tables:

Bitwise Operations (bits 7, 6, 5 and 4)


ASCII Code (84) 01010100 01010100 01010100 01010100
Bitmask values (128, 64, 32, 16) 10000000 01000000 00100000 00010000
Result of AND 00000000 01000000 00000000 00010000
100
Result of right-shifting (7, 6, 5 and 4 places) 00000000 00000001 00000000 00000001

Bitwise Operations (bits 3, 2, 1 and 0)


ASCII Code (84) 01010100 01010100 01010100 01010100
Bitmask values (8, 4, 2, 1) 00001000 00000100 00000010 00000001
Result of AND 00000000 01000100 00000000 00000000
Result of right-shifting (3, 2, 1 and 0 places) 00000000 00000001 00000000 00000000

3. For each bit of the ASCII code, we get the colour of the pixel into which we are going to encode it,
and OR the integer value of the colour's blue component with 1 (effectively forcing the least significant
bit to be set to 1).
4. We then AND the corresponding ByteArray() element with the modified blue colour component, so that
its least significant bit will be the same as that of the ByteArray() element.
5. Finally, we create an updated version of the original colour using the modified (and possibly unchanged)
value for the blue component, and set the pixel to the updated colour.

The inner loop of our encoding procedure executes eight times in total in order to encode the bits of a single
character into a sequence of eight pixels. The outer loop repeats for as many times as there are characters in
the message, until they are all encoded. The encoding of the message length value onto the red component of
the first 16 pixels in the image works in exactly the same way - it's just a (potentially) bigger number.

If you understand how the encoding procedure works, you should be able to discern how the decoding
algorithm works without too much trouble by looking at the code, so we will forgo any further explanation. Of
course, the best way to prove that the code actually works is to try it out, so if we open our encrypted bitmap
file, we should be able to retrieve the encoded message. The screenshot below shows the result of doing just
that with a longer passage of text which we encoded into the "Elephant.ebm" bitmap file.

101
A message has been retrieved from an encrypted bitmap image

102
Simple File I/O with Visual Basic

File input and output (or file I/O) involves retrieving data (the input) from a file for use in your program. New
data created by your program, or existing data that has been modified by your program, is the program's
output. This output data may be stored in the same file, or in a new file. The commands available to the
programmer for file I/O include those required to open and close files, read data from a file, and write the
results of any data processing back to a file.

The data files we will be working with here will be text files. Text files can be used to store fairly large blocks of
text (for example, a short story, a letter, or a poem). We are more interested here, however, in text files that
can store records. A record is a single line in a file that consists of one or more fields. A customer record, for
example, could contain the fields CustomerName, StreetAddress, Town and TelNumber.

In text-based data files, the fields in a record are usually comma-delimited (i.e. each field is separated from the
next by a comma). A typical file processing operation opens a file, reads the records into memory and
processes them one at a time. The results of this processing are normally output to the screen. Once all of the
required records have been read from the file, the file is closed.

If records have been modified by a program, the amended records are usually either written back to the same
file, or to a another file. Writing data to a file is a separate operation in which the file must be re-opened, or a
new file created. If amended records are written back to the same file, the original version of each record will
be replaced (overwritten) by the new version. If new data is being added to a file, the records will be added
(appended) to the end of the file, and the existing data will be preserved.

The act of opening a file and reading the records it contains into memory can be described as pre-processing,
because this action prepares the records for processing. When records are read from a file, they are
accessed sequentially (one after the other), in whatever order they happen to be stored in within the file.
Sequential input from a file is called a data stream.

We are now going to create a simple program to read data from a text file and display the data in a text box.

1. Create a new project called FileReader and create an interface with a RichTextBox control and two
buttons, as shown below.

103
The FileReader program interface

2. Save the project to create the project folder.


3. Download the file country.zip from the link below, and unzip the contents into the \bin\Debug sub-
directory of your project folder (the .zip file contains a file called country.dat).
Download the file country.zip
4. Set the control names as shown in the table below.

FileReader Form Controls

Control Name

Form frmFileReader
RichTextBox1 rtbOutput
Button1 cmdRead
Button2 cmdExit

5. In the form design window, double-click on the cmdRead button and add the following code:

Dim srdFile As IO.StreamReader


Dim strLine As String

rtbOutput.Clear()

104
srdFile = New IO.StreamReader("country.dat")
Do Until srdFile.Peek = -1
strLine = srdFile.ReadLine()
rtbOutput.AppendText (strLine & vbNewLine)
Loop
srdFile.Close()

6. Double-click on the cmdExit button and add the following command at the cursor:

End

Your code should now look like the screenshot below.

The FileReader program code

The first line of code declares a variable of type IO.StreamReader. This is one of the standard classes defined
within Visual Basic's System.IO namespace, and as the name suggests it includes methods for reading data
from files. The second line of code declares a string variable called strLine which will hold each line of text
read from the file.

105
In the next line of code, the command rtbOutput.clear() calls the RichTextBox object's clear() procedure to
clear any pre-existing contents from the box. The fourth line of code uses the New keyword to allocate memory
to the variable srdFile (it essentially creates a new instance of a StreamReader object), associates it with the
file country.dat, and opens the file for input.

Providing the country.dat file is in the \bin\debug sub-directory of your project folder, the program will be able
to find it. If you were to move the file to a different folder, you would need to add the drive and path information
in order to enable the program to find it.

The Do . . . Until loop iterates through the instructions contained within it until the exit condition is met. The exit
condition in this case is when the StreamReader object's Peek method detects the end of the file (in which
case it returns a value of -1). Each iteration of the loop reads one line of the file into the strLinevariable using
the StreamReader object's ReadLine() method, and appends the data to the output displayed by
the RichTextBox object, using that object's AppendText() method.

Note that the command rtbOutput.AppendText(strLine & vbNewLine) also appends a newline character
(represented by the Visual Basic constant vbNewLine) to each line of text to be displayed. The last line of
code for the cmdRead button's Click event uses the StreamReader object's close() method to close the file.
When you have finished working with a file, you should always close it.

If you run the program and click on the Read File button, you should see something like the following
screenshot:

106
The FileReader program output

Reading a text file one line at a time is relatively easy, as we have seen. When each line in the file is a record
consisting of multiple fields, we need to do some more work in order to extract the information from each field
and display it in the correct format. The fields in such a file are usually separated by a comma and are often
referred to as comma-separated values. For this reason, files that contain data in this format often have the
extension ".csv".

To demonstrate how data in a comma separated value file is processed, we will create an application to read
and display the contents of a file that contains league table information for the Barclays Premier League. The
file league_table.csv can be downloaded here.

The records in the file are preceded by a header on the first line giving the date on which the information was
published. Each record in the file consists of the name of a Premier League team, the number of games
played, the number of games won, the number of games drawn, the number of games lost, and the total
number of points achieved to date. We are going to create a program to read the file and display the contents
in an appropriate format.

1. Create a new project called LeagueTable and create an interface with a RichTextBox control, as
shown below.

The LeagueTable program interface

107
2. Save the project to create the project folder.
3. Download the league_table.csv file from the link above if you have not already done so, and save it to
the \bin\Debug sub-directory of your project folder.
4. Set the Font property of the RichTextBox control to "Courier New, 8.25pt"
5. Set the control names as shown in the table below.

LeagueTable Form Controls

Control Name

Form frmLeagueTable
Label1 lblTitle
Label2 lblDate
RichTextBox1 rtbOutput
Button1 cmdDisplayInfo
Button2 cmdExit

6. In the form design window, double-click on the cmdDisplayInfo button and add the following code at the
cursor:

Dim srdFile As IO.StreamReader


Dim strRec() As String
Dim strLine As String

srdFile = New IO.StreamReader("league_table.csv")


strLine = srdFile.ReadLine
lblDate.Text = "Date: " & strLine
rtbOutput.Clear()
rtbOutput.AppendText _
("Team Played Won Drawn Lost Points" _
& vbNewLine & vbNewLine)
Do Until srdFile.Peek = -1
strLine = srdFile.ReadLine
strRec = strLine.Split(",")
rtbOutput.AppendText(strRec(0).PadRight(15) & _
strRec(1).PadRight(8) & strRec(2).PadRight(8) _
& strRec(3).PadRight(8) & strRec(4).PadRight(8) & _

108
strRec(5).PadRight(8) & vbNewLine)
Loop
srdFile.Close()

7. Double-click on the cmdExit button and add the following command:

End

Your code editor window should now look like the screenshot below.

The LeagueTable program code

Here is the output from the LeagueTable program:

109
The LeagueTable program output

As in the previous program, we have declared a string variable called strLine to hold the contents of each line
of text read from the file. Since each line of text is actually a record consisting of comma-delimited fields, we
want to be able to access the data in each field without having to write complex procedures for extracting each
field from the input string (a process referred to as parsing). We do this using the Split()method provided for
Visual Basic strings, which breaks a string into its constituent substrings with reference to a specific delimiter
symbol (in this case a comma) which is passed to the Split() method as a parameter.

The variable strRec() is declared as an empty array of type String, into which we can place the field data from
each record. The string values returned by Split() will become the array elements within the strRec()array
variable, and can be referenced using the array subscripts 0 - 5.

The code preceding the Do . . . Until loop creates the StreamReader object and reads in the first line of the
file (the date information), which is then displayed at the top of the form. The RichTextBox is cleared (as for
the FileReader program), and the column headings for the data are displayed.

110
The loop iterates until the end of the file is encountered. On each iteration, one record is read from the file and
is assigned to the six-element strRec() array after being split into its constituent fields. Each array element is
then appended to the RichTextBox output, and the newline character (represented by the Visual Basic
constant vbNewLine) is appended to each line of output.

The PadRight() method ensures that each array element occupies the same number of text columns, and that
the output is vertically aligned. The StreamReader class defines a number of methods for handling file input.
The most commonly used methods, including those we have already used, are summarised below.

StreamReader Methods

Name Description

Close Closes the StreamReader object and its stream, and releases system resources associated with it.
Peek Returns the next available character without advancing the file pointer.
Read Reads the next character from the input stream and advances the file pointer by one character.
ReadLine Reads a line of characters from the input stream and returns the data as a string.
ReadToEnd Reads all data in the input stream from the current position to the end of the stream.

We have already mentioned that the data in a file is accessed sequentially as a stream of bytes. As the
program reads each item of data in a file, it moves a special placeholder called a file pointer to the start of the
next data item so that it can keep track of where it is in the file. This is a bit like putting a bookmark in a book
when you put it down, so that you know where to start reading the next time you pick up the book.

A data file is usually loaded into memory from a secondary storage device in its entirety. Low-level file-access
operations, on the other hand, read the file one byte at a time once it is in memory, advancing the file pointer to
the start of the next byte each time they perform a read operation. The end of a line of text in a file is marked
by a special character called a newline character. Methods like ReadLine use that fact to read all of the
characters in a line of text into a buffer until they encounter the newline character, at which point they stop
reading characters.

The end of file character signals the end of the file, enabling the Peek method (which reads the next character
in the file without moving the file pointer) to detect an end-of-file condition.

111
So far we have read data from a file and displayed it on the screen, but the data has to get into the file in the
first place. This is achieved by using an application that accepts user input (or date retrieved from another file) ,
and writes it to an output file. The data may also undergo some kind of processing before it is written to the
output file.

We will create a simple program that accepts user input and writes it to a text file as a series of comma
separated values. The program will record data about the highest temperatures recorded for some of Western
Europe's capital cities throughout the year 2010.

1. Create a new project called Temperatures and create an interface like the one shown below.

The Temperatures program interface

2. Save the project to create the project folder.


3. Set the control names as shown in the table below.

Temperatures Form Controls

Control Name

Form1 frmTemperatures
TextBox1 txtCity
TextBox2 txtJan

112
Temperatures Form Controls

Control Name

TextBox3 txtApr
TextBox4 txtJul
TextBox5 txtOct
Button1 cmdEnterData
Button2 cmdDisplayData
Button3 cmdExit

4. In the form design window, double-click on the cmdEnterData button and add the following code:

Dim swrFile As IO.StreamWriter


Dim strLine As String
strLine = txtCity.Text & "," & txtJan.Text & "," & _
txtApr.Text & "," & TxtJul.Text & "," & txtOct.Text
swrFile = IO.File.AppendText ("temperature.csv")
txtCity.Clear()
txtJan.Clear()
txtApr.Clear()
txtJul.Clear()
txtOct.Clear()
swrFile.WriteLine(strLine)
swrFile.Close()

The first line of code declares a variable of type IO.StreamWriter. This is another one of the standard classes
defined within Visual Basic's System.IO namespace, and as the name suggests it includes methods for writing
data to files.

The second line of code declares a string variable called strLine which will hold each record to be written to
the file as a string containing comma-delimited field values.

The third line of code concatenates the input data entered by the user into the various text boxes into
the strLine string variable. The fourth line of code opens a file called temperatures.csv and associates it with
the swrFile object.

113
Note that the file is opened in append mode. This means that if the file already exists, the file pointer will be
set to the end of the file so that new data is added (appended) to the file. Any existing data is preserved, and is
not overwritten. If the file does not exist, it is created.

The next five lines of code clear the text boxes for the next item of input data. The penultimate line writes the
comma-delimited record stored in strLine to the file, and the last line closes the file once more.

5. Double-click on the cmdDisplayData button and add the following command at the cursor:

Dim srdFile As IO.StreamReader


Dim strfile As String
srdFile = New IO.StreamReader("temperature.csv")
strfile = srdFile.ReadToEnd
MsgBox(strfile)
srdFile.Close()

6. Double-click on the cmdExit button and add the following command at the cursor:

End

Your code editor window should now look like the screenshot below.

114
The Temperature program code

The code for the cmdDisplayData button is very similar to some of the code you have seen previously, except
that we have used the StreamWriter object's ReadToEnd() method to read all of the data in the file into a
string variable (strFile). The resulting string is subsequently displayed in a message box. Test the program by
entering temperature data from the table below into the program one record at a time, and clicking on
the Display data button.

Maximum Temperatures (°F) in European Cities (2010)

City Jan Apr Jul Oct

Amsterdam 41 53 69 57

Athens 54 67 90 74

115
Maximum Temperatures (°F) in European Cities (2010)

City Jan Apr Jul Oct

Berlin 35 55 74 55

Copenhagen 36 50 72 53

Dublin 47 54 67 57

Glasgow 43 53 66 54

Helsinki 27 43 71 45

Lisbon 56 64 79 69

London 44 56 73 58

Madrid 50 63 89 67

Oslo 30 50 73 49

Paris 42 60 76 59

Rome 54 68 88 73

Stockholm 31 45 70 48

Vienna 34 57 75 55
Zurich 36 60 77 57

The StreamWriter class defines numerous methods for handling file output. Some of these methods, including
the ones we have already seen, are summarised below.

StreamWriter Methods

Name Description

Close Closes the StreamWriter object and its stream.


Write(Boolean) Writes the text representation of a Boolean value to a text stream.
Write(Char) Writes a character to a text stream.
Write(Char()) Writes a character array to a text stream.
Write(Decimal) Writes the text representation of a decimal value to a text stream.
Write(Double) Writes the text representation of an 8-byte floating-point value to a text stream.

116
StreamWriter Methods

Name Description

Write(Int32) Writes the text representation of a 4-byte signed integer to a text stream.
Write(Int64) Writes the text representation of an 8-byte signed integer to a text stream.
Write(Single) Writes the text representation of a 4-byte floating-point value to a text stream.
Write(String) Writes a string to a text stream.
Write(UInt32) Writes the text representation of a 4-byte unsigned integer to a text stream.
Write(UInt64) Writes the text representation of an 8-byte unsigned integer to a text stream.
WriteLine Writes a line terminator to a text stream.

Introduction to VB Classes and Objects

Visual Basic is an object-oriented programming language. An object encapsulates both data and
the methods (procedures) that act on the data. The details of the data structures used and the implementation
of the methods are hidden within the object. All the programmer needs to know in order to use an object is
what tasks the object can perform, and the parameters required by the object in order to perform those tasks.

When you click on the Button item in the Visual Basic toolbox, a button object is placed on your form. Click on
the Button item again, and another button object that looks exactly like the first button object will be placed on
your form. Each of these objects has the same properties and methods, with the same default value for each
property except the (Name) property. In fact, clicking on any of the items in the Visual Basic toolbox will result
in a predefined object of one kind or another being placed on your form.

The button objects on your form are all instances of a class. Essentially, a class is a template from which an
object is created, and specifies the properties and methods that will be common to all objects of that class.
Each text box you place on a form is therefore an instance of the class TextBox. Once you have created the
text box object, you can set its properties and invoke its methods. For example, you can set the Textproperty
by assigning it the value of a string variable. Or you could clear the contents of the text box by calling
the Clear() method.

As programmers, we don't need to know how the methods provided by Visual Basic's built in objects are
implemented. We just need to know what properties we can access, and what methods are available to us.
117
This changes when we start creating our own user-defined classes; we need to to know a little more about how
classes work. With that in mind, we are going to create an application that calls on the services of a user-
defined class.

1. Open a new project called "Classes" and create a form that looks like the one illustrated below.

The Classes program interface

2. Rename the form's controls as shown in the table below.

Properties for "frmClasses" form

Control Name

Form frmClasses
TextBox 1 txtCompany
TextBox 2 txtAddress01
TextBox 3 txtAddress02
TextBox 4 txtTown
TextBox 5 txtPostCode
TextBox 6 txtTel
TextBox 7 txtFax
TextBox 8 txtEmail
TextBox 9 txtNotes
Button 1 cmdEnter
Button 2 cmdDisplay
Button 3 cmdExit

Creating the CCustomer class

It is a convention to precede the name of a class with the letter 'C'. We will therefore use the
name CCustomer for our class. Use the following steps to create the CCustomer class:

118
3. From the Project menu, select Add Class...
4. Change the default file name to "CCustomer.vb" and click on Add
5. The code editor window for the class will open with the following code already created for you:

Public Class CCustomer

End Class

Applications communicate with objects through the properties and methods they expose. When you program
with Visual Basic, you are working with objects (like the controls you place on a form) by accessing their
properties and invoking their methods. One of the first things we should perhaps look at is the issue
of encapsulation - a central theme in object-oriented programming. Encapsulation essentially means hiding
both the complexity of an object and the object's data.

Hiding complexity essentially means that the only thing a user of the object has to worry about is what the
object does - not how it does it. The object offers methods for carrying out various tasks. How those methods
are implemented internally is of no concern to the user. In principle, as long as each method's inputs and
outputs remain the same, the code that implements the functionality of an object's methods can be completely
rewritten (perhaps in order to make the object's implementation more efficient) without affecting the way in
which the object is used.

Hiding data essentially means hiding an object's member variables from view within the object by declaring
them as private using the Private keyword. Private member variables are only accessible using the object's
publicly accessible methods. Although it is perfectly possible to make an object's member variables publicly
accessible, it is considered to be poor programming practice for a number of reasons. For one thing, it means
that an object's variables can potentially be accessed and modified from anywhere in the program - which
becomes a problem when you suddenly find the variables taking on unexpected values and have to track down
the culprit.

Our CCustomer class has a number of member variables, which we are going to declare as private for the
reasons discussed above, and because there is no compelling reason not to do so. In order to be able to
access our member variables, we will provide two methods for each variable - one to set the variable's value,
and another to retrieve it. Such methods are often referred to as accessor methods.

6. Add the following code (we have been quite liberal with comments!) to the class definition:

119
'These are the member variables used to hold data.
'The word "Private" means they cannot be accessed directly.
Private mCompany, mAddress01, mAddress02, mTown As String
Private mPostCode, mTel, mFax, mEmail As String

'This procedure retrieves the value of the variable mCopmpany


Public Function GetCompany() As String
GetCompany = mCompany
End Function

'This procedure assigns a value to the variable mCopmpany


Public Sub SetCompany (strValue As String)
mCompany = strValue
End Sub

'This procedure retrieves the value of the variable mAddress01


Public Function GetAddress01() As String
GetAddress01 = mAddress01
End Function

'This procedure assigns a value to the variable mAddress01


Public Sub SetAddress01 (strValue As String)
mAddress01 = strValue
End Sub

'This procedure retrieves the value of the variable mAddress02


Public Function GetAddress02() As String
GetAddress02 = mAddress02
End Function

'This procedure assigns a value to the variable mAddress02


Public Sub SetAddress02 (strValue As String)
mAddress02 = strValue
End Sub

'This procedure retrieves the value of the variable mTown


Public Function GetTown() As String
GetTown = mTown
120
End Function

'This procedure assigns a value to the variable mTown


Public Sub SetTown (strValue As String)
mTown = strValue
End Sub

'This procedure retrieves the value of the variable mPostCode


Public Function GetPostCode() As String
GetPostCode = mPostCode
End Function

'This procedure assigns a value to the variable mPostCode


Public Sub SetPostCode (strValue As String)
mPostCode = strValue
End Sub

'This procedure retrieves the value of the variable mTel


Public Function GetTel() As String
GetTel = mTel
End Function

'This procedure assigns a value to the variable mTel


Public Sub SetTel (strValue As String)
mTel = strValue
End Sub

'This procedure retrieves the value of the variable mFax


Public Function GetFax() As String
GetFax = mFax
End Function

'This procedure assigns a value to the variable mFax


Public Sub SetFax (strValue As String)
mFax = strValue
End Sub

'This procedure retrieves the value of the variable mEmail


121
Public Function GetEmail() As String
GetEmail = mEmail
End Function

'This procedure assigns a value to the variable mEmail


Public Sub SetEmail (strValue As String)
mEmail = strValue
End Sub

Using the CCustomer class

An object is an instance of a class. We are going to add some code to our application's form that creates an
instance of the CCustomer class, which is defined in the module CCustomer.vb. Our program will
communicate with this class object via it's interface. The interface consists of the methods and properties
defined within the class declaration. Every instance of the class will have these methods and properties.

7. Add the following code to the form's class definition:

Dim strDetails As String


Dim customer As New CCustomer

Private Sub cmdExit_Click(sender As Object, e As EventArgs) _


Handles cmdExit.Click
End
End Sub

Private Sub cmdEnter_Click(sender As Object, e As EventArgs) _


Handles cmdEnter.Click
customer.SetCompany(txtCompany.Text)
customer.SetAddress01(txtAddress01.Text)
customer.SetAddress02(txtAddress02.Text)
customer.SetTown(txtTown.Text)
customer.SetPostCode(txtPostCode.Text)
customer.SetTel(txtTel.Text)
customer.SetFax(txtFax.Text)
customer.SetEmail(txtEmail.Text)
msgbox("Company record has been entered.", , "Company Record")

122
End Sub

Private Sub cmdDisplay_Click(sender As Object, e As EventArgs) _


Handles cmdDisplay.Click
strDetails = ""
strDetails &= customer.GetCompany() & vbCrLf
strDetails &= customer.GetAddress01() & vbCrLf
strDetails &= customer.GetAddress02() & vbCrLf
strDetails &= customer.GetTown() & vbCrLf
strDetails &= customer.GetPostCode() & vbCrLf
strDetails &= customer.GetTel() & vbCrLf
strDetails &= customer.GetFax() & vbCrLf
strDetails &= customer.GetEmail()
txtDetails.Text = strDetails
End Sub

Run the program and enter some customer details, click on the Enter Details button to send the data to
the customer object, then click on the Display Details button to retrieve the details from the customer object
and display them in the Company Details: box. You should see something like the illustration below.

123
The program output should look something like this

Data Validation with Visual Basic

Data validation is the process of ensuring, at least as far as is possible, that the data given to a program by a
user or from a file (essentially, the program's input) is of the correct type, and in the correct format. Although
the programmer will obviously take every precaution to ensure the correct operation of the program, and will
attempt to eliminate bugs that could cause a problem through a rigorous process of testing, they have no real
control over mistakes made by the user during data entry. Nor can they guarantee that any data files used by
the program will be free of errors and in the correct format.

There are however measures that can be taken to restrict the program's input to valid data. Such measures
involve the application of validation rules to any data being input to the program. Input not meeting the
program's requirements (i.e. input that does not obey the validation rules) can be dealt with in a pre-defined
way, and will not cause the program to crash or produce spurious output. Data validation rules can also make
an application more user friendly, since they enable the program to warn the user immediately when there is a
124
problem rather than simply allowing them to continue entering data until the program crashes or some other
problem occurs.

The validation rules used will depend on the application, and in particular on the type of data being input to the
program. Database management systems like Oracle or Microsoft Access provide extensive facilities for
creating data validation rules. For our Visual Basic applications, we will need to work a little harder to ensure
that the input is valid, because we will need to define and code our own validation rules.

In a software project of any size, the task of determining what is and what isn't valid data is usually undertaken
long before any code is written, and is part of an initial requirements analysis phase. Even so, we can think in
general terms about the kind of data validation required for a typical application, and how it might be
implemented. Some common data validation requirements are outlined below.

 Permitted character check - useful for determining whether an input string contains valid
characters. For example, a telephone number may include the digits 0-9, and the non-numeric
characters "+" and "-", as well as spaces and brackets.
 Type check - the data entered must be of the correct datatype. If an integer value is expected,
floating-point values (or any other non-integer values) will not be accepted.
 Format check - this sort of check is often applied to things like dates. Programs often expect
dates to be entered in a very specific format (e.g. dd/mm/yyyy or dd:mm:yyyy). The same
principle can be applied to things like credit card or bank account numbers, where a specific
minimum number of digits is expected and non-numeric characters (usually) not permitted.
 Range check - often used for numeric values that must fall within a pre-defined range of values
(for example, permitted operating temperatures, a person's age, or a valid day of the month).
 Limit check - used for numeric values that must either be greater than or equal to some lower
limit, or less than or equal to some upper limit (e.g. a minimum order quantity, or the maximum
number of books someone can borrow from a library at one time).
 Value entered check - used for things like required fields in online forms where the
user mustenter some data (for example their name or telephone number) and must not leave the
field blank.

There are many ways in which validation can be carried out, and it is down to analysts and software
developers to decide what kind of validation is required and how to implement it. To demonstrate some simple
validation techniques, we will create a data entry form of the type typically found on a web page. Proceed as
follows:

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

125
The OnlineForm application interface

2. Set the control names as shown in the table below. Note that we have included a comprehensive list
of properties for all controls on the form. You can of course change the form layout if you wish,
but control names for all input fields must be as shown in order for the code to work properly.

OnlineForm Form Controls

Control Properties

Form Name: frmOnlineForm


Size: 900, 500
Text: "Online Form"
Label Name: lblFormTitle
AutoSize: False
Font: Microsoft Sans Serif, 16pt, style=Bold
Location: 305, 22
Size: 275, 25
Text: "Acme DVD Rental Online"
Label Name: lblApplicationForm
AutoSize: False
Font: Microsoft Sans Serif, 12pt
Location: 277, 57
Size: 330, 20
Text: "Membership Application Form (*required field)"

126
OnlineForm Form Controls

Control Properties

Label Name: lblTitle


AutoSize: False
Font: Microsoft Sans Serif, 10pt
Location: 21, 104
Size: 125, 20
Text: "Title*"
TextAlign: MiddleRight
Label Name: lblLastName
AutoSize: False
Font: Microsoft Sans Serif, 10pt
Location: 21, 137
Size: 125, 20
Text: "Last name*"
TextAlign: MiddleRight
Label Name: lblFirstName
AutoSize: False
Font: Microsoft Sans Serif, 10pt
Location: 21, 170
Size: 125, 20
Text: "First name(s)*"
TextAlign: MiddleRight
Label Name: lblAddress
AutoSize: False
Font: Microsoft Sans Serif, 10pt
Location: 21, 203
Size: 125, 20
Text: "Street address*"
TextAlign: MiddleRight
Label Name: lblTown
AutoSize: False
Font: Microsoft Sans Serif, 10pt
Location: 21, 269
Size: 125, 20
Text: "Town/City*"
TextAlign: MiddleRight
Label Name: lblCounty
AutoSize: False
Font: Microsoft Sans Serif, 10pt
Location: 21, 302
Size: 125, 20
Text: "County/State"
TextAlign: MiddleRight
Label Name: lblCountry
AutoSize: False
Font: Microsoft Sans Serif, 10pt
Location: 21, 368
Size: 125, 20
Text: "Country*"
TextAlign: MiddleRight
Label Name: lblTel
AutoSize: False

127
OnlineForm Form Controls

Control Properties

Font: Microsoft Sans Serif, 10pt


Location: 450, 106
Size: 140, 20
Text: "Telephone*"
TextAlign: MiddleRight
Label Name: lblMob
AutoSize: False
Font: Microsoft Sans Serif, 10pt
Location: 450, 139
Size: 140, 20
Text: "Mobile"
TextAlign: MiddleRight
Label Name: lblEmail
AutoSize: False
Font: Microsoft Sans Serif, 10pt
Location: 450, 172
Size: 140, 20
Text: "Email*"
TextAlign: MiddleRight
Label Name: lblPassword
AutoSize: False
Font: Microsoft Sans Serif, 10pt
Location: 450, 205
Size: 140, 20
Text: "Password*"
TextAlign: MiddleRight
Label Name: lblConfirmPasword
AutoSize: False
Font: Microsoft Sans Serif, 10pt
Location: 450, 238
Size: 140, 20
Text: "Confirm password*"
TextAlign: MiddleRight
Label Name: lblDOB
AutoSize: False
Font: Microsoft Sans Serif, 10pt
Location: 450, 337
Size: 140, 20
Text: "D.O.B. (dd/mm/yyyy)*"
TextAlign: MiddleRight
Label Name: lblCatchpa
AutoSize: False
Font: Microsoft Sans Serif, 10pt
Location: 450, 370
Size: 140, 20
Text: "Complete the sum:*"
TextAlign: MiddleRight
Label Name: lblPasswordNote
AutoSize: False
Font: Microsoft Sans Serif, 10pt
Location: 603, 269

128
OnlineForm Form Controls

Control Properties

Size: 261, 53
Text: "Note: "Password must be between six
and sixteen characters in length and contain
only letters and numbers"
TextAlign: MiddleRight
Label Name: lblSum
AutoSize: False
Font: Mistral, 16pt, style=Bold
Forecolor: Fuchsia
Location: 606, 370
Size: 70, 23
Text: "? + ? ="
TextAlign: MiddleCenter
ComboBox Name: cmbTitle
Location: 154, 103
Size: 65, 21
TextBox Name: txtLastName
Location: 154, 138
Size: 150, 20
TextBox Name: txtFirstName
Location: 154, 171
Size: 150, 20
TextBox Name: txtAddress01
Location: 154, 204
Size: 204, 20
TextBox Name: txtAddress02
Location: 154, 237
Size: 204, 20
TextBox Name: txtTown
Location: 154, 270
Size: 150, 20
TextBox Name: txtCounty
Location: 154, 303
Size: 150, 20
TextBox Name: txtPostcode
Location: 154, 336
Size: 75, 20
TextBox Name: txtCountry
Location: 154, 369
Size: 150, 20
TextBox Name: txtTelephone
Location: 606, 107
Size: 150, 20
TextBox Name: txtMobile
Location: 606, 138
Size: 150, 20

129
OnlineForm Form Controls

Control Properties

TextBox Name: txtEmail


Location: 606, 171
Size: 250, 20
TextBox Name: txtPassword01
Location: 606, 204
Size: 150, 20
UseSystemPasswordChar: True
TextBox Name: txtPassword02
Location: 606, 237
Size: 150, 20
UseSystemPasswordChar: True
TextBox Name: txtDOB
Location: 606, 336
Size: 100, 20
TextBox Name: txtSum
Location: 698, 369
MaxLength: 10
Size: 30, 20
Button Name: cmdSubmit
Location: 781, 416
Size: 75, 23

Note that the last box requires the user to input the result of a simple sum (randomly generated) to prevent
web robot programs from hijacking the form. The popular term CAPTCHA (an approximate abbreviation
of Completely Automated Public Turing Test To Tell Computers and Humans Apart) is often used to describe
this kind of validation. The idea is that a human being will be able to solve a relatively easy arithmetic problem
whereas a robotic program would not even recognise it as such (in a real-world implementation the sum would
not only be randomly generated, but would be presented as a slightly distorted or noisy image in order to
prevent an intelligent robot from being able to decipher the question).

Note also that the two password text boxes should have their PasswordChar property set to "*". The main
event handler on the form will of course be for the Submit button's Click event, which will check the form data
for validity before sending it to the (in this case imaginary) server application. There will be a number of
validation routines attached to other controls, however, to ensure that invalid input is handled as early as
possible. The first field on the form is the cmbTitle ComboBox (we will limit the possible choices in the drop-
down selection to Mr, Mrs, Miss and Ms To keep things simple).

3. Click once on the cmbTitle ComboBox to highlight it and access its properties.
4. Set the MaxLength property to 20.

130
5. Click on the browse button of the Items property.
6. In the String Collection Editor dialog box, enter the options as shown below.

The String Collection Editor dialog box

The MaxLength property determines the maximum number of characters that the user can enter in a field
such as a text box or (in this case) combo box. We have therefore allowed the user the opportunity to enter a
title that is not on the list, such as "Doctor" or "Professor", but have restricted the length of the input string to
prevent abuse. Although not explicitly stated below, you should also impose appropriate limits on the input
length for the remaining fields.

The password fields are a good example of a situation where you can reduce the requirements for data
validation by imposing such a limit. Set the MaxLength property for each password field to 16, and your
validation in terms of password length is then only required to check for a valid minimum number of characters
(since the maximum number cannot be exceeded in any case). The same is true for the user's date of birth
(DOB) and the CAPTCHA sum (which as you will see, can never exceed two digits).

7. The first field that requires specific validation code is the Telephone field. Switch to the code editor
window, select the txtTelephone control using the drop-down list at the top centre of the Code Editor
window, then select the LostFocus event using the drop-down list at the top right-hand side of the
code editor window.

131
8. In the body of the LostFocus event handler, enter the following code:

Dim strAllowedChars As String = "0123456789() -+ "


If Len(txtTelephone.Text) = 0 Then
Exit Sub
Else
For i = 0 To Len(txtTelephone.Text) - 1
If InStr(1, strAllowedChars, txtTelephone.Text(i)) = 0 Then
MsgBox("Invalid telephone number.")
txtTelephone.Focus()
exit sub
End If
Next
End If

The event handler starts with the declaration of a local character array variable called allowedChars() that
contains the characters that we will allow the user to enter as part of the telephone number. The rest of the
code is taken up with an If . . . Else . . . End If statement.

If the user has not entered anything at all, the code exits the subroutine (the problem of this being a mandatory
field will be dealt with separately by the Submit button's event handler, should the field remain empty when the
user tries to submit the form data). Otherwise, the code loops through the input string one character at a time,
to make sure that all of the characters input by the user match one of the allowed characters.

The Visual Basic Instr() function checks for the presence of one string inside another, and if it does not find it
returns zero. If this occurs, the event handler will display a message to tell the user that the telephone number
entered is invalid, restore the focus to the Telephone field, and exit the subroutine. Run the program and test
the code by entering an invalid telephone number. You should see something like the following illustration:

132
The user has typed a lower case "o" instead of a zero

9. The next field that needs validation code is the Email field. Switch to the code editor window, select
the txtEmail control using the drop-down list at the top centre of the code editor window, then select
the LostFocus event using the drop-down list at the top right-hand side of the code editor window.
10. In the body of the LostFocus event handler, enter the following code:

Dim intAt, intDot As Integer


If Len (txtEmail.Text) = 0 Then
Exit Sub
Else
intAt = InStr(1, txtEmail.Text, "@")
intDot = InStr(intAt + 1, txtEmail.Text, ".")
If (intAt = 0) Or (intDot = 0) Or (intDot = (intAt + 1)) _
Or(InStr(intAt + 1, txtEmail.Text, "@") < 0) _
Or(Len(txtEmail.Text) < intDot + 1) _
Or(InStr(intDot + 1, txtEmail.Text, ".") < 0) Then
MsgBox("Invalid email address.")
txtEmail.Focus()
Exit Sub

133
End If
End If

As before, if the user has not entered anything at all, the code exits the subroutine and the mandatory field
problem will be dealt with separately. The local integer variables intAt and intDot are used to store the
location (returned by the InStr() function) of the first occurrence (if any) of the at sign ("@") character in the
string, and the location of the first occurrence thereafter (if any) of the period (".") character.

Note that the first argument to the InStr() function specifies at what position within the target string the search
starts, so any occurrence of a period before the "@" is ignored. The code essentially ensures that there is one
(and only one) "@" in the string, plus at least one period in the substring that follows the "@" (but not
immediately following the "@"). It also checks to make sure that at least the first occurrence of a period is
followed by at least one other character.

If any of these conditions is not met, the event handler displays a message to tell the user that the e-mail
address entered is not valid, restores the focus to the Email field, and exits the subroutine. As before, run the
program and test the code by entering an invalid e-mail address, but be aware that the validation provided here
is very rudimentary - it is perfectly possible to enter an invalid email address that does nottrigger an error
message. To implement a comprehensive validity check for email addresses would require considerably more
code!

We turn next to the password field. The constraints on the password field are such that is easier to validate
user input. All characters entered must be alphanumeric, and there must be at least six characters but no more
than sixteen characters.

11. Switch to the code editor window, select the txtPassword01 control using the drop-down list at the
top centre of the code editor window, then select the LostFocus event using the drop-down list at the
top right-hand side of the code editor window.
12. In the body of the LostFocus event handler, enter the following code:

Dim n As Integer
If Len(txtPassword01.Text) = 0 Then
Exit Sub
ElseIf Len(txtPassword01.Text) < 6 Then
MsgBox("Invalid password")
txtPassword01.Clear()
txtPassword01.Focus()

134
Exit Sub
Else
For i = 0 To Len(txtPassword01.Text) - 1
n = Asc(txtPassword01.Text(i))
If(n < 48) Or ((n > 57) And (n < 65)) Or ((n > 90) And (n < 97)) Or (n >
122) Then
MsgBox("Invalid password")
txtPassword01.Clear()
txtPassword01.Focus()
Exit Sub
End If
Next
End If

The txtPassword control's LostFocus event handler declares a local integer variable (n) to hold the ASCII
codes returned by the Asc() function. As with the previous event handlers, the code exits the subroutine
immediately if the text box is empty, and the mandatory field situation is dealt with elsewhere.

If the user has entered a password, the next part of the code checks whether or not the six character minimum
requirement has been met. If not, a message is displayed telling the user that the password is invalid, the text
box is cleared, and the focus is restored to the text box.

The final part of the code (which executes if the password is six characters or more in length) loops through
each character in the password to check that it is either an upper or lower case letter or a number. If not, the
user will see a message telling them that the password is invalid, the text box is cleared, and the focus is
restored to the text box.

Since the sole requirement of the second password text box is that the contents should be identical to the first,
the code is somewhat simpler.

11. Switch to the code editor window, select the txtPassword02 control using the drop-down list at the
top centre of the code editor window, then select the LostFocus event using the drop-down list at the
top right-hand side of the Code Editor window.
12. In the body of the LostFocus event handler, enter the following code:

If Len(txtPassword02.Text) = 0 Then
Exit Sub

135
ElseIf txtPassword02.Text <> txtPassword01.Text Then
MsgBox("Passwords do not match - please re-enter.")
txtPassword01.Clear()
txtPassword02.Clear()
txtPassword01.Focus()
End If

The event handler here simply compares the two password strings to see if they match (note that the
comparison is case sensitive). If they do not, a message is displayed informing the user of this fact, both
password boxes are cleared, and the focus reverts to the first password box.

The next piece of validation code must check to see whether the user has input their date of birth in the correct
format:

dd/mm/yyyy.

13. Switch to the code editor window, select the txtDOB control using the drop-down list at the top centre
of the code editor window, then select the LostFocus event using the drop-down list at the top right-
hand side of the code editor window.
14. In the body of the LostFocus event handler, enter the following code:

If Len(txtDOB.Text) = 0 Then
Exit Sub
ElseIf Len(txtDOB.Text) < 10 Then
MsgBox("You have entered an invalid or incorrectly formatted date.")
txtDOB.Focus()
ElseIf IsDate(txtDOB.Text) = False Or txtDOB.Text(2) <> "/" Or txtDOB.Text
(5) <> "/" Then
MsgBox("You have entered an invalid or incorrectly formatted date.")
txtDOB.Focus()
ElseIf DateTime.Parse(txtDOB.Text) > Now.Date Or DateTime.Parse(txtDOB.Text)
_
<DateTime.Parse("01/01/1900") Then
MsgBox("The date you have entered is outside the acceptable range.")
txtDOB.Focus()
End If

136
As before, the validation here is not concerned with the absence of data, so an empty text box causes the code
to exit the subroutine. If data has been entered, the code checks to see if that the correct number of characters
has been used. If not, the user sees a message telling them the date is invalid or incorrectly formatted, the
focus is returned to the D.O.B. text box, and the subroutine ends.

If the correct number of characters has been entered, the code will check to see if the characters entered
represent a valid date using the isDate() function. It also checks to see if the the specified separator ("/") has
been used between day (dd), month (mm) and year (yyyy). If the input is not a valid date, or is not in the
required format, the user is advised that the date is invalid or incorrectly formatted, the focus is returned to the
D.O.B. text box, and the subroutine ends.

The final piece of validation checks to make sure that the dates fall between the beginning of the twentieth
century and the current date (in the real world, the range of acceptable dates would probably be more tightly
constrained, but this example serves to demonstrate how it might work).

The final field on the form requires the user to answer a simple addition question to establish that they are not
a robotic program seeking to gain access to the web server via the online form. The question itself will be
randomly generated when the form loads, and will consist of an addition involving two single-digit numbers in
the range 1 to 9.

The code (see below) uses the Randomize() function to initialise Visual Basic's random number generator.
The Rnd() function generates random values between 0 and 1. The code generates a random number
between 0 and 9 by multiplying the randomly generated value by 8 and adding 1 to the result, rounding up or
down to get the nearest whole number. This guarantees that only numbers between 1 and 9 will be generated.

15. Enter the following statement within the body of the form's class definition, before any subroutines:

Public x, y As Integer

16. In the form design window, double-click on a blank part of the form and in the body of the
form's Load event handler, enter the following code:

Randomize()
x = Rnd() * 8 + 1
y = Rnd() * 8 + 1
lblSum.Text = x & " + " & y & " ="

17. When the user enters their answer, the validation code must make sure that the answer is correct.
Select the txtSum control using the drop-down list at the top centre of the code editor window, then
137
select the LostFocus event using the drop-down list at the top right-hand side of the code editor
window.
18. In the body of the LostFocus event handler, enter the following code:

If Len(txtSum.Text) = 0 Then
Exit Sub
ElseIf(IsNumeric(txtSum.Text)) = False Then
MsgBox("You must enter a valid number.")
ElseIf CInt(txtSum.Text) <> (x + y) Then
MsgBox("The answer you have given is incorrect.")
Else
Exit Sub
End If
txtSum.Clear()
txtSum.Focus()

The event handler code above is fairly self explanatory. If the user enters an incorrect answer to the simple
addition (or just complete garbage!) , the code displays an error message, clears the user's answer, and
returns the cursor to the text box.

The code we have written so far covers various kinds of user input error that can occur for different input
boxes. We still have to deal with the general case of required fields not being completed at all, and we will do
this when the user clicks on the Submit button.

19. In the form design window, double-click on the Submit button and enter the following code at the
cursor:

If cmbTitle.Text = "" Then


MsgBox("You have not completed the Title field. This is a mandatory
field.")
cmbTitle.Focus()
ElseIf txtLastName.Text = "" Then
MsgBox("You have not entered your last name. This is a mandatory field.")
txtLastName.Focus()
ElseIf txtFirstName.Text = "" Then
MsgBox("You have not entered your first name(s). This is a mandatory
field.")
txtFirstName.Focus()

138
ElseIf txtAddress01.Text = "" Then
MsgBox("Please enter the first line of your address. This is a mandatory
field.")
txtAddress01.Focus()
ElseIf txtTown.Text = "" Then
MsgBox("Please enter your town/city. This is a mandatory field.")
txtTown.Focus()
ElseIf txtPostcode.Text = "" Then
MsgBox("Please enter your postal or zip code. This is a mandatory field.")
txtPostcode.Focus()
ElseIf txtCountry.Text = "" Then
MsgBox("Please enter your country name. This is a mandatory field.")
txtCountry.Focus()
ElseIf txtTelephone.Text = "" Then
MsgBox("Please enter your telephone number. This is a mandatory field.")
txtTelephone.Focus()
ElseIf txtEmail.Text = "" Then
MsgBox("Please enter your e-mail address. This is a mandatory field.")
txtEmail.Focus()
ElseIf txtPassword01.Text = "" Then
MsgBox("Please enter a password. This is a mandatory field.")
txtPassword01.Focus()
ElseIf txtPassword02.Text = "" Then
MsgBox("Please confirm your password. This is a mandatory field.")
txtPassword02.Focus()
ElseIf txtDOB.Text = "" Then
MsgBox("Please enter your date of birth. This is a mandatory field.")
txtDOB.Focus()
ElseIf txtSum.Text = "" Then
MsgBox("Please answer the simple question to verify that you are not a
robot.")
txtSum.Focus()
Else
MsgBox("You have successfully submitted your details!")
End If

139
The Submit button's Click event handler essentially checks each input box in turn to check that it is not empty.
If one of the mandatory fields is found to be empty, the user is asked to complete it.

The validation code provided for our online form is not particularly sophisticated and could certainly be
improved upon. It does however manage to catch some of the more obvious input errors as they occur, rather
than allowing the user to complete the form and submit the form data to a server-side script. It would be
frustrating for the user to have the script return an error message after having unsuccessfully tried to process
the data, and possibly have to start the entire process all over again.

Although we have used a web form as our example here, it is good practice to build validation into any kind of
application that requires input from a user, or from a file. One of the best known clichés related to computing is
"Garbage in, garbage out!". Taking reasonable precautions to prevent invalid data from being input to the
program in the first place greatly reduces the chances of the program producing invalid output.

The Pizza Delivery Project

The pizza delivery project has often been the subject of student programming assignments because it offers
opportunities to explore a diverse range of controls, use program loops, and even incorporate some database
programming and print routines. And, although the application is not intended for commercial use, there is
certainly scope for further development - I have seen similar applications offered on the Internet, perhaps
written by former students!

1. Open a new project called "PizzaDelivery". Save your project immediately to create the program folder.
2. Download the file pizza_delivery.zip using the link below, unzip the file, and copy or save the contents
into your project folder's /bin/Debug subdirectory.
pizza_delivery.zip.
3. Create an interface like the one illustrated below. Note that you will also need
two PrintDocumentcontrols (see below for details). The zipped file pizza_delivery.zip contains the
database and program icon for the application, which should now be in your
project's \bin\Debug\ subfolder.

140
The PizzaDelivery program interface

4. Set the properties of the controls as shown in the table below (properties not shown should be left at
their default values). Note: if a control appears within a panel, it should be created as a child of the panel
control (make sure the panel is selected before creating each child control). The locations given for
these controls are relative to the top left-hand corner of the panel control.

PizzaDelivery Form Controls

Control Name Additional Properties

Form frmPizzaDelivery BackColor: Dark Orange


Icon: pizza.ico
Size: 775, 600
Text: "Pete's Pizza Delivery"

141
PizzaDelivery Form Controls

Control Name Additional Properties

Label lblHeader Font: Arial Rounded MT Bold, 14pt


ForeColor: White
Location: 10, 10
Text: "Pete's Pizza Delivery - Order Form"
Label lblTelephone Autosize: False
ForeColor: White
Location: 472, 13
Size: 66, 20
Text: "Telephone:"
TextAlign: MiddleRight
TextBox txtTel Location: 544, 14
Size: 120, 20
Button cmdContinue Location: 670, 12
Text: "Continue"
Panel pnlLeft BackColor: Yellow
BorderStyle: FixedSingle
Enabled: False
Location: 10, 50
Size: 325, 500
Label lblSurname Autosize: False
Location: 10, 10
Size: 66, 20
Text: "Surname:"
TextAlign: MiddleRight
Label lblForename Autosize: False
Location: 10, 35
Size: 66, 20
Text: "Forename:"
TextAlign: MiddleRight
Label lblAddress Autosize: False
Location: 10, 60
Size: 66, 20
Text: "Address:"
TextAlign: MiddleRight
Label lblTown Autosize: False
Location: 10, 110
Size: 66, 20
Text: "Town:"
TextAlign: MiddleRight
Label lblPostcode Autosize: False
Location: 10, 135
Size: 66, 20

142
PizzaDelivery Form Controls

Control Name Additional Properties

Text: "Postcode:"
TextAlign: MiddleRight
Label lblOrderDetails Autosize: False
Location: 10, 175
Size: 100, 20
Text: "Order details:"
TextAlign: MiddleLeft
Button cmdSave Enabled: False
Location: 216, 10
Size: 94, 23
Text: "Save Customer"
TextBox txtSurname Location: 80, 11
Size: 120, 20
TextBox txtForename Location: 80, 36
Size: 120, 20
TextBox txtAddress01 Location: 80, 61
Size: 230, 20
TextBox txtAddress02 Location: 80, 86
Size: 230, 20
TextBox txtTown Location: 80, 111
Size: 120, 20
TextBox txtPostcode Location: 80, 136
Size: 60, 20
TextBox txtOrder Location: 13, 200
Multiline: True
ScrollBars: Vertical
Size: 300, 280
Panel pnlRight BackColor: Yellow
BorderStyle: FixedSingle
Enabled: False
Location: 345, 50
Size: 400, 500
Panel pnlTopping BackColor: PapayaWhip
BorderStyle: FixedSingle
Location: 10, 10
Size: 120, 150
Label lblTopping Font: Microsoft Sans Serif, 10pt, style=Bold
Location: 26, 10
Text: "Topping"
RadioButton radTop00 Location: 15, 40
Text: "Margherita"

143
PizzaDelivery Form Controls

Control Name Additional Properties

RadioButton radTop01 Location: 15, 60


Text: "Four Seasons"
RadioButton radTop02 Location: 15, 80
Text: "Meat Feast"
Panel pnlBase BackColor: PapayaWhip
BorderStyle: FixedSingle
Location: 140, 10
Size: 120, 150
Label lblBase Font: Microsoft Sans Serif, 10pt, style=Bold
Location: 37, 10
Text: "Base"
RadioButton radBas00 Location: 15, 40
Text: "25 cm/10 in."
RadioButton radBas01 Location: 15, 60
Text: "30 cm/12 in."
RadioButton radBas02 Location: 15, 80
Text: "35 cm/14 in."
Panel pnlExtras BackColor: PapayaWhip
BorderStyle: FixedSingle
Location: 270, 10
Size: 120, 150
Label lblExtras Font: Microsoft Sans Serif, 10pt, style=Bold
Location: 34, 10
Text: "Extras"
CheckBox chkExt00 Location: 15, 40
Text: "Mushrooms"
CheckBox chkExt01 Location: 15, 60
Text: "Green Peppers"
CheckBox chkExt02 Location: 15, 80
Text: "Anchovies"
CheckBox chkExt03 Location: 15, 100
Text: "Cheese"
Button cmdAddItem Location: 140, 166
Size: 120, 23
Text: "Add Item"
Panel pnlDrinks BackColor: GreenYellow
BorderStyle: FixedSingle
Enabled: False
Location: 10, 200
Size: 250, 150

144
PizzaDelivery Form Controls

Control Name Additional Properties

Label lblDrinks Font: Microsoft Sans Serif, 10pt, style=Bold


Location: 10, 10
Text: "Drinks"
Label lblQty Font: Microsoft Sans Serif, 10pt, style=Bold
Location: 184, 10
Text: "Qty"
Label lblCola Location: 12, 41
Text: "Cola"
Label lblLemonade Location: 12, 66
Text: "Lemonade"
Label lblOrange Location: 12, 91
Text: "Orange"
Label lblWater Location: 12, 116
Text: "Mineral Water"
NumericUpDown nudDrk00 Autosize: False
Location: 187, 39
Size: 40, 20
NumericUpDown nudDrk01 Autosize: False
Location: 187, 64
Size: 40, 20
NumericUpDown nudDrk02 Autosize: False
Location: 187, 89
Size: 40, 20
NumericUpDown nudDrk03 Autosize: False
Location: 187, 114
Size: 40, 20
Button cmdClear Location: 10, 428
Size: 120, 23
Text: "Clear Order"
Button cmdPrint Location: 10, 457
Size: 120, 23
Text: "Print Delivery Note"
Label lblOrdVal Autosize: False
Location: 220, 404
Size; 100, 13
Text: "Order value:"
TextAlign: MiddleRight
Label lblDelChg Autosize: False
Location: 220, 429
Size; 100, 13
Text: "Delivery charge:"
TextAlign: MiddleRight
145
PizzaDelivery Form Controls

Control Name Additional Properties

Label lblOrdTot Autosize: False


Location: 220, 454
Size; 100, 13
Text: "Order total:"
TextAlign: MiddleRight
Label lblOrderValue Autosize: False
BackColor: White
BorderStyle: Fixed3D
Location: 326,400
Size: 60, 20
Text: none
TextAlign: MiddleRight
Label lblDeliveryCharge Autosize: False
BackColor: White
BorderStyle: Fixed3D
Location: 326, 425
Size: 60, 20
Text: noneTextAlign: MiddleRight
Label lblOrderTotal Autosize: False
BackColor: White
BorderStyle: Fixed3D
Location: 326, 450
Size: 60, 20
Text: none
TextAlign: MiddleRight
PrintDocument prtDeliveryNote -
PrintDocument prtContinuation -

The commented program code is reproduced in its entirety below. Hopefully the comments, together with the
explanatory notes that follow, will be sufficient to inform the reader how the code works. Most of the code is
relatively straightforward, and the program essentially brings together various elements that have been the
subject of other pages in this section.

The PizzaDelivery project code:

Public Class frmPizzaDelivery


'Declare database object variables
Dim con As New OleDb.OleDbConnection

146
Dim ds As New DataSet
Dim da As OleDb.OleDbDataAdapter
Dim sql As String
Dim cmd As OleDb.OleDbCommand

'Declare control arrays


Dim RadioArrayTopping(0 To 2) As RadioButton
Dim RadioArrayBase(0 To 2) As RadioButton
Dim CheckArrayExtras(0 To 3) As CheckBox

'Declare module variables


Dim Topping() As String = {"Margherita", "Four Seasons", "Meat Feast"}
Dim Base() As String = {"9""", "12""", "14"""}
Dim Extras() As String = {"Mushrooms", "Green Peppers", "Anchovies",
"Cheese"}
Dim Drinks() As String = {"Cola", "Lemonade", "Orange", "Mineral Water"}
Dim PizzaPrice(,) As Single = {{3.0, 4.0, 5.5}, {3.5, 4.5, 6.0}, {4.0, 5.0,
6.5}}
Dim ExtraPrice() As Single = {0.5, 0.4, 0.6, 0.5}
Dim DrinkPrice() As Single = {1.0, 1.0, 1.0, 0.9}
Dim itemNo As Integer = -1
Dim strOrder01 As String = ""
Dim strOrder02 As String = ""
Dim strDrinks As String = ""
Dim pizzaVal, drinkVal, orderVal, delivery, total As Single

'Structure to hold options for each pizza ordered


Structure pizzaStruct
Dim Topp As Integer
Dim Base As Integer
Dim Ex00 As Boolean
Dim Ex01 As Boolean
Dim Ex02 As Boolean
Dim Ex03 As Boolean
End Structure

'Structure to hold drink selection


Structure drinkStruct
147
Dim Drink00 As Integer
Dim Drink01 As Integer
Dim Drink02 As Integer
Dim Drink03 As Integer
End Structure

'Declare module structure variables


Dim pizzaArray(-1) As pizzaStruct
Dim drinkSelection As drinkStruct

Private Sub frmMain_Load(sender As Object, e As EventArgs) Handles


MyBase.Load
'Set up control arrays
RadioArrayTopping(0) = radTop00
RadioArrayTopping(1) = radTop01
RadioArrayTopping(2) = radTop02
RadioArrayBase(0) = radBas00
RadioArrayBase(1) = radBas01
RadioArrayBase(2) = radBas02
CheckArrayExtras(0) = chkExt00
CheckArrayExtras(1) = chkExt01
CheckArrayExtras(2) = chkExt02
CheckArrayExtras(3) = chkExt03
'Initialise order
clearOrder()
End Sub

Function checkTelNum() As Boolean


'Make sure telephone number is valid
Dim strAllowedChars As String = "0123456789() -+ "
If Len(txtTel.Text) < 5 Then
Return False
Else
For i = 0 To Len(txtTel.Text) - 1
If InStr(1, strAllowedChars, txtTel.Text(i)) = 0 Then
Return False
End If
148
Next
Return True
End If
End Function

Private Sub cmdContinue_Click(sender As Object, e As EventArgs) Handles


cmdContinue.Click
'Check for valid telephone number before proceeding
If checkTelNum() = False Then
MsgBox("You have not entered a valid telephone number.")
txtTel.Focus()
Exit Sub
End If
'If telephone number is valid, disable telephone number input
'and enable main form functionality
cmdContinue.Enabled = False
txtTel.Enabled = False
pnlLeft.Enabled = True
pnlRight.Enabled = True
enableCustomerInput()
'Open database and check for existing customer using telephone number
con.ConnectionString = "Provider=Microsoft.ACE.OLEDB.12.0; Data
Source=PizzaDelivery.accdb;"
con.ConnectionString &= " Persist Security Info=False;"
con.Open()
sql = "SELECT * FROM Customer WHERE TelephoneNo = '" & txtTel.Text & "'"
da = New OleDb.OleDbDataAdapter (sql, con)
da.Fill(ds, "CustInfo")
con.Close()
If ds.Tables("CustInfo").Rows.Count = 0 Then
'If customer telephone number not found, inform user
'and move focus to customer surname input box
MsgBox("Number not found in database.")
cmdSave.Enabled = True
txtSurname.Focus()
Exit Sub
Else
'Otherwise add customer details to form
149
txtTel.Text = ds.Tables("CustInfo").Rows(0).Item(0)
txtSurname.Text = ds.Tables("CustInfo").Rows(0).Item(1)
txtForename.Text = ds.Tables("CustInfo").Rows(0).Item(2)
txtAddress01.Text = ds.Tables("CustInfo").Rows(0).Item(3)
txtAddress02.Text = ds.Tables("CustInfo").Rows(0).Item(4)
txtTown.Text = ds.Tables("CustInfo").Rows(0).Item(5)
txtPostcode.Text = ds.Tables("CustInfo").Rows(0).Item(6)
'Protect customer details from changes
disableCustomerInput()
End If
End Sub

Function checkCustomer() As Boolean


'Make sure customer details are complete
If txtSurname.Text = "" Then
MsgBox("Please enter the customer's surname.")
Return False
ElseIf txtAddress01.Text = "" Then
MsgBox("Please enter the customer's address.")
Return False
ElseIf txtPostcode.Text = "" Then
MsgBox("Please enter the customer's postcode.")
Return False
Else
Return True
End If
End Function

Function addItem() As Boolean


'Declare local variables
Dim toppingSelected As Boolean = False
Dim baseSelected As Boolean = False
Dim top, bas As Integer
'Check that a base and a topping are selected and get choices
For i = 0 To 2
If RadioArrayTopping(i).Checked = True Then
toppingSelected = True
top = i
150
End If
If RadioArrayBase(i).Checked = True Then
baseSelected = True
bas = i
End If
Next
'If either base or topping not selected, prompt user
If toppingSelected = False Then
MsgBox("You have not selected a topping.")
Return False
End If
If baseSelected = False Then
MsgBox("You have not selected a base.")
Return False
Else
'Increment item number
itemNo += 1
'If this is first pizza item enable drinks panel
If itemNo = 0 Then pnlDrinks.Enabled = True
'Increase the size of the pizza item array dynamically
ReDim Preserve pizzaArray(itemNo)
'Save user choices
pizzaArray(itemNo).Topp = top
pizzaArray(itemNo).Base = bas
pizzaArray(itemNo).Ex00 = CheckArrayExtras(0).Checked
pizzaArray(itemNo).Ex01 = CheckArrayExtras(1).Checked
pizzaArray(itemNo).Ex02 = CheckArrayExtras(2).Checked
pizzaArray(itemNo).Ex03 = CheckArrayExtras(3).Checked
End If

pizzaVal = 0 'Reset pizza item value

For i = 0 To itemNo
'Calculate total value of pizzas currently selected
pizzaVal += PizzaPrice(pizzaArray(i).Topp, pizzaArray(i).Base)
If pizzaArray(itemNo).Ex00 = True Then pizzaVal += ExtraPrice(0)
If pizzaArray(itemNo).Ex01 = True Then pizzaVal += ExtraPrice(1)
If pizzaArray(itemNo).Ex02 = True Then pizzaVal += ExtraPrice(2)
151
If pizzaArray(itemNo).Ex03 = True Then pizzaVal += ExtraPrice(3)
Next

orderVal = pizzaVal + drinkVal 'Calculate order value

If orderVal >= 10.0 Then


'If order value is £10.00 or more delivery is free
delivery = 0.0
Else
'If order value is less than £10.00 delivery is £2.00
delivery = 2.0
End If

total = orderVal + delivery 'Calculate total to pay

'Update order display


txtOrder.Text = strOrder01
lblOrderValue.Text = Format(orderVal, "currency")
lblDeliveryCharge.Text = Format(delivery, "currency")
lblOrderTotal.Text = Format(total, "currency")
clearItem() 'clear pizza selection ready for next selection
If itemNo > 14 Then
'Number of items per order is limited to 15 - disable input of further
items
MsgBox("You have reached the maximum number of items for a single
order.")
cmdAddItem.Enabled = False
pnlTopping.Enabled = False
pnlBase.Enabled = False
pnlExtras.Enabled = False
End If
Return True 'Item was added to order
End Function

Function addDrinks()
'Get quantities of each drink selected
drinkSelection.Drink00 = nudDrk00.Value
drinkSelection.Drink01 = nudDrk01.Value
152
drinkSelection.Drink02 = nudDrk02.Value
drinkSelection.Drink03 = nudDrk03.Value

drinkVal = 0 'Initialise value of drinks to zero

'Calculate total value of drinks selected


drinkVal += DrinkPrice(0) * drinkSelection.Drink00
drinkVal += DrinkPrice(1) * drinkSelection.Drink01
drinkVal += DrinkPrice(2) * drinkSelection.Drink02
drinkVal += DrinkPrice(3) * drinkSelection.Drink03

orderVal = pizzaVal + drinkVal 'calculate order value

'Determine whether delivery charge should be applied


If orderVal >= 10.0 Then
delivery = 0.0
Else
delivery = 2.0
End If

total = orderVal + delivery 'calculate total to pay

'Update order details and clear current pizza item


txtOrder.Text = strOrder01
lblOrderValue.Text = Format(orderVal, "currency")
lblDeliveryCharge.Text = Format(delivery, "currency")
lblOrderTotal.Text = Format(total, "currency")
clearItem()
End Function

Sub writeOrder()
'Construct order string - if order has more than 7 items not including
drinks,
'create a second order string to enable printing of delivery note on two
pages
Dim limit As Integer
txtOrder.Text = ""
strOrder01 = ""
153
strOrder02 = ""
If itemNo > -1 Then
If itemNo > 6 Then
limit = 6 'Limit first page of delivery note to 7 items
Else
limit = itemNo
End If
For i = 0 To limit 'Create order string for first page of delivery note
strOrder01 &= "1 x "
strOrder01 &= Base(pizzaArray(i).Base) & " "
strOrder01 &= Topping(pizzaArray(i).Topp)
If(pizzaArray(i).Ex00 = False And pizzaArray(i).Ex01 = False And _
pizzaArray(i).Ex02 = False And pizzaArray(i).Ex03 = False) Then
strOrder01 &= ", no extras."
Else
strOrder01 &= ", with extra: "
End If
If pizzaArray(i).Ex00 = True Then strOrder01 &= vbNewLine & " " &
Extras(0)
If pizzaArray(i).Ex01 = True Then strOrder01 &= vbNewLine & " " &
Extras(1)
If pizzaArray(i).Ex02 = True Then strOrder01 &= vbNewLine & " " &
Extras(2)
If pizzaArray(i).Ex03 = True Then strOrder01 &= vbNewLine & " " &
Extras(3)
strOrder01 &= vbNewLine & vbNewLine
Next
If itemNo > 6 Then 'Create order string for second page of delivery note
For i = 7 To itemNo
strOrder02 &= "1 x "
strOrder02 &= Base(pizzaArray(i).Base) & " "
strOrder02 &= Topping(pizzaArray(i).Topp)
If(pizzaArray(i).Ex00 = False And pizzaArray(i).Ex01 = False And _
pizzaArray(i).Ex02 = False And pizzaArray(i).Ex03 = False) Then
strOrder02 &= ", no extras."
Else
strOrder02 &= ", with extra: "
End If
154
If pizzaArray(i).Ex00 = True Then strOrder02 &= vbNewLine & " " &
Extras(0)
If pizzaArray(i).Ex01 = True Then strOrder02 &= vbNewLine & " " &
Extras(1)
If pizzaArray(i).Ex02 = True Then strOrder02 &= vbNewLine & " " &
Extras(2)
If pizzaArray(i).Ex03 = True Then strOrder02 &= vbNewLine & " " &
Extras(3)
strOrder02 &= vbNewLine & vbNewLine
Next
End If
End If
writeDrinks() 'Call procedure to create list of drinks ordered
txtOrder.Text = strOrder01 & strOrder02 & strDrinks 'Concatenate strings
for screen output
'Update display values for goods value, delivery charge and total order
value
lblOrderValue.Text = Format(orderVal, "currency")
lblDeliveryCharge.Text = Format(delivery, "currency")
lblOrderTotal.Text = Format(total, "currency")
End Sub

Sub writeDrinks()
'Construct list of drinks and quantities ordered (appended to order
string)
If drinkSelection.Drink00 = 0 And drinkSelection.Drink01 = 0 And _
drinkSelection.Drink02 = 0 And drinkSelection.Drink03 = 0 Then
Exit Sub
End If
strDrinks = "Drinks:" & vbNewLine
If drinkSelection.Drink00 > 0 Then
strDrinks &= " " & drinkSelection.Drink00 & " x " & Drinks(0) & vbNewLine
End If
If drinkSelection.Drink01 > 0 Then
strDrinks &= " " & drinkSelection.Drink01 & " x " & Drinks(1) & vbNewLine
End If
If drinkSelection.Drink02 > 0 Then
strDrinks &= " " & drinkSelection.Drink02 & " x " & Drinks(2) & vbNewLine
155
End If
If drinkSelection.Drink03 > 0 Then
strDrinks &= " " & drinkSelection.Drink03 & " x " & Drinks(3) & vbNewLine
End If
strDrinks &= vbNewLine & vbNewLine
End Sub

Private Sub cmdAdd_Click(sender As Object, e As EventArgs) Handles


cmdAddItem.Click
'If user has selected a pizza base and topping, add item to order and
update order strings
If addItem() = False Then
Exit Sub
Else
writeOrder()
End If
End Sub

Private Sub cmdClear_Click(sender As Object, e As EventArgs) Handles


cmdClear.Click
'Clear the current order to cancel, or in readiness for next order
clearOrder()
cmdContinue.Enabled = True
txtTel.Enabled = True
pnlLeft.Enabled = False
pnlRight.Enabled = False
End Sub

Sub clearOrder()
'Reset program variables and restore display to initial state
ds.Clear()
nudDrk00.Value = 0
nudDrk01.Value = 0
nudDrk02.Value = 0
nudDrk03.Value = 0
txtTel.Clear()
txtSurname.Clear()
txtForename.Clear()
156
txtAddress01.Clear()
txtAddress02.Clear()
txtTown.Clear()
txtPostcode.Clear()
strOrder01 = ""
txtOrder.Text = ""
lblOrderValue.Text = ""
lblDeliveryCharge.Text = ""
lblOrderTotal.Text = ""
itemNo = -1
orderVal = 0
delivery = 0
total = 0
enablePizzaInput()
clearItem()
End Sub

Sub disableCustomerInput()
'Prevent customer details being accidentally changed
cmdSave.Enabled = False
txtSurname.Enabled = False
txtForename.Enabled = False
txtAddress01.Enabled = False
txtAddress02.Enabled = False
txtTown.Enabled = False
txtPostcode.Enabled = False
End Sub

Sub enableCustomerInput()
'Allow new customer details to be entered
cmdSave.Enabled = True
txtSurname.Enabled = True
txtForename.Enabled = True
txtAddress01.Enabled = True
txtAddress02.Enabled = True
txtTown.Enabled = True
txtPostcode.Enabled = True
End Sub
157
Sub enablePizzaInput()
'Allow further pizza selections to be made
cmdAddItem.Enabled = True
pnlTopping.Enabled = True
pnlBase.Enabled = True
pnlExtras.Enabled = True
End Sub

Sub clearItem()
'Clear current pizza selection
For i = 0 To 2
RadioArrayTopping(i).Checked = False
RadioArrayBase(i).Checked = False
Next
For i = 0 To 3
CheckArrayExtras(i).Checked = False
Next
End Sub

'update order details and program display if the drink selection changes
Private Sub nudDrk00_ValueChanged(sender As Object, e As EventArgs) Handles
nudDrk00.ValueChanged
addDrinks()
writeOrder()
End Sub

Private Sub nudDrk01_ValueChanged(sender As Object, e As EventArgs) Handles


nudDrk01.ValueChanged
addDrinks()
writeOrder()
End Sub

Private Sub nudDrk02_ValueChanged(sender As Object, e As EventArgs) Handles


nudDrk02.ValueChanged
addDrinks()
writeOrder()
End Sub
158
Private Sub nudDrk03_ValueChanged(sender As Object, e As EventArgs) Handles
nudDrk03.ValueChanged
addDrinks()
writeOrder()
End Sub

Private Sub cmdSave_Click(sender As Object, e As EventArgs) Handles


cmdSave.Click
'Save new customer details to database
Dim custString As String
Dim count As Integer = 0

If checkCustomer() = False Then Exit Sub

custString = "'" & Replace (txtTel.Text, "'", "''") & "', '" & Replace
(txtSurname.Text, "'", "''") _
& "', '" & Replace (txtForename.Text, "'", "''") & "', '" & Replace
(txtAddress01.Text, "'", "''") _
& "', '" & Replace (txtAddress02.Text, "'", "''") & "', '" & Replace
(txtTown.Text, "'", "''") _
& "', '" & Replace (txtPostcode.Text, "'", "''") & "'"
con.Open()

sql = "INSERT into Customer values (" & custString & ") "
cmd = New OleDb.OleDbCommand (sql, con)
count = cmd.ExecuteNonQuery
If count > 0 Then
MsgBox("Customer record created.")
disableCustomerInput()
End If
con.Close()
End Sub

Private Sub cmdPrint_Click(sender As Object, e As EventArgs) Handles


cmdPrint.Click
If checkCustomer() = False Then Exit Sub
prtDeliveryNote.Print()
159
If itemNo > 6 Then
prtContinuation.Print()
End If
End Sub

Private Sub prtDeliveryNote_PrintPage(sender As Object, e As


Drawing.Printing.PrintPageEventArgs) _
Handles prtDeliveryNote.PrintPage
'Declare variables for delivery note print procedure (fonts, brushes, print
coordinates etc.)
Dim headerFont As Font = New Font("Arial", 24, GraphicsUnit.Point)
Dim detailFontBold As Font = New Font("Arial", 10, FontStyle.Bold,
GraphicsUnit.Point)
Dim detailFont As Font = New Font("Arial", 10, GraphicsUnit.Point)
Dim headerBrush As Brush = Brushes.Blue
Dim detailBrush As Brush = Brushes.Black
Dim x As Single
Dim y As Single
Dim strOutput As String

e.Graphics.PageUnit = GraphicsUnit.Inch 'Set print units

'Most of the code below is setting up parameters for printing various


'parts of the delivery note and then calling the e.Graphics.DrawString()
'method to send them to the printer
strOutput = "Pete's Pizza Delivery Service"
x = (e.MarginBounds.Left / e.Graphics.DpiX) + 0.5
y = (e.MarginBounds.Top / e.Graphics.DpiY) + 0.5
e.Graphics.DrawString(strOutput, headerFont, headerBrush, x, y)
y += 1
strOutput = "Delivery address:"
e.Graphics.DrawString(strOutput, detailFontBold, detailBrush, x, y)
x += 1.5
strOutput = txtForename.Text & " " & txtSurname.Text
e.Graphics.DrawString(strOutput, detailFont, detailBrush, x, y)
strOutput = txtAddress01.Text
y += (e.Graphics.MeasureString(strOutput, detailFont).Height)
e.Graphics.DrawString(strOutput, detailFont, detailBrush, x, y)
160
If txtAddress02.Text <> "" Then
strOutput = txtAddress02.Text
y += (e.Graphics.MeasureString(strOutput, detailFont).Height)
e.Graphics.DrawString(strOutput, detailFont, detailBrush, x, y)
End If
strOutput = txtTown.Text & " " & txtPostcode.Text
y += (e.Graphics.MeasureString(strOutput, detailFont).Height)
e.Graphics.DrawString(strOutput, detailFont, detailBrush, x, y)
y += 0.5
x -= 1.5
strOutput = "Telephone:"
e.Graphics.DrawString(strOutput, detailFontBold, detailBrush, x, y)
x += 1.5
strOutput = txtTel.Text
e.Graphics.DrawString(strOutput, detailFont, detailBrush, x, y)
y += 0.5
x -= 1.5
strOutput = "Order:"
e.Graphics.DrawString(strOutput, detailFontBold, detailBrush, x, y)
x += 1.5
If itemNo <= 6 Then
strOutput = strOrder01 & strDrinks
Else
strOutput = strOrder01 & vbNewLine & vbNewLine & "continued ..."
End If
e.Graphics.DrawString(strOutput, detailFont, detailBrush, x, y)
y = 9
x = 5.5
strOutput = "Order value:"
e.Graphics.DrawString(strOutput, detailFontBold, detailBrush, x, y)
x += 1.5
strOutput = "£" & CStr(Format(orderVal, "Fixed")).PadLeft(4)
e.Graphics.DrawString(strOutput, detailFont, detailBrush, x, y)
y += 0.5
x -= 1.5
strOutput = "Delivery:"
e.Graphics.DrawString(strOutput, detailFontBold, detailBrush, x, y)
x += 1.5
161
strOutput = "£" & CStr(Format(delivery, "Fixed")).PadLeft(4)
e.Graphics.DrawString(strOutput, detailFont, detailBrush, x, y)
y += 0.5
x -= 1.5
strOutput = "Total to pay:"
e.Graphics.DrawString(strOutput, detailFontBold, detailBrush, x, y)
x += 1.5
strOutput = "£" & CStr(Format(total, "Fixed")).PadLeft(4)
e.Graphics.DrawString(strOutput, detailFontBold, detailBrush, x, y)
'Return False when no more pages....
e.HasMorePages = False
End Sub

Private Sub prtContinuation_PrintPage(sender As Object, e As


Drawing.Printing.PrintPageEventArgs) _
Handles prtContinuation.PrintPage
'Print continuation page if more than 7 items ordered (not including
drinks)
Dim headerFont As Font = New Font("Arial", 24, GraphicsUnit.Point)
Dim detailFontBold As Font = New Font("Arial", 10, FontStyle.Bold,
GraphicsUnit.Point)
Dim detailFont As Font = New Font("Arial", 10, GraphicsUnit.Point)
Dim headerBrush As Brush = Brushes.Blue
Dim detailBrush As Brush = Brushes.Black
Dim x As Single
Dim y As Single
Dim strOutput As String

e.Graphics.PageUnit = GraphicsUnit.Inch
strOutput = "Pete's Pizza Delivery Service"
x = (e.MarginBounds.Left / e.Graphics.DpiX) + 0.5
y = (e.MarginBounds.Top / e.Graphics.DpiY) + 0.5
e.Graphics.DrawString(strOutput, headerFont, headerBrush, x, y)
y += 1
strOutput = "Order continued:"
e.Graphics.DrawString(strOutput, detailFontBold, detailBrush, x, y)
x += 1.5
strOutput = strOrder02 & strDrinks
162
e.Graphics.DrawString(strOutput, detailFont, detailBrush, x, y)
e.HasMorePages = False
End Sub
End Class

General Notes

This is probably one of the longest pieces of coding featured on these pages to date, which reflects the relative
complexity of the program and the large number of controls used. When the program starts, the only active
control is the text box that allows the user to enter a customer's telephone number. This hopefully prevents any
duplication of effort by making sure that, if the customer details are already stored in the database, they will
automatically be displayed in the left-hand panel of the application. If this is the case, the customer information
fields are populated using the stored details, and the fields are disabled to prevent inadvertent changes.

If the caller is a new customer, the user is informed, and may enter the new details in the text fields provided.
They can also add the new customer details to the database at any point before the order is cleared, so long
as the information entered is complete. Once the customer details have been saved, the input boxes will be
disabled to prevent further changes.

Note that the application does not currently include an administrative function to allow customer data to be
deleted or updated - this is a program feature that could be added (there are no doubt many improvements that
could be made!).

Radio buttons are used to allow the user to select exactly one topping and one base per item, and a pizza
cannot be added to the order until both have been selected. These parameters must be specified for each
pizza item ordered, and will determine the pizza's basic price. The extras are of course optional (by definition) ,
so if the user does not select any extras the item can still be added to the order.

Because each customer may want a selection of extras (or even all four options), the choices are determined
using check boxes. This allows the user to select any of the four extras, regardless of whether or not other
extras have already been selected.

163
Drinks can be added to the order at any time, and are dealt with separately from the pizza items (although note
that until at least one pizza item has been added to the order, no drinks may be selected). Drinks are selected
using the NumericUpDown control.

The controls for each type of selection are grouped on their own panel, allowing the selection for drinks (for
example) to be enabled or disabled independently of the other controls. It is also important for the radio button
controls, since it creates a control group in which only one radio button may be checked at any one time;
selecting a different radio button will de-select any previously selected radio button automatically.

The other point of interest concerning the controls is that each group of controls has been set up as a control
array with similar characteristics to the control arrays found in Visual Basic 6, allowing them to be accessed
and manipulated using program loops. The declarations and initialisation statements for the control arrays can
be seen at the top of the form's class definition.

Another interesting feature of the code is the use of a structure to group together the variables that store the
pizza selection for each pizza item. The choice of topping and base are both stored as integers, while the
extras (which are either selected or not selected) are stored as Boolean variables.

The order details displayed to the user are updated in real time as pizza items or drinks are added to the order,
including the order value, delivery charge (if applicable) and order total. One fairly obvious shortcoming with
the programming as it stands is that, once added to the order, a pizza item cannot be deleted, which means
that if the customer changes their mind half way through an order the user must clear the order and start again.
Thanks to the fact that each pizza item is stored in a structure, this shortcoming could be remedied relatively
easily (I leave this to those of you with the enthusiasm to develop the program further!). The selection of drinks
can be changed dynamically, without requiring the user to start again from scratch.

The only feature of the program that introduces anything radically new to these pages is the ability to print a
delivery note (which is after all very important for a pizza delivery program!) The code I have used here is
perhaps not the most elegant solution, but it appears to work in the (admittedly limited) tests that I have carried
out.

The program has deliberately been limited to 15 pizza items in order to reduce the complexity of the print
procedure, which allows for up to seven pizza items (plus drinks) to be printed on a single A4 delivery note.
More items (up to fifteen in total not including drinks) can be ordered, and the additional items will be printed on
a continuation sheet.

164
The subject of printing with Visual Basic seems to be the cause of some consternation for some student
programmers. Hopefully the code provided here, while it may not be the most efficient solution, will at least
provide a starting point for those with similar programming problems to solve.

On a final note, the colour scheme chosen by me is purely arbitrary and will probably give those of you that
have studied graphic design nightmares. All I can say is that it seemed like a good idea at the time, and I don't
really have much incentive at the present time to go back and redesign the interface. My primary interest, after
all, is in the code and making it work. I am under no illusions about my prowess (or lack thereof) as a graphic
designer, and gladly defer to those better qualified in this respect.

The Pong Game

Elsewhere in this section we have used a picture box to display an image file. We can also use images
somewhat more dynamically by making them move around in a form. We can change the location of a picture
box (or any other visible control) by changing its Location property at run time (i.e. programmatically, rather
than in the form design window).

The top left corner of an application's main form has the x, y coordinates 0, 0. A control is positioned on the
form programmatically by setting its x and y coordinates relative to this point. The x coordinate determines the
horizontal distance between the left-hand side of the form and the left-hand side of the control, while
the y coordinate similarly determines the vertical distance between the top of the form and the top of the
control.

The x and y coordinates of a control are stored as pixel values in the control's Location property. These
values can be changed programatically, allowing us to move the control around on the screen. To demonstrate
the kind of thig this allows us to do, we are going to create our own Visual Basic version of a very old arcade
game - one of the first arcade games ever, in fact!

1. Open a new project called "Pong" and save it to create the project folder for the application.
2. We have created a zipped file called pong.zip that contains the program icon and playing area
background image for the application. Download the file from the link below and extract the contents to
the project directory.
Download the pong.zip file here.
3. Create an interface like the one illustrated below (note that you will three Timer controls).

165
The Pong program interface

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

Pong Form Controls

Control Name Additional Properties

Form frmPong BackColor: Black


Icon: pong.ico
Size: 680, 580
Text: "Pong"

166
Pong Form Controls

Control Name Additional Properties

PictureBox picPongTable Image: pong_table.bmp


Location: 12, 12
Sze: 640, 480
Label lblBall AutoSize: False
BackColor: White
Location: 324, 244
Size: 16, 16
Text: None
Visible: False
Label lblPaddleLeft AutoSize: False
BackColor: White
Location: 23, 212
Size: 16, 80
Text: None
Label lblPaddleRight AutoSize: False
BackColor: White
Location: 625, 212
Size: 16, 80
Text: None
Label lblLeftScore BackColor: Black
ForeColor: White
Font: Microsoft Sans Serif 24pt Regular
Location: 210, 40
Text: "0"
Label lblRightScore BackColor: Black
ForeColor: White
Font: Microsoft Sans Serif 24pt Regular
Location: 420, 40
Text: "0"
Button cmdPlay Location: 334, 507
Size: 75, 23
Text: "Play"
Button cmdPause Location: 415, 507
Size: 75, 23
Text: "Pause"
Button cmdReset Location: 496, 507
Size: 75, 23
Text: "Reset"
Button cmdExit ForeColor: Red
Location: 577, 507
Size: 75, 23
Text: "Exit"
Timer tmrBall Enabled: False
Interval: 10

167
Pong Form Controls

Control Name Additional Properties

Timer tmrPaddle Enabled: False


Interval: 10
Timer tmrBreak Enabled: False
Interval: 3000

The program in its entirety is presented at the bottom of this page. Hopefully, the code is sufficiently-well
commented to enable the reader to figure out how the program works. The code is fairly generic, so it should
be possible to make modifications to the implementation with only minor changes to the code.

In this single-player version of the game, the computer operates the left-hand paddle according to the program
logic provided. It attempts to vertically align its paddle with the ball when the ball enters the left-hand half of the
court travelling from right to left. The player operates the right-hand paddle, and always serves first; after that,
whoever wins the point gets to serve.

The angle of serve is randomly selected by the program each time play restarts (the code generates a random
value for the ball's y vector). The angle at which the ball leaves the computer's paddle is similarly randomised.

The player can exercise some control over the angle at which they return the ball, which depends on which
part of the paddle makes contact with the ball. Contact with the top of the paddle will cause the ball to rise
steeply away from the paddle, while contact with the bottom of the paddle will cause it to leave the paddle in a
downward direction. Contact with the mid-section of the paddle plays the ball back down the court horizontally,
and there are intermediate return angles for other sections of the paddle.

The object of the game is to get the ball past the computer's paddle to score points. The player scores one
point each time they get the ball past the computer's paddle. If the computer gets the ball past the player's
paddle, the computer gets a point. The first player to score eleven points wins (scores are displayed at the top
of the playing area).

Using the program


 Click on the Start button to begin the game.

168
 Click on the Pause button to pause the game (or Resume to continue) .
 Click on the Reset button to reset the game to its initial state.
 Click on the Exit button to close the program immediately.
 Move the paddle up or down by holding down the left (up) or right (down) mouse button.

The code

Public Class frmPong


Dim vx, vy As Single 'x and y vectors for ball
Dim paddleZone As Integer 'section of paddle making contact with ball
Dim winScore As Integer = 11 'score that must be attained to win game
Dim scoreLeft, scoreRight, lastPoint As Integer 'game scores and last player
to score
Dim upFlag As Boolean = False 'True when left mouse button held down
Dim dnFlag As Boolean = False 'True when right mouse button held down
Dim breakCount As Integer = 0 'counter for tmrBreak clock ticks
Dim intLCourt, intRcourt, intTCourt, intBCourt, intCCourt As Integer
'left, right, top and bottom boundaries of court
Dim intBallW, intBallH As Integer 'ball width and height
Dim intPadLH, intPadRH As Integer 'left and right paddle heights
Dim intPadLTop, intPadRTop As Integer 'initial y coordinate for top of
paddles
Dim intPadFaceL, intPadFaceR 'x coordinates for contact surfaces of left and
right paddles

Private Sub frmPong_Load(sender As Object, ByVal e As EventArgs) _


Handles MyBase.Load
'initialise global variables
intLCourt = picPongTable.Left + 5
intRcourt = picPongTable.Left + picPongTable.Width - 5
intTCourt = picPongTable.Top + 5
intBCourt = picPongTable.Top + picPongTable.Height - 5
intCCourt = picPongTable.Left + picPongTable.Width / 2
intBallW = lblBall.Width
intBallH = lblBall.Height
intPadLH = lblPaddleLeft.Height
intPadRH = lblPaddleRight.Height

169
intPadLTop = picPongTable.Top + picPongTable.Height / 2 -
lblPaddleLeft.Height / 2
intPadRTop = picPongTable.Top + picPongTable.Height / 2 -
lblPaddleRight.Height / 2
intPadFaceL = lblPaddleLeft.Left + lblPaddleLeft.Width
intPadFaceR = lblPaddleRight.Left
End Sub

Private Sub tmrPaddle_Tick(sender As Object, ByVal e As EventArgs) _


Handles tmrPaddle.Tick
If lblBall.Left < (intCCourt - intBallW / 2) And vx < 0 Then
'ball is in left court and moving towards left paddle
If(lblBall.Top + intBallH / 2) >(lblPaddleLeft.Top + intPadLH / 2) Then
'ball is below centre of left paddle
If(lblPaddleLeft.Top + intPadLH) < intBCourt Then
'paddle is not yet at at bottom of court
lblPaddleLeft.Top += 4 'move left paddle down 4 pixels
End If
ElseIf(lblBall.Top + intBallH / 2) < (lblPaddleLeft.Top + intPadLH / 2)
Then
'ball is above centre of left paddle
If lblPaddleLeft.Top > intTCourt Then 'paddle is not yet at top of
court
lblPaddleLeft.Top -= 4 'move left paddle up 4 pixels
End If
End If
Else 'ball is not in left court or is moving away from left paddle
If lblPaddleLeft.Top < intPadLTop Then
'left paddle is above initial start position
lblPaddleLeft.Top += 1 'move paddle down by 1 pixel
ElseIf lblPaddleLeft.Top > intPadLTop Then
'left paddle is below initial start position
lblPaddleLeft.Top -= 1 'move paddle up by 1 pixel
End If
End If
'make sure left paddle is between top and bottom borders of court
If lblPaddleLeft.Top < intTCourt Then
lblPaddleLeft.Top = intTCourt
170
ElseIf lblPaddleLeft.Top >(intBCourt - intPadLH) Then
lblPaddleLeft.Top =(intBCourt - intPadLH)
End If

If upFlag = True And lblPaddleRight.Top > intTCourt Then


'left mouse button is down and left paddle is not at top of court
lblPaddleRight.Top -= 4 'move right paddle up 4 pixels
ElseIf dnFlag = True And lblPaddleRight.Top < (intBCourt - intPadRH) Then
'right mouse button is down and left paddle is not at bottom of court
lblPaddleRight.Top += 4 'move right paddle down 4 pixels
End If
'make sure right paddle is between top and bottom boundaries of court
If lblPaddleRight.Top < intTCourt Then
lblPaddleRight.Top = intTCourt
ElseIf lblPaddleRight.Top >(intBCourt - intPadRH) Then
lblPaddleRight.Top =(intBCourt - intPadRH)
End If
End Sub

Private Sub tmrBall_Tick(sender As Object, ByVal e As EventArgs) _


Handles tmrBall.Tick
'this procedure moves the ball once per clock tick when the timer is turned
on,
'and checks the ball's position to see whether it has made contact with the
top
'or bottom boundaries of the court, made contact with a paddle, or reached
the
'left or right-hand end of the court
lblBall.Top = lblBall.Top + vy 'move ball up or down by vy pixels
lblBall.Left = lblBall.Left + vx 'move ball left or right by vx pixels
If lblBall.Top < intTCourt Then lblBall.Top = intTCourt
'keep ball below upper boundary of court
If lblBall.Top >(intBCourt - intBallH) Then lblBall.Top =(intBCourt -
intBallH)
'keep ball above lower boundary of court
If lblBall.Top <= intTCourt Or lblBall.Top >=(intBCourt - intBallH) Then vy
= -vy
'make ball "bounce"(change vertical direction of ball)
171
If vx < 0 Then 'ball is moving from right to left
If lblBall.Top >(lblPaddleLeft.Top - intBallH) And lblBall.Top _
<(lblPaddleLeft.Top + intPadLH) Then
'vertical coordinates of ball and left paddle overlap
If lblBall.Left <= intPadFaceL Then 'ball has made contact with left
paddle
'change direction of ball and generate x and y vector values for ball
'based on randon selection of paddleZone values (+3 to -3)
Randomize()
paddleZone = CInt((Rnd() * 6) - 3)
Select Case paddleZone
Case 3
vy = -5
vx = 2
Case 2
vy = -4
vx = 3
Case 1
vy = -3
vx = 4
Case 0
vy = 0
vx = 5
Case -1
vy = 3
vx = 4
Case -2
vy = 4
vx = 3
Case -3
vy = 5
vx = 2
End Select
End If
Else
If lblBall.Left <= intLCourt Then
'vertical coordinates of ball and left paddle do not overlap
'and ball has reached left boundary of court
172
pointScored()
End If
End If
ElseIf vx > 0 Then 'ball is moving from left to right
If lblBall.Top > (lblPaddleRight.Top - intBallH) And lblBall.Top _
< (lblPaddleRight.Top + intPadRH) Then
'vertical coordinates of ball and right paddle overlap
If(lblBall.Left + intBallW) > lblPaddleRight.Left Then
'ball has made contact with right paddle
getZoneR() 'get section of right paddle making contact with ball
'change direction of ball and generate x and y vector values for ball
'depending on calculated paddleZone value (+3 to -3)
Select Case paddleZone
Case 3
vy = -5
vx = -2
Case 2
vy = -4
vx = -3
Case 1
vy = -3
vx = -4
Case 0
vy = 0
vx = -5
Case -1
vy = 3
vx = -4
Case -2
vy = 4
vx = -3
Case -3
vy = 5
vx = -2
End Select
End If
Else
If lblBall.Left > (intRcourt - intBallW) Then
173
'vertical coordinates of ball and right paddle do not overlap
'and ball has reached right boundary of court
pointScored()
End If
End If
End If
End Sub

Private Sub tmrBreak_Tick(sender As Object, ByVal e As EventArgs) _


Handles tmrBreak.Tick
'this procedure executes when a point has been scored
breakCount += 1 'record number of times timer has ticked
If breakCount = 1 Then 'this is first timer tick
lblPaddleLeft.Top = intPadLTop 'reset left paddle position
lblPaddleRight.Top = intPadRTop 'reset right paddle position
If lastPoint = 1 Then 'computer won last point
lblBall.Top = lblPaddleLeft.Top + intPadLH / 2 - intBallH / 2
'position ball in line with centre of left paddle
lblBall.Left = intPadFaceL 'place ball immediately to right of left
paddle
vx = 5 'set ball's x vector value to 5
ElseIf lastPoint = 2 Then 'player won last point
lblBall.Top = lblPaddleRight.Top + intPadRH / 2 - intBallH / 2
'position ball in line with centre of right paddle
lblBall.Left = intPadFaceR - lblBall.Width
'place ball immediately to left of right paddle
vx = -5 'set ball's x vector value to -5
End If
'generate random y vector value for ball (5 to -5
Randomize()
vy = (Rnd() * 10) - 5
ElseIf breakCount = 2 Then 'this is second timer tick
breakCount = 0 'reset timer counter
lblBall.Visible = True 'restore visibility of ball
tmrBall.Start() 'restart tmrBall
tmrPaddle.Start() 'restart tmrPaddle
tmrBreak.Stop() 'stop tmrBreak
End If
174
End Sub

Private Sub cmdPlay_Click(sender As Object, ByVal e As EventArgs) _


Handles cmdPlay.Click
'this procedure starts game - player will server first
lblBall.Top = lblPaddleRight.Top + intPadRH / 2 - intBallH / 2
'position ball in line with centre of right paddle
lblBall.Left = intPadFaceR - lblBall.Width
'place ball immediately to left of right paddle
lblBall.Visible = True 'make ball visible
vx = -5 'set ball's x vector to -5
'generate random y vector value for ball (5 to -5)
Randomize()
vy = (Rnd() * 10) - 5
tmrPaddle.Start() 'start tmrPaddle
tmrBall.Start() 'start tmrBall
cmdPlay.Enabled = False 'disable Start button
End Sub

Private Sub cmdPause_Click(sender As Object, ByVal e As EventArgs) _


Handles cmdPause.Click
'this procedure allows player to pause the game
If cmdPause.Text = "Pause" Then 'game is in progress
tmrPaddle.Stop() 'stop tmrPaddle
tmrBall.Stop() 'stop tmrBall
cmdPause.Text = "Resume" 'change button caption to "Resume"
ElseIf cmdPause.Text = "Resume" Then 'game is paused
tmrPaddle.Start() 'start tmrPaddle
tmrBall.Start() 'start tmrBall
cmdPause.Text = "Pause" 'change button caption to "Pause"
End If
End Sub

Private Sub cmdReset_Click(sender As Object, ByVal e As EventArgs) _


Handles cmdReset.Click
'this procedure stops game and resets game variable and display
reset()
resetScores()
175
End Sub

Private Sub cmdExit_Click(sender As Object, ByVal e As EventArgs) _


Handles cmdExit.Click
'this procedure exits program immediately
End
End Sub

Sub getZoneR()
'this procedure checks where ball "made contact" with
'rightPaddle and sets value of paddleZone accordingly
'numbers would need adjustment if paddle height changed
If(lblBall.Top + intBallH / 2) < (lblPaddleRight.Top + 10) Then
paddleZone = 3
ElseIf(lblBall.Top + intBallH / 2) < (lblPaddleRight.Top + 20) Then
paddleZone = 2
ElseIf(lblBall.Top + intBallH / 2) < (lblPaddleRight.Top + 30) Then
paddleZone = 1
ElseIf(lblBall.Top + intBallH / 2) < (lblPaddleRight.Top + 50) Then
paddleZone = 0
ElseIf(lblBall.Top + intBallH / 2) < (lblPaddleRight.Top + 60) Then
paddleZone = -1
ElseIf(lblBall.Top + intBallH / 2) < (lblPaddleRight.Top + 70) Then
paddleZone = -2
Else
paddleZone = -3
End If
End Sub

Sub pointScored()
'this procedure executes if ball reaches either end of court
tmrBall.Stop() 'stop tmrBall
tmrPaddle.Stop() 'stop tmrPaddle
If lblBall.Left < intPadFaceL Then 'ball has bypassed the left paddle
scoreRight = scoreRight + 1 'add 1 to player's score
lastPoint = 2 'player won last point
lblRightScore.Text = scoreRight 'display new player score
ElseIf(lblBall.Left + intBallW) > intPadFaceR Then
176
'ball has bypassed the right paddle
scoreLeft = scoreLeft + 1 'add 1 to computer's score
lastPoint = 1 'computer won last point
lblLeftScore.Text = scoreLeft 'display new computer score
End If
lblBall.Visible = False 'hide ball
If scoreLeft = winScore Then 'computer has won game
reset() 'call reset game variables and game display
MsgBox("Sorry, better luck next time!") 'display consolatory message
resetScores() 'reset scores and score display
ElseIf scoreRight = winScore Then 'player has won game
reset() 'call reset game variables and game display
MsgBox("Congratulations!") 'display congratulatory message
resetScores() 'reset scores and score display
Else
tmrBreak.Start() 'game still in progress, start tmrBreak
End If
End Sub

Sub reset()
'reset game variables, timers and display
tmrBall.Stop()
tmrBreak.Stop()
tmrPaddle.Stop()
lblBall.Visible = False
lblPaddleLeft.Top = intPadLTop
lblPaddleRight.Top = intPadRTop
cmdPlay.Enabled = True
End Sub

Sub resetScores()
'reset scores and score display
scoreLeft = 0
scoreRight = 0
lblLeftScore.Text = "0"
lblRightScore.Text = "0"
End Sub

177
Private Sub frmPong_MouseDown(sender As Object, ByVal e As _
Windows.Forms.MouseEventArgs) _
Handles Me.MouseDown, picPongTable.MouseDown
'executes if mouseDown event occurs
If e.Button = MouseButtons.Right Then 'right mouse button is down
dnFlag = True 'set dnFlag
ElseIf e.Button = MouseButtons.Left Then 'left mouse button is down
upFlag = True 'set upFlag
End If
End Sub

Private Sub frmPong_MouseUp(sender As Object, ByVal e As _


Windows.Forms.MouseEventArgs) _
Handles Me.MouseUp, picPongTable.MouseUp
'executes if mouseUp event occurs
If e.Button = MouseButtons.Right Then 'right mouse button has been released
dnFlag = False 'unset dnFlag
ElseIf e.Button = MouseButtons.Left Then 'left mouse button has been
released
upFlag = False 'unset upFlag
End If
End Sub

End Class

Arrays and Structures in Visual Basic

An array is a collection of related variables of the same type. All of the variables in the array have the same
name, but each is given a unique index number in accordance with its position within the array. These index
numbers (called array subscripts) start from zero, so the first item in an array has the index number 0, the
second item has the index number 1, and so on.

178
All of the data items in an array (called the array elements) are stored in contiguous locations in memory, and
occupy the same amount of space. In order to find a particular array element, it is only necessary to know the
address in memory of the first item in the array and the array subscript of the element being sought. This
feature makes it very easy to use a loop construct to navigate through the elements of an array, or to sort the
elements into alphabetical or numerical order.

An array variable must be declared just like other variables, but it is not necessary to specify the number of
elements. That said, you can declare an array and populate it with data at the same time. The following code
declares an array of type String with seven elements, and then populates it:

Dim strWeekDays(6) As String


strWeekDays(0) = "Monday"
strWeekDays(1) = "Tuesday"
strWeekDays(2) = "Wednesday"
strWeekDays(3) = "Thursday"
strWeekDays(4) = "Friday"
strWeekDays(5) = "Saturday"
strWeekDays(6) = "Sunday"

The following statement declares and populates the array at the same time (note that this time there is no
number within the parentheses) :

Dim strWeekDays() As String = {"Monday", "Tuesday", "Wednesday", "Thursday",


"Friday", "Saturday", "Sunday"}

You can also declare an array without specifying the number of elements it will hold:

Dim strMembers() As String

A simple one-dimensional array is essentially a numbered list of variables of the same data type. To
demonstrate the principle, we will create a simple program that stores a set of exam scores in an array, and
then manipulates the array data in various ways. Proceed as follows:

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

179
The ExamScore program interface

2. Set the control names as shown in the table below.

ExamScores Form Controls

Control Name

Form1 frmExamScores
Button1 cmdRawData
Button2 cmdSortData
Button3 cmdAverage
Button4 cmdHighScore
Button5 cmdLowScore
Button6 cmdExit

3. In the code editor, add the following program statement to the frmExamScores class declaration:

Dim examScores() As Integer = _


{32, 64, 21, 97, 88, 59, 70, 55, 44, 11, 23, 66, 70, 47, 61, 77, 28, 38, 60,
93}

4. In the form design window, double-click on the cmdRawData button and add the following code at the
cursor:
180
Dim strOutput As String = ""
For count = 0 To 19
strOutput &= examScores(count) & vbNewLine
Next
MsgBox(strOutput)

Your code editor window should now look like the illustration below.

The array declaration and cmdRawData Click event procedure

5. Run the program and click on the Display raw data button. The message box that appears should look
something like the following:

181
The message box output for the Display raw data button

The initial Dim statement assigns twenty exam scores (each representing marks out of 100) to the integer
array examScores(). The For . . . Next loop accesses each array element in turn, and appends each value,
plus a newline character, to the string variable strOutput. The message box then displays the string.

Simply displaying the array values is relatively simple, as you can see. The other functions we want to
implement include displaying the average, highest and lowest scores, which involves slightly more effort.

The most difficult thing we will attempt is to sort the scores and display them in order of magnitude. At one time
this would have involved writing something like a bubble sort. Thanks to the facilities available for handling
arrays in Visual Basic however, the task is now much easier.

6. In the form design window, double-click on the cmdSortData button and add the following code at the
cursor:

182
Dim strOutput As String = ""
Array.Sort(examScores)
For count = 0 To 19
strOutput &= examScores (count) & vbNewLine
Next
MsgBox(strOutput)

The code for the cmdSortData button is almost identical to the code for the cmdRawData button, except for
the command Array.Sort(examScores). This line of code calls on the Array.Sort() method provided for arrays
in Visual Basic to sort the array values by ascending numerical order. The method is limited to sorting one-
dimensional arrays, but if that is all you need to do it is quite useful.

7. Run the program and click on the Display sorted data button. The message box that appears should
look something like the following:

The message box output for the Display sorted data button

183
Note that the task of programming the loops is made somewhat easier because we know that there are 20
array elements. It is possible (probable, in fact) that we will not always know in advance how big the array is.
Fortunately, Visual Basic provides methods for arrays that allows us to find the size of the array (GetLength()),
and the upper and lower bounds of the array (GetLowerBound() and GetUpperBound()).

The length of the array is the number of items it contains. The lower bound is the lowest index number
(or array subscript) used by the array (normally 0) while the upper bound is the highest index number (normally
one less than the number of items in the array). As an example of how this could help us, if we did not know
the length of the examScores() array we could replace the first line in each of our loops:

For count = 0 To 19

with:

For count = examScores.GetLowerBound(0) To examScores.GetUpperBound(0)

Note also that the Array.sort() function changes the order of the array variables within the array, so if you
need to preserve the original ordering of the array elements, you may need to include some intermediate code
to save the state of the original array prior to sorting.

One option is to create a copy of the array and perform a sort on the copy, thus preserving the order of the
original. Visual Basic even provides a Copy method for arrays that enables us to do this. The code for
the cmdSortData button's Click event could be amended to read as follows:

Dim strOutput As String = ""


Dim examScoresNew (examScores.Length - 1) As Integer
Array.Copy (examScores, examScoresNew, examScores.Length)
Array.Sort (examScoresNew)
For count = examScoresNew.GetLowerBound(0) To examScoresNew.GetUpperBound(0)
strOutput &= examScoresNew(count) & vbNewLine
Next
MsgBox(strOutput)

184
In the bubble sort algorithm referred to above, the program loops through the array one element at a time, and
compares each item with the next item in the array. If they are in the wrong order with respect to each other,
they must swap places. After the first pass through the loop, the last item in the array is in its correct position.

Since the remaining items may still require some adjustment the process is repeated, but (if you write the
bubble sort code efficiently) only for the remaining items. After the second pass through the loop, the second to
last item in the array is now also in its correct position. For each repetition of the procedure, the number of
array elements that must be traversed is reduced by one, until the sort is complete.

The algorithm effectively relies on a nested loop construct and the availability of an additional variable in which
to temporarily store one of the two values being swapped (if I were going to swap the contents of two jars, I
would need a third jar in which to temporarily store the contents of one of the first jar so that I could transfer the
contents of the second jar to it).

The mechanism may become a little clearer once you have examined the code. We could re-write the code for
the cmdSortData button's Click event handler to use the bubble sort method as follows:

Dim strOutput As String = ""


Dim examScoresNew(examScores.Length - 1) As Integer
Dim intSwap As Integer
Array.Copy(examScores, examScoresNew, examScores.Length)
For i = examScoresNew.GetLowerBound(0) To examScoresNew.GetUpperBound(0) - 1
For j = examScoresNew.GetLowerBound(0) To (examScoresNew.GetUpperBound(0) - 1
- i)
If examScoresNew(j) > examScoresNew(j + 1) Then
intSwap = examScoresNew(j)
examScoresNew(j) = examScoresNew(j + 1)
examScoresNew(j + 1) = intSwap
End If
Next
Next
For count = examScoresNew.GetLowerBound(0) To examScoresNew.GetUpperBound(0)
strOutput &= examScoresNew(count) & vbNewLine
Next
MsgBox(strOutput)

185
The bubble sort algorithm works for character data and strings, as well as for numbers, sorting the array
elements alphabetically rather than by numerical ascendancy. Bear in mind, however, that the sort uses ASCII
character codes to sort the data, so an upper-case character will always appear ahead of a lower case
character regardless of their respective position in the alphabet. Sorting string values therefore normally
requires that the string values are first converted to either all upper-case or all lower-case strings in order for a
meaningful sort to be achieved.

Note that the above code can probably be "tweaked" to make it more efficient. It can also be relatively easily
changed in order to perform a sort in the reverse direction (i.e. in descending rather than ascending numerical
order).

The code for the remaining buttons (cmdAverage, cmdHighScore, cmdLowScore and cmdExit) is given
below in its entirety. The code itself is sufficiently trivial that a lengthy explanation of how it works should be
unnecessary. Suffice it to say that the code further demonstrates that an array is a useful structure that can be
manipulated relatively easily using a For . . . Next loop.

Private Sub cmdAverage_Click(sender As Object, e As EventArgs) Handles


cmdAverage.Click
Dim intTotal As Integer = 0
For count = 0 To 19
intTotal += examScores(count)
Next
MsgBox("Average score: " & CSng(intTotal / 20))
End Sub

Private Sub cmdHighScore_Click(sender As Object, e As EventArgs) _


Handles cmdHighScore.Click
Dim intHighScore As Integer = 0
For count = 0 To 19
If intHighScore < examScores(count) Then
intHighScore = examScores(count)
End If
Next
MsgBox("High score: " & intHighScore)
End Sub

Private Sub cmdLowScore_Click(sender As Object, e As EventArgs) _


186
Handles cmdLowScore.Click
Dim intLowScore As Integer = 100
For count = 0 To 19
If intLowScore > examScores(count) Then
intLowScore = examScores(count)
End If
Next
MsgBox ("Low score: " & intLowScore)
End Sub

Private Sub cmdExit_Click (sender As Object, e As EventArgs) Handles


cmdExit.Click
End
End Sub

Working with files

In real life, we are faced with problems that are somewhat more complex than the simple example described
above. For a start, the data we need to process will probably be stored in a file, and we may not know how
many records are in the file before we open it.

If we are going to stick with the example of a set of exam results, we should consider that these results will
never exist in isolation. Each result will be associated with a student, who is probably identified in our file
system by a student ID number. Indeed, it is likely that during a term or semester each student will sit a
number of exams, and the scores for each exam will be recorded in the student's record for that term or
semester.

Let us therefore consider a scenario in which twenty students have just completed the first semester on an
Honours degree, and have taken a separate exam for each of six modules. The results are stored in a text-
based data file (exam_results.dat) that stores the student ID, name, and exam results for each student in a
record consisting of eight comma-delimited fields (an eight-digit student ID number, six exam results, and a
string containing the student's last name and first initial). We will create a program that reads the file and
makes the following data items available:

 the number of students taking each exam


 the average score for each exam
187
 the median for each exam
 the standard deviation for each exam
 the highest score for each exam, and the student who achieved it
 the average exam score for each student
 the projected Honours degree level for each student (e.g. 1, 2.1, 2.2 etc.)

The source data file is sorted by student ID number, but the application must be able to create a new data file
containing the student ID and name, together with the student's average exam result and projected Honours
degree level. The target file should be sorted by student last name and first initial.

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

The StudentRecords program interface

9. Set the control names as shown in the table below.

188
StudentRecords Form Controls

Control Name

Form1 frmStudentRecords
Label1 lblTitle
RichTextBox1 rtbOutput
Button1 cmdLoad
Button2 cmdStats
Button3 cmdPerformance
Button4 cmdCreateFile
Button5 cmdExit
Label2 lblNumStudents
TextBox1 txtNumStudents

10. Save the project to create the project folder.


11. Download the file exam_results.zip from the link below, and unzip the contents into
the \bin\Debug sub-directory of your project folder (the .zip file contains the exam_results.dat file).
Download the file exam_results.zip
12. In the form design window, right-click anywhere in the form and select View Code from the pop-up
menu.
13. Add the following lines of code to the form's class definition (Note: these statements must appear before
any event handlers or sub-routines):

Dim strStudent(,) As String


Dim intResults(,) As Integer
Dim strRecord() As String
Dim strTemp() As String
Dim intRecTotal As Integer
Dim dblAverages(5) As Double
Dim dblMedians(5) As Double
Dim dblStdDevs(5) As Double
Dim intHiScores(5) As Integer
Dim strHiScoreStudent(5) As String
Dim dblStudentAverage() As Double
Dim strStudentLevel() As String
Dim strNamesSorted() As String

189
The first two statements declare empty two-dimensional arrays (strStudent(,) and intResults(,)) to hold the
data we want to extract from the input file, in the format in which we need it. Whereas a one-dimensional array
is like an ordered list of items of the same data type, a two-dimensional array is more like a table (i.e. you can
store data in rows and columns).

For this application we need two arrays, one to hold the student ID number and student name (which are
stored as strings), and one to hold the six examination results for each student (which we need to store as
integers in order to perform calculations on them). Two-dimensional arrays, like one-dimensional arrays, can
only store data items with the same data type. The arrays have not been dimensioned, since we must assume
that the number of records in the input file could vary.

The un-dimensioned string arrays strRecord() and strTemp() are declared next, and are used during the file
input stage of the program. The strRecord() array will hold each comma-delimited string read in from the input
file, and will be dynamically re-dimensioned during this process to accommodate the correct number of
records. The strTemp() array will be used to hold the contents of each comma-delimited input string in turn as
an array.

The next line declares the integer variable intRecTotal which will store the number of records read in from the
file. It will subsequently be used in various processing loops to set the upper array bound.

The next three statements create floating point array variables


(dblAverages(), dblMedians() and dblStdDevs()) that will store the results of
the average, median and standard deviation calculations for each of the six modules.

The next two lines create array variables to hold information about the highest scores for each module. The
integer array intHighScores() will hold the highest marks for each exam, and the string
variable strHighScoreStudents will hold the names of the students achieving those scores.

The next two lines declare variables to store data about each student's progress to date. The floating point
array variable dblStudentAverage() holds the average exam score for each student, while the string array
variable strStudentLevel() will hold information about the degree level currently attained (e.g. "First", "Upper
Second" and so on).

190
The last declaration creates a string array called strNamesSorted(), which will hold the names of the students
sorted into alphabetical order. This array will be used for comparison purposes when sorting the student data
by name to be written to the output file.

12. Set the Font property of the RichTextBox control to "Courier New, 8.25pt"
13. Set the Enabled property of the cmdStats, cmdPerformance, and cmdCreateFile control to "False"
14. In the form design window, double-click on the cmdLoad button and add the following code at the
cursor:

Dim srdFile As IO.StreamReader


intRecTotal = -1
lblTitle.Text = "Student Input Data"
rtbOutput.Clear()
rtbOutput.AppendText _
("StudentID Mod1 Mod2 Mod3 Mod4 Mod5 Mod6 Candidate" & vbNewLine)
rtbOutput.AppendText _
("--------- ---- ---- ---- ---- ---- ---- ---------" & vbNewLine)
srdFile = New IO.StreamReader("exam_results.dat")
Do Until srdFile.Peek = -1
intRecTotal += 1
ReDim Preserve strRecord(intRecTotal)
strRecord(intRecTotal) = srdFile.ReadLine()
Loop
srdFile.Close()
ReDim strStudent(intRecTotal, 1)
ReDim intResults(intRecTotal, 5)
For i = 0 To intRecTotal
strTemp = strRecord(i).Split(",")
strStudent(i, 0) = strTemp(0)
strStudent(i, 1) = strTemp(7)
For j = 0 To 5
intResults(i, j) = strTemp(j + 1)
Next
rtbOutput.AppendText(strStudent(i, 0).PadRight(9))
For j = 0 To 5
rtbOutput.AppendText(Convert.ToString(intResults(i, j)).PadLeft(6))
Next
rtbOutput.AppendText(" " & strStudent(i, 1).PadRight(9) & vbNewLine)

191
Next
txtNumStudents.Text = intRecTotal + 1
cmdLoad.Enabled = False
cmdStats.Enabled = True
cmdPerformance.Enabled = True
cmdCreateFile.Enabled = True

This is probably the longest single block of code in the whole program. The first line, as we have seen
previously, creates a StreamReader object called srdFile. The next line sets the value of incrRecTotal to 0.
We will use this variable to keep track of the number of records read in from the exam_results.dat file, and it
will also serve as the upper array bound for various arrays.

The third line of code sets the text displayed by the label at the top of the form to "Student Input Data" to inform
the user what the output they are looking at represents, and the next line clears the RichTextBox in readiness
for the student record data to be displayed.

The next two lines set up the column headings for the student record data.

The next line uses the New keyword to allocate memory to the StreamReader variable srdFile, and
associates it with the exam_results.dat file. It also opens the file for input. The Do . . . Until loop
increments intRecTotal, re-dimensions the strRecord() array variable to accommodate the next record to be
read, and reads the next record from the file into the array using
the StreamReader object's ReadLine()method. It carries on doing this until
the StreamReader object's Peek method detects the end of the file.

Once the program exits the Do . . . Until loop, the input stream is closed and the two-dimensional
arrays strStudent() and intResults() are re-dimensioned to the required size (this is determined using the final
value of incrRecTotal).

The main For . . . Next loop splits each of the comma-delimited record strings into its eight constituent parts
(the student's ID, the six examination marks for each student, and the student's name). The loop then inserts
these values into the strStudent() array (the student ID and student name) and the intResults() array (the six
exam results). The last part of the loop outputs the formatted data to the RichTextBox, making use of
the PadLeft() and PadRight() methods to vertically align the data within each column.

192
One point of interest here is the use of two nested For . . . Next loops within the main For . . . Next loop - one
to allocate the exam six results for each record to the intResults() array, and one to output them on each line
of output in the RichTextBox.

The line immediately following the main For . . . Next loop displays the number of students for whom records
are stored in the input data file by setting the text property of the text box txtNumStudents to the value
of incrRecTotal (the number of student records read).

The last four lines of code are simply concerned with disabling the cmdLoad button (since the button's event
handler code only needs to be executed once), and enabling the buttons that create the program's output
(these were initially disabled because they cannot produce any output until the source data has been loaded).

At this stage you may be wondering why we didn't just place the above code into the form's Load event, since
this would save the user the bother of having to click on the cmdLoad button at all. The reason is that we want
to make some enhancements to the interface later on that will enable the user to open any file that contains
student exam records in the same format.

To see what we have achieved so far, run the program and click on the Load file button to display the data.
Your output should look something like the screenshot below.

193
The formatted student exam results data

The Exam statistics button will display all of the required statistical information for the six module
examinations taken by the students included in the input file. This information includes the average
examination mark, the median and standard deviation, and the highest score (and by whom it was achieved).

We could of course write all the code to extract and display this information in the button's event handler, but it
would become somewhat unwieldy and difficult to maintain if we did that. We might also consider the possibility
that some of the functionality involved might be usefully encapsulated in separate procedures so that it can be
re-used elsewhere. With this in mind therefore, the task of calculating the statistics will be carried out using four
separate procedures, namely getAverages(), getMedians(), getStdDevs() and getHiScores().

The code for these procedures is given below, and should be entered as shown somewhere within the form's
class definition.

194
Sub getAverages():

Sub getAverages()
Dim intSum As Integer
dblAverages = {0, 0, 0, 0, 0, 0}
For i = 0 To 5
intSum = 0
For j = 0 To intRecTotal
intSum += intResults(j, i)
Next
dblAverages(i) = intSum / (intRecTotal + 1)
Next
End Sub

The average mark for each examination is calculated by adding together all the individual student marks for the
examination and dividing the result by the number of students taking part. The getAverages()subroutine
declares a local integer variable intSum to hold the result of adding together the results for each examination,
and initialises the dblAverages() array elements to zero. It then uses a For . . . Next loop for each module
examination in which intsum is initialised to zero at the beginning of the loop.

A second (nested) For . . . Next loop adds the result for each student to the total (intSum). The last line of
code in the main loop divides intSum by the number of records (intRecTotal) to get the average mark for each
examination and assigns the result to the dblAverages() array.

Sub getMedians():

Sub getMedians()
Dim intScores(intRecTotal) As Integer
dblMedians = {0, 0, 0, 0, 0, 0}
For i = 0 To 5
For j = 0 To intRecTotal
intScores(j) = intResults(j, i)
Next
Array.Sort(intScores)
If intRecTotal Mod 2 = 1 Then
dblMedians(i) = _

195
(intScores((intRecTotal / 2) + 0.5) + intScores((intRecTotal / 2) -
0.5)) / 2
Else
dblMedians(i) = intScores((intRecTotal / 2))
End If
Next
End Sub

The median of a range of values is the value that occurs at the mid-point of the range. In other words, if there
are n values higher than the median, there will be also be n values lower than the median. Obviously if the
total number of values is even, there is no single value at the mid-point. If this is the case, the average of the
two numbers on either side of the mid-point is used.

The getMedians() subroutine first declares an integer array variable to hold the marks for a single
examination. It then initialises all of the array elements of dblMedians() to zero. The rest of the subroutine is
taken up by a For . . . Next loop that iterates six times (once for each set of examination marks). A For . . .
Next loop nested within the main loop reads the marks for an examination into the intScores() array.
The Array.Sort() method is then called to sort the intScores() array elements into numerical order.

The If . . . Else conditional statement checks to see whether the number of values is odd or even. If the
number is odd, the median value is that of the array element whose subscript is equal to intRecTotaldivided
by two. If the number is even, the median value is the average of the two array elements whose subscripts are
equal to intRecTotal divided by two minus 1, and intRecTotal divided by two.

If you are not sure how this works, assume there are twenty values, in which case intRecTotal will be 20. We
therefore want the average of the tenth and eleventh values to get the median, so we need to add together the
values with the subscripts 9 and 10, and divide the result by two. Dividing twenty (the value of intRecTotal) by
two gives us the higher subscript (10). Subtracting one from this value gives us the lower subscript (9).

Once the median for each set of examination marks has been determined, it is assigned to
the dblMedians() array.

Sub getStdDevs():

196
Sub getStdDevs()
Dim dblSumDiffsSquared As Double
dblStdDevs = {0, 0, 0, 0, 0, 0}
For i = 0 To 5
dblSumDiffsSquared = 0
For j = 0 To intRecTotal
dblSumDiffsSquared += (intResults(j, i) - dblAverages(i)) ^ 2
Next
dblStdDevs(i) = Math.Sqrt(dblSumDiffsSquared / intRecTotal)
Next
End Sub

The standard deviation of a range of values is a measure of the distribution of a dataset around the mean
value. Essentially, it is a measure of how spread-out the values are. The lower the standard deviation, the less
spread-out they are. To find the standard deviation, we first need to find the sum of the squares of the
differences between each value in the dataset and the mean value. We then divide this sum by the number of
results in the dataset. Finally, we find the square root of the result to arrive at the standard deviation. The
mathematical formula for standard deviation is shown below.

Σ(x‾- x) 2
σ = √ n-1

The code for the subroutine declares a floating point variable (dblSumDiffsSquared) to hold the sum of the
squared differences, and initialises all of the array elements of dblStdDevs() to zero. The subroutine's
main For . . . Next loop gets the standard deviation values for each examination and stores them in
the dblStdDevs() array.

The inner For . . . Next loop calculates the difference between each examination mark and the average,
squares it, and adds it to dblSumDiffsSquared. The last line of the inner loop uses Visual
Basic's Math.Sqrt() function to get the square root of dblSumDiffsSquared divided
by intRecTotal (the intRecTotal variable is, by definition, n-1).

Sub getHiScores():

197
Sub getHiScores()
Dim intRecIndex (5) As Integer
intHiScores = {0, 0, 0, 0, 0, 0}
For i = 0 To 5
For j = 0 To intRecTotal
If intResults(j, i) > intHiScores(i) Then
intHiScores(i) = intResults(j, i)
intRecIndex(i) = j
strHiScoreStudent(i) = strStudent(intRecIndex(i), 1)
End If
Next
Next
For i = 0 To 5
For j = 0 To intRecTotal
If intResults(j, i) = intHiScores(i) And j <> intRecIndex(i) Then
strHiScoreStudent(i) &= ", " & strStudent (j, 1)
End If
Next
Next
End Sub

The getHighScores() subroutine creates an integer array (intRecIndex()) to hold the array subscripts for the
first occurrence of the highest mark for each examination. There exists the possibility that two students, or
more than two students, will jointly achieve the highest mark. For this reason the subroutine uses two For . . .
Next loops, one after the other. Both of these loops contain nested For . . . Next loops.

The first outer loop finds the highest score for each of the six examinations, and the name of the first student in
the list with which this score is associated. The inner loop iterates through the scores for each exam to
establish the highest score, stores the result in the intHiScores() array, and stores the name of the first
student found to have achieved this score in the strHiScoreStudent() array.

The second loop iterates through the results for each exam once more, this time for the purpose of finding any
other students that have achieved the same score and appending their names to the existing elements of
the strHiScoresStudents() array. Now that the required subroutines have been created, we are ready to write
the code for the cmdStats button.

198
15. In the form design window, double-click on the cmdStats button and add the following code at the
cursor:

lblTitle.Text = "Examination Statistics"


rtbOutput.Clear()
rtbOutput.AppendText _
("Module No Average Median Std Dev Hi Score Candidate (s) " & vbNewLine)
rtbOutput.AppendText _
("--------- ------- ------ ------- -------- ------------" & vbNewLine)
getAverages()
getMedians()
getStdDevs()
getHiScores()
For i = 0 To 5
rtbOutput.AppendText("Module " & i + 1)
rtbOutput.AppendText _
(Convert.ToString(Format (dblAverages(i), "Fixed")).PadLeft(9))
rtbOutput.AppendText _
(Convert.ToString(Format (dblMedians(i), "Fixed")).PadLeft(7))
rtbOutput.AppendText _
(Convert.ToString(Format (dblstdDevs(i), "Fixed")).PadLeft(8))
rtbOutput.AppendText _
(Convert.ToString(intHiScores(i)).PadLeft(8))
rtbOutput.AppendText _
(" " & strHiScoreStudent(i) & vbNewLine)
Next

The event handler for the cmdStats button first sets the text displayed by the label at the top of the form to
"Examination Statistics" to inform the user what the output they are looking at represents, then clears
the RichTextBox and sets up the column headers for the statistical output. It then calls the four
subroutines getAverages(), getMedians(), getStdDevs() and getHiScores(). These subroutines essentially
do all the hard work (the number-crunching, if you like). The final part of the event handler code is a For . . .
Nextloop that outputs the formatted data to the RichTextBox. Once you have entered the code, run the
program, click on the Load file button, then click on the Exam Statistics button to display the data. Your
output should look something like the screenshot below.

199
The formatted examination statistics data

The cmdPerformance button will display the average score and current level of achievement for each student.
Once again, we are creating separate subroutines to do the number crunching, the code for which is shown
below. Enter the code as shown somewhere within the form's class definition.

Sub getStudentAverages():

Sub getStudentAverages()
Dim scoreTotal As Integer
ReDim dblStudentAverage(intRecTotal)
For i = 0 To intRecTotal
scoreTotal = 0
For j = 0 To 5
scoreTotal += intResults(i, j)

200
Next
dblStudentAverage(i) = scoreTotal / 6
Next
End Sub

The getStudentAverages() subroutine declares a local integer variable (scoreTotal) to hold the sum of each
student's six exam scores. The second line of code re-dimensions the dblStudentAverage() array. The rest of
the code employs a For . . . Next loop and nested For . . . Next loop to iterate through each student's exam
scores and calculate the averages.

Sub getStudentLevels():

Sub getStudentLevels()
ReDim strStudentLevel(intRecTotal)
For i = 0 To intRecTotal
If dblStudentAverage(i) >= 70 Then
strStudentLevel(i) = "First"
ElseIf dblStudentAverage(i) >= 60 Then
strStudentLevel (i) = "Upper Second"
ElseIf dblStudentAverage(i) >= 50 Then
strStudentLevel(i) = "Lower Second"
ElseIf dblStudentAverage(i) >= 40 Then
strStudentLevel(i) = "Third"
ElseIf dblStudentAverage(i) >= 35 Then
strStudentLevel(i) = "Ordinary Pass"
Else
strStudentLevel(i) = "Fail"
End If
Next
End Sub

The getStudentLevel() subroutine starts by re-dimensioning the strStudentLevel() array. It then uses a For .
. . Next loop to examine the average score for each student. A series of If . . . Elseif statements is used to
determine what degree level the student has currently achieved. The guidelines used by universities in the UK
to determine what level of degree should be awarded are as follows:

201
 First: >= 70%
 Upper Second: >=60% to <70%
 Lower Second: >=50% to <60%
 Third: >=40% to <50%
 Ordinary Pass: >=35% to <40%
16. In the form design window, double-click on the cmdPerformance button and add the following code at
the cursor:

lblTitle.Text = "Student Performance"


rtbOutput.Clear()
rtbOutput.AppendText("StudentID".PadRight(12) & "Student Name".PadRight(15))
rtbOutput.AppendText("Average Mark".PadRight(15) & "Level Achieved" &
vbNewLine)
rtbOutput.AppendText("---------".PadRight(12) & "------------".PadRight(15))
rtbOutput.AppendText("------------".PadRight(15) & "--------------" &
vbNewLine)
getStudentAverages()
getStudentLevels()
For i = 0 To intRecTotal
rtbOutput.AppendText(strStudent (i, 0).PadRight(12) & strStudent(i,
1).PadRight(15))
rtbOutput.AppendText(Convert.ToString(Format(dblStudentAverage(i),
"Fixed")).PadLeft(12))
rtbOutput.AppendText(" " & strStudentLevel(i))
rtbOutput.AppendText(vbNewLine)
Next

The cmdPerformance button's event handler code first sets the text displayed by the label at the top of the
form to "Student Performance" to inform the user what the output they are looking at represents, clears
the RichTextBox and sets up the column headers for the student performance data, and calls the
subroutines getStudentAverages() and getStudentLevels().

The last part of the code is a For . . . Next loop that simply outputs the formatted data for each student. Once
you have entered the code, run the program, click on the Load file button, then click on the Student
Performance button to display the output, which should look something like the screenshot below.

202
The formatted student performance data

The last major task is to write the code for the cmdCreateFile button, which saves the student performance
data to an output file. Once again we have a subroutine to write before we can encode the button's event
handler, this time to create an ordered array of student names in order to be able to output the performance
data to the file in alphabetical order by student name and initial. The code for the subroutine is shown below.
Enter the code as shown somewhere within the form's class definition.

Sub sortNames()
ReDim strNamesSorted(intRecTotal)
For i = 0 To intRecTotal
strNamesSorted(i) = LCase(strStudent(i, 1))
Next
Array.Sort(strNamesSorted)
End Sub

203
The subroutine starts by re-dimensioning the strNamesSorted array(). It then uses a For . . . Next loop to
copy the student names from the strStudent() array into the strNamesSorted() array. Note however that in
the process of copying, the names are converted to lower-case. The last line of the subroutine uses
the Array.Sort() method to sort the strNamesSorted() array alphabetically.

17. In the form design window, double-click on the cmdCreateFile button and add the following code at the
cursor:

Dim swrFile As IO.StreamWriter


Dim strOutput As String
swrFile = IO.File.CreateText("student_progress.dat")
sortNames()
getStudentAverages()
getStudentLevels()
For i = 0 To intRecTotal
For j = 0 To intRecTotal
strOutput = ""
If LCase (strStudent(j, 1)) = strNamesSorted(i) Then
strOutput &= strStudent(j, 0) & ","
strOutput &= strStudent(j, 1) & ","
strOutput &= Convert.ToString(Format(dblStudentAverage(j), "Fixed")) &
","
strOutput &= strStudentLevel(j)
swrFile.WriteLine(strOutput)
End If
Next
Next
swrFile.Close()
MsgBox("Output file created.")

The first line of the code in this event handler creates a variable of type StreamWriter (swrFile). The second
line creates a local string variable called strOutput that we will use to build the output for each line to be
written to the output file. The third line creates the student_progress.dat file (or overwrites the file with a new,
empty version if it already exists), opens the file for output, and associates
the StreamWritervariable swrFile with it.

The event handler then calls the sortNames(), getStudentAverages() and getStudentLevels()subroutines.
The main For . . . Next loop employs a nested For . . . Next loop to make one complete cycle through the
204
student performance data for each student name in the strNamesSorted() array. Each time a match is found
the student data for that student name is appended to a comma-delimited output string and written to the
output file.

The last two lines of code close the file and display a message box to tell the user that the file was created
(note that there is no error handling included at the moment). By default, the file will be created in
the \bin\Debug sub-directory of your project folder. Once you have entered the code, run the program, click on
the Load file button, then click on the Create student file button to display the data. Your output should look
something like the screenshot below (note that this event handler does not write to the RichTextBox).

The Create student file button displays a message when the file has been created

205
Using Structures

The StudentRecords program is now fully functional and has been implemented using one-dimensional and
two-dimensional arrays. This works quite well, because arrays lend themselves to the kind of list processing
we have essentially been engaged in. The code is however somewhat unwieldy at times, and not particularly
efficient. Because an array can only hold variables of a single data type, for example, we cannot put all the
student data into a single array but must instead split it between a multi-dimensional string array (for the
student ID and student name) and a multi-dimensional integer array (to hold the examination scores).

The problem becomes exacerbated when we are dealing with even more different types of variables. A more
detailed student data file might require string variables to hold name and address information, integer variables
to hold examination and coursework scores, floating point variables to hold statistical information, date and
time variables to store information such as the student's date of birth, Boolean variables to indicate whether or
not a student has completed a particular module, and so on.

The structure is a user-defined data type that can store information consisting of multiple terms with various
data types, including other structures. This means we could store the different kinds of student data handled by
our StudentRecords program in a single structure variable. Better still, because structures are data types in
their own right, it is even possible to put them into an array. In fact we could have written our StudentRecords
program using a single array of structures rather than having to use many different arrays.

Like other types of variable, a structure variable must be declared before it can be used. The general form of a
structure declaration is shown below.

Scope Structure structureName


Dim memberName As Type
. . .
End Structure

Scope is simply the scope of the structure, either Public or Private. The Structure keyword is followed
by structureName, which is the name by which the user-defined data type will be known. Within the body of
the structure declaration, the individual structure members (or member variables) are declared using a series
of Dim statements. The statement End Structure marks the end of the structure declaration.

206
For the StudentRecords program, we could use the following structure to store the relevant data items:

Public Structure structStudentRecord


Dim strID As String
Dim strName As String
Dim strResults() As Integer
End Structure

Note that the examination results for a student are still stored as an array, but the array itself is now one of the
member variables of structStudentRecord. We present below the revised code for
the StudentRecords program, this time using a structure variable to hold the input file data, and creating an
array of structures to enable us to take advantage of the list processing features of arrays.

You will find that the code is not significantly reduced in volume, but it may be slightly easier to follow. Note
that in order to access a structure variable member, the name of the structure is used together with the name
of the member variable. The two names are separated by a dot, as in the following examples:

structRecords(4).strName

structRecords(4).intResults(0)

In the first example, the variable being referenced is the strName member variable of the fifth element of the
array of structures, structRecords(). In the second example the same structure variable is being referenced,
but this time the data item of interest is the first element in the intResults() array member variable.

Here is the revised code for the StudentRecords program in its entirety:

Public Class frmStudentRecords

Public Structure structStudentRecord


Dim strID As String
Dim strName As String
Dim intResults() As Integer
End Structure

207
Dim structRecords() As structStudentRecord

Dim intRecTotal As Integer


Dim dblAverages(5) As Double
Dim dblMedians (5) As Double
Dim dblstdDevs(5) As Double
Dim intHiScores(5) As Integer
Dim strHiScoreStudent(5) As String
Dim dblStudentAverage() As Double
Dim strStudentLevel() As String
Dim strNamesSorted() As String

Sub getAverages()
Dim intSum As Integer
dblAverages = {0, 0, 0, 0, 0, 0}
For i = 0 To 5
intSum = 0
For j = 0 To intRecTotal
intSum += structRecords(j) .intResults (i)
Next
dblAverages(i) = intSum / (intRecTotal + 1)
Next
End Sub

Sub getMedians()
Dim intScores(intRecTotal) As Integer
dblMedians = {0, 0, 0, 0, 0, 0}
For i = 0 To 5
For j = 0 To intRecTotal
intScores(j) = structRecords(j).intResults(i)
Next
Array.Sort(intScores)
If intRecTotal Mod 2 = 1 Then
dblMedians(i) = (intScores((intRecTotal / 2) + 0.5) +
intScores((intRecTotal / 2) - 0.5)) / 2
Else
dblMedians(i) = intScores((intRecTotal / 2))
End If
208
Next
End Sub

Sub getStdDevs()
Dim dblSumDiffsSquared As Double
dblstdDevs = {0, 0, 0, 0, 0, 0}
For i = 0 To 5
dblSumDiffsSquared = 0
For j = 0 To intRecTotal
dblSumDiffsSquared += (structRecords(j).intResults(i) -
dblAverages(i)) ^ 2
Next
dblstdDevs(i) = Math.Sqrt(dblSumDiffsSquared / intRecTotal)
Next
End Sub

Sub getHiScores()
Dim intRecIndex(5) As Integer
intHiScores = {0, 0, 0, 0, 0, 0}
For i = 0 To 5
For j = 0 To intRecTotal
If structRecords(j).intResults(i) > intHiScores(i) Then
intHiScores(i) = structRecords (j).intResults(i)
intRecIndex(i) = j
strHiScoreStudent(i) = structRecords(j).strName
End If
Next
Next
For i = 0 To 5
For j = 0 To intRecTotal
If structRecords(j).intResults(i) = intHiScores(i) And j <>
intRecIndex(i) Then
strHiScoreStudent(i) &= ", " & structRecords(j).strName
End If
Next
Next
End Sub

209
Sub getStudentAverages()
Dim scoreTotal As Integer
ReDim dblStudentAverage(intRecTotal)
For i = 0 To intRecTotal
scoreTotal = 0
For j = 0 To 5
scoreTotal += structRecords(i).intResults(j)
Next
dblStudentAverage(i) = scoreTotal / 6
Next
End Sub

Sub getStudentLevels()
ReDim strStudentLevel(intRecTotal)
For i = 0 To intRecTotal
If dblStudentAverage(i) >= 70 Then
strStudentLevel(i) = "First"
ElseIf dblStudentAverage(i) >= 60 Then
strStudentLevel(i) = "Upper Second"
ElseIf dblStudentAverage(i) >= 50 Then
strStudentLevel(i) = "Lower Second"
ElseIf dblStudentAverage(i) >= 40 Then
strStudentLevel(i) = "Third"
ElseIf dblStudentAverage(i) >= 35 Then
strStudentLevel(i) = "Ordinary Pass"
Else
strStudentLevel(i) = "Fail"
End If
Next
End Sub

Sub sortNames()
ReDim strNamesSorted(intRecTotal)
For i = 0 To intRecTotal
strNamesSorted(i) = LCase(structRecords(i).strName)
Next
Array.Sort(strNamesSorted)
End Sub
210
Private Sub cmdLoad_Click(sender As Object, e As EventArgs) Handles
cmdLoad.Click
Dim srdFile As IO.StreamReader
Dim strArrayTemp() As String
intRecTotal = -1
lblTitle.Text = "Student Input Data"
rtbOutput.Clear()
rtbOutput.AppendText _
("StudentID Mod1 Mod2 Mod3 Mod4 Mod5 Mod6 Candidate" & vbNewLine)
rtbOutput.AppendText _
("--------- ---- ---- ---- ---- ---- ---- ---------" & vbNewLine)
srdFile = New IO.StreamReader("exam_results.dat")
Do Until srdFile.Peek = -1
intRecTotal += 1
ReDim Preserve structRecords(intRecTotal)
ReDim structRecords(intRecTotal).intResults(5)
strArrayTemp = srdFile.ReadLine().Split(",")
structRecords(intRecTotal).strID = strArrayTemp(0)
For i = 0 To 5
structRecords(intRecTotal).intResults(i) = CInt(strArrayTemp(i + 1))
Next
structRecords(intRecTotal).strName = strArrayTemp(7)
rtbOutput.AppendText(structRecords(intRecTotal).strID.PadRight(9))
For i = 0 To 5
rtbOutput.AppendText(Convert.ToString(structRecords(intRecTotal).intRe
sults(i)).PadLeft(6))
Next
rtbOutput.AppendText(" " & structRecords
(intRecTotal).strName.PadRight(9) & vbNewLine)
Loop
srdFile.Close()
txtNumStudents.Text = intRecTotal + 1
cmdLoad.Enabled = False
cmdStats.Enabled = True
cmdPerformance.Enabled = True
cmdCreateFile.Enabled = True
End Sub
211
Private Sub cmdStats_Click(sender As Object, e As EventArgs) Handles
cmdStats.Click
lblTitle.Text = "Examination Statistics"
rtbOutput.Clear()
rtbOutput.AppendText("Module No Average Median Std Dev Hi Score
Candidate(s)" & vbNewLine)
rtbOutput.AppendText("--------- ------- ------ ------- -------- -----------
-" & vbNewLine)
getAverages()
getMedians()
getstdDevs()
getHiScores()
For i = 0 To 5
rtbOutput.AppendText("Module " & i + 1)
rtbOutput.AppendText(Convert.ToString(Format(dblAverages(i),
"Fixed")).PadLeft(9))
rtbOutput.AppendText(Convert.ToString(Format(dblMedians(i),
"Fixed")).PadLeft(7))
rtbOutput.AppendText(Convert.ToString(Format(dblstdDevs(i),
"Fixed")).PadLeft(8))
rtbOutput.AppendText(Convert.ToString(intHiScores(i)).PadLeft(8))
rtbOutput.AppendText(" " & strHiScoreStudent(i) & vbNewLine)
Next
End Sub

Private Sub cmdPerformance_Click(sender As Object, e As EventArgs) Handles


cmdPerformance.Click
lblTitle.Text = "Student Performance"
rtbOutput.Clear()
rtbOutput.AppendText("StudentID".PadRight(12) & "Student
Name".PadRight(15))
rtbOutput.AppendText("Average Mark".PadRight(15) & "Level Achieved" &
vbNewLine)
rtbOutput.AppendText("---------".PadRight(12) & "------------".PadRight
(15))
rtbOutput.AppendText("------------".PadRight(15) & "--------------" &
vbNewLine)
212
getStudentAverages()
getStudentLevels()
For i = 0 To intRecTotal
rtbOutput.AppendText(structRecords(i).strID.PadRight(12) &
structRecords(i).strName.PadRight(15))
rtbOutput.AppendText(Convert.ToString(Format(dblStudentAverage(i),
"Fixed")).PadLeft(12))
rtbOutput.AppendText(" " & strStudentLevel(i))
rtbOutput.AppendText(vbNewLine)
Next
End Sub

Private Sub cmdCreateFile_Click(sender As Object, e As EventArgs) Handles


cmdCreateFile.Click
Dim swrFile As System.IO.StreamWriter
Dim strOutput As String
swrFile = IO.File.CreateText("student_progress.dat")
sortNames()
getStudentAverages()
getStudentLevels()
For i = 0 To intRecTotal
For j = 0 To intRecTotal
strOutput = ""
If LCase(structRecords(j).strName) = strNamesSorted(i) Then
strOutput &= structRecords(j) .strID & ","
strOutput &= structRecords(j) .strName & ","
strOutput &= Convert.ToString(Format (dblStudentAverage(j),
"Fixed")) & ","
strOutput &= strStudentLevel(j)
swrFile.WriteLine(strOutput)
End If
Next
Next
swrFile.Close()
MsgBox("Output file created.")
End Sub

Private Sub cmdExit_Click(sender As Object, e As EventArgs) Handles


213
cmdExit.Click
End
End Sub

End Class

The main disadvantage of arrays is that the size of an array is fixed. This is not a problem if you know in
advance how many elements you will need. The size of an array holding the days of the week or the months of
the year, for example, will always have the same number of elements. If you have a group of related elements
whose size can vary (the number of books in a library, for example) you have to make your array large enough
to meet future needs, as well as any existing requirement.

Obviously, the larger an array is, the more space it occupies in memory, and the longer it takes to sort or
search through the array elements. Making an array twice as big as we actually need in order to allow for an
increase in the number of elements however, whilst prudent, is not a particularly efficient way to use memory,
especially if the amount of memory available is limited.

It is possible in Visual Basic to change the size of (redimension) an array programatically, as we have seen.
This is one way of getting around the problem of ensuring that our array will always be large enogh to
accomodate our requirements, but since it involves allocating a new (larger) block of memory and moving the
entire contents of the array from its existing location to the new block of memory, this is again not a particularly
efficient use of memory - or processor time (we will be look at alternatives to the use of arrays elsewhere).

Using Visual Basic's Timers

The Timer is a built-in Visual Basic control that allows you to execute code after a specific amount of time has
elapsed from the moment at which the timer is enabled, or repeatedly at specific time intervals. Once enabled,
the timer will generate a Tick event at predetermined intervals.

The time that elapses between ticks is specified by the Timer object's Interval property, which can be set at
design time or programmatically. The length of the interval is specified in milliseconds (a millisecond is one
thousandth of a second, or 0.001 seconds). The default interval is 100 milliseconds, but the range of possible
values is 1 to 2,147,483,647.

214
A timer is typically used in a Visual Basic program by creating an event handler for its Tick event (the event
handler contains code that will be executed when a Tick event occurs). The event handler's code will only
execute if the Timer object is enabled. The timer is enabled by setting its Enabled property to True, either
programmatically or at design time.

When a Timer object is first added to a form, the Enabled property is set to False by default. The Timerobject
has a Start() method that sets its Enabled property to True, and a Stop() method that sets
the Enabled property to False.

The Timer object does not actually appear on the form itself, either in design mode or when debugging /
executing the program. It is only visible in the Component Tray (the grey area at the bottom of the form design
window). We will create a simple clock-calendar program to demonstrate how the timer may be used.

1. Create a new project called "Clock-Calendar", and create an interface that looks something like the
illustration below. Note that we have used labels rather than text boxes for the actual time and date -
they will be used only to display information, not to accept user input.

The clock-calendar interface

2. In the toolbox, find the Timer object (under the Components tab) and double click on it to place
a Timer control in your application. You will see it appear in the form design window's Component Tray.
3. Set the control names as shown in the table below.

Clock-Calendar Form Controls

Control Name

Form frmClockCalendar

215
Clock-Calendar Form Controls

Control Name

Top label lblCurrentTime


Bottom label lblCurrentDate
Button cmdExit
Timer tmrClock

4. In the form design window, click once on the Timer object in the Component Tray to access its
properties window, then change the Interval property to "1000" and the Enabled property to "True".
5. Double-click on the Exit button to switch to the code window, and add the following command to the
event handler that has been created for you:

End

6. Return to the form design window and double click somewhere within the body of the form to create the
form's Load event handler, then enter the code below at the cursor (the Now command retrieves the
current date and time information, while the Format() function puts the information into the required
format).

lblCurrentTime.Text = Format(Now, "Long Time")


lblCurrentDate.Text = Format(Now, "Long Date")

7. The application now displays the correct date and time when we run it, but the clock does not update
itself. To make this happen, return to the form design window and double click on the Timerobject in
the Component Tray to create the timer's Tick event handler, then enter the following code at the
cursor:

lblCurrentTime.Text = Format(Now, "Long Time")


lblCurrentDate.Text = Format(Now, "Long Date")

Run the application and you should see the correct time and date (see the illustration below). The code we
added to the Tick event will cause the time and date information to be updated once every second.

216
The clock-calendar shows the current time and date

The Clock-Calendar application is a somewhat trivial example of how a timer is used in an application, but
there are many examples in our everyday lives of real-world situations in which timers perform a crucial role.
The wash-cycle of a washing machine is one example of a process that is controlled by a timer. Automatic
doors in buildings are also controlled by timers (if their proximity sensors do not detect a person in close
proximity to the doors, they will automatically close themselves after a predetermined interval).

Perhaps one of the most ubiquitous examples of how timers impact on our daily life, whether we use a vehicle
to get from place to place or travel on foot, is the traffic light. The timing circuits on a set of traffic lights must
function perfectly, or there is a good chance of an accident occurring! We will now create a simulation to
demonstrate how timers might be used to control a set of four traffic lights at a small crossroads. One of the
two intersecting roads runs from North to South, while the other runs from East to West.

1. Create a new project called "TrafficLights" and create an interface that includes four PictureBoxobjects,
each 61 pixels wide and 180 pixels high, as shown below.

217
The Traffic Light program interface

2. Set the control names as shown in the table below.

Traffic Light Form Controls

Control Name

Form frmTrafficLights
Button1 cmdStart
Button2 cmdPause
Button3 cmdReset
Button4 cmdExit
PictureBox1 picNorth
PictureBox2 picSouth
PictureBox3 picEast
PictureBox4 picWest

218
3. Save your work (if you have not already done so) to create the project folder for the "TrafficLights"
application.
4. Each traffic light will be represented at any given time by one of four images, each of which represents
on possible state for the light, i.e. Red, Red and Amber, Green, or Amber. Download the images for this
application from the link below and unzip the images into your project folder.
Download the images here.
5. We are going to add the images to the project as resources. From the Project menu select TrafficLight
Properties.... You should see a screen that looks like the illustration below.

The TrafficLight project Properties window

6. Click on the Resources tab. You should see the following screen:

219
You can add resources to a project in this window

7. Click on the drop-down arrow next to Add a Resource, select Add Existing file..., browse to the project
folder and select the image file traffic_lights_01.bmp. You should now see a screen like the one shown
below.

220
The first image file has been added to the project's resources

8. Repeat the above procedure for the remaining three image files.
9. Return to the form design window and double click on the form to create the form's Load event handler.
10. At the cursor, insert the following code:

picNorth.Image = My.Resources.traffic_lights_01
picSouth.Image = My.Resources.traffic_lights_01
picEast.Image = My.Resources.traffic_lights_01
picWest.Image = My.Resources.traffic_lights_01
cmdPause.Enabled = False
cmdReset.Enabled = False

11. Run the program to see the result of your efforts so far. You should see a screen like the one shown
below.

221
The TrafficLight application screen

12. Add two Timer objects to your form and rename them tmrNS and tmrEW.
13. Set the Interval property for both timers to "1000".

In their initial state, both pairs of traffic lights (North-South and East-West) are set to Red. This is the default
condition and means that no traffic will be moving initially. The normal sequence for traffic lights
is Red (stop), Red and Amber (get ready), Green (go), Amber (prepare to stop) and Red (stop) once more.

While one pair of lights goes through this sequence, the other pair must remain on Red to ensure that traffic
flows across the intersection from North to South and vice versa, or from East to West and vice versa, but not
both at the same time. In real life, the timing used can vary from one set of traffic lights to another depending
on the amount of traffic that goes through the intersection on a daily basis, on the time of day (since traffic
flows can vary considerably throughout the day), and on the day of the week (weekdays are often busier than
weekends).

222
For the purposes of this application we will assume that there is a uniform flow of traffic through the
intersection in all four directions. Each pair of traffic lights will be sequenced as follows:

I. Red - 3 seconds
II. Red and Amber - 3 seconds
III. Green - 10 seconds
IV. Amber - 3 seconds

Although the order is not particularly important, we will start proceedings with the North-South pair of lights.
When this pair of lights has completed one cycle, during which the East-West pair will remain in
the Redcondition, it will return itself to the Red condition. The East-West pair of lights will then commence its
own cycle, during which the North-South pair of lights will remain in the Red condition. The entire sequence is
then repeated.

The simulation is achieved by changing the images displayed by each PictureBox object as required in order
to reflect the current status of each pair of lights. Since a timer simply "ticks" at pre-determined intervals and
does not actually keep track of how many times it has ticked, we need to create variables to store this
information.

14. Add the following line of code, immediately following the frmTrafficLights class declaration:

Dim NSCount, EWCount As Integer

15. Add the following code at the start of the form's Load procedure:

NSCount = 0
EWCount = 0

16. Return to the form design window and double-click on the Start button to create the event handler for
the button's Click event.
17. At the cursor, enter the following code:

If NSCount = 0 And EWCount = 0 Then


tmrNS.Start()
ElseIf NSCount > 0 Then
tmrNS.Start()
Else
tmrEW.Start()
End If
223
cmdPause.Enabled = True
cmdReset.Enabled = True
cmdStart.Enabled = False

18. Return to the form design window and double-click on the tmrNS timer object in the Component Tray to
create the event handler for the timer's Tick event.
19. At the cursor, enter the following code:

NSCount += 1
If NSCount = 3 Then
picNorth.Image = My.Resources.traffic_lights_02
picSouth.Image = My.Resources.traffic_lights_02
ElseIf NSCount = 6 Then
picNorth.Image = My.Resources.traffic_lights_03
picSouth.Image = My.Resources.traffic_lights_03
ElseIf NSCount = 16 Then
picNorth.Image = My.Resources.traffic_lights_04
picSouth.Image = My.Resources.traffic_lights_04
ElseIf NSCount = 19 Then
picNorth.Image = My.Resources.traffic_lights_01
picSouth.Image = My.Resources.traffic_lights_01
NSCount = 0
tmrNS.Stop ()
tmrEW.Start ()
End If

Clicking the Start button sets the Enabled property of tmrNS or tmrEW to True. Which timer is started
depends on whether this is a new start, or whether the program is already running. If the program is running
but has been paused, the code must determine which timer was last active.

When a timer is enabled, the Tick event handler will execute once for each time interval of 1000 milliseconds
(1 second). It also enables the Pause and Reset buttons, and disables the Start button. The
variable NSCount is incremented by one each time this code is executed. When the value
of NSCountreaches 3 (a condition checked for by the first If statement), the existing images
for picNorth and picSouth (showing the traffic lights in the Red condition) are swapped for copies of images
showing the traffic lights in the Red and Amber condition.

224
The code within the second If statement is triggered when NSCount reaches 6, replacing the Red and
Amber traffic lights with Green traffic lights. The third If statement is activated when NSCount reaches 16and
changes the traffic light images to Amber.

The last If statement is activated when NSCount reaches 19, and restores the Red traffic light images. It also
resets NSCount to 0, disables tmrNS (by calling its stop() procedure), and enables tmrEW (by calling
its start() procedure) .

20. Return to the form design window and double-click on the tmrEW timer object in the Component Tray to
create the event handler for the timer's Tick event (this will be very similar to the code for tmrNS).
21. At the cursor, enter the following code:

EWCount += 1
If EWCount = 3 Then
picEast.Image = My.Resources.traffic_lights_02
picWest.Image = My.Resources.traffic_lights_02
ElseIf EWCount = 6 Then
picEast.Image = My.Resources.traffic_lights_03
picWest.Image = My.Resources.traffic_lights_03
ElseIf EWCount = 16 Then
picEast.Image = My.Resources.traffic_lights_04
picWest.Image = My.Resources.traffic_lights_04
ElseIf EWCount = 19 Then
picEast.Image = My.Resources.traffic_lights_01
picWest.Image = My.Resources.traffic_lights_01
EWCount = 0
tmrEW.Stop ()
tmrNS.Start ()
End If

22. Add the following code to the program's Pause button:

If NSCount > 0 Then


tmrNS.Stop ()
cmdPause.Enabled = False
cmdStart.Enabled = True
ElseIf EWCount > 0 Then
tmrEW.Stop ()
cmdPause.Enabled = False
225
cmdStart.Enabled = True
End If

23. Finally, add the code below to the program's Reset button and run the program to check its operation.

picNorth.Image = My.Resources.traffic_lights_01
picSouth.Image = My.Resources.traffic_lights_01
picEast.Image = My.Resources.traffic_lights_01
picWest.Image = My.Resources.traffic_lights_01
tmrNS.Enabled = False
tmrEW.Enabled = False
NSCount = 0
EWCount = 0
cmdPause.Enabled = False
cmdReset.Enabled = False
cmdStart.Enabled = True

226
The TrafficLight application in operation

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.
3. Create an interface like the one illustrated below (this is both the application's login form and its opening
screen).

227
The MathTest login form interface

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

frmMathTest Form Controls

Control Name Additional Properties

Form frmMathTest Icon: mathtest.ico


Size: 275, 175
Text: "MathTest - Login"
Label lblLoginInstruction Location: 12, 14
Text: "Please enter username and password to log in:"
Label lblUserName Location: 12, 44
Text: "Username:"
TextAlign: MiddleRight
Label lblPassword Location: 12, 69
Text: "Password:"
TextAlign: MiddleRight
TextBox txtUserName Location: 77, 40
MaxLength: 15
Size: 166, 20
TextBox txtPassword Location: 77, 65
MaxLength: 15
Size: 166, 20
Button cmdLogin Location: 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

228
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.

5. Click on the Project menu and select Add Module...


6. Create a code module with the name "modMain"
7. 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

229
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.

230
8. Click on the Project menu and select Add Windows Form...
9. Name the form frmMenu
10. Create an interface like the one illustrated below.

The MathTest menu form interface

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

frmMenu Form Controls

Control Name Additional Properties

Form frmMenu Icon: mathtest.ico


Size: 300, 360
Text: "MathTest - Main Menu"
Panel pnlSelectTestType Location: 12, 10
Size: 260, 110
Label lblSelectType Location: 15, 15
Text: "Select a test type:"
RadioButton radAdd Location: 150, 15
Text: "Addition"
RadioButton radSub Location: 150, 37
Text: "Subtraction"

231
frmMenu Form Controls

Control Name Additional Properties

RadioButton radMul Location: 150, 59


Text: "Multiplication"
RadioButton radDiv Location: 150, 81
Text: "Division"
Panel pnlSelectLevel Location: 12, 130
Size: 260, 90
Label lblSelectLevel Location: 15, 15
Text: "Select a difficulty level:"
RadioButton radEasy Location: 150, 15
Text: "Easy"
RadioButton radMode Location: 150, 37
Text: "Moderate"
RadioButton radHard Location: 150, 59
Text: "Hard"
Label lblResults Font: Microsoft Sans Serif, 8.25pt, style=Underline
ForeColor: Blue
Location: 12, 240
Text: "View your previous scores"
Button cmdStart Location: 197, 240
Text: "Start test"
Button cmdExit ForeColor: Red
Location: 197, 287
Text: "Exit"

12. Click on the Project menu and select Add Windows Form...
13. Name the form frmTestPage
14. Create an interface like the one illustrated below.

The MathTest test form interface

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

frmTestPage Form Controls

Control Name Additional Properties

Form frmTestPage Icon: mathtest.ico


Size: 440, 160
Text: "MathTest - Test Page"
Label lblQNo AutoSize: False
Location: 15, 24
Size: 90, 22
Text: "Question No:"
TextAlign: MiddleLeft
Label lblOp1 AutoSize: False
BorderStyle: FixedSingle
Location: 116, 25
Size: 30, 20
Text: None
TextAlign: MiddleCenter
Label lblOperator AutoSize: False
Location: 157, 27
Size: 26, 16
Text: None
TextAlign: MiddleCenter
Label lblOp2 AutoSize: False
BorderStyle: FixedSingle
Location: 194, 25
Size: 30, 20
Text: None
TextAlign: MiddleCenter
Label lblEquals AutoSize: False
Location: 230, 25
Size: 26, 20
Text: "="
TextAlign: MiddleCenter
TextBox txtAnswer Location: 262, 25
Size: 50, 20
TextAlign: Centre
Button cmdSubmit Location: 323, 24
Size: 87, 23
Text: "Submit Answer"
Button cmdCancel ForeColor: Red
Location: 323, 80

233
frmTestPage Form Controls

Control Name Additional Properties

Size: 87, 23
Text: "Cancel Test"

16. Click on the Project menu and select Add Windows Form...
17. Name the form "frmTestResults"
18. Create an interface like the one illustrated below.

The MathTest results form interface

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

234
frmTestResults Form Controls

Control Name Additional Properties

Form frmTestResults Icon: mathtest.ico


Size: 620, 520
Text: "MathTest - Results Page"
Label lblHeader Font: Microsoft Sans Serif, 16pt, style=Bold
Location: 10, 10
Text: "Your Test Results:"
Label lblQ01 AutoSize: False
Font: Consolas, 10pt
Location: 10, 58
Size: 360, 16
Text: None
TextAlign: MiddleLeft
Label lblQ02 AutoSize: False
Font: Consolas, 10pt
Location: 10, 89
Size: 360, 16
Text: None
TextAlign: MiddleLeft
Label lblQ03 AutoSize: False
Font: Consolas, 10pt
Location: 10, 120
Size: 360, 16
Text: None
TextAlign: MiddleLeft
Label lblQ04 AutoSize: False
Font: Consolas, 10pt
Location: 10, 151
Size: 360, 16
Text: None
TextAlign: MiddleLeft
Label lblQ05 AutoSize: False
Font: Consolas, 10pt
Location: 10, 182
Size: 360, 16
Text: None
TextAlign: MiddleLeft
Label lblQ06 AutoSize: False
Font: Consolas, 10pt
Location: 10, 213
Size: 360, 16
Text: None
TextAlign: MiddleLeft
Label lblQ07 AutoSize: False
Font: Consolas, 10pt

235
frmTestResults Form Controls

Control Name Additional Properties

Location: 10, 244


Size: 360, 16
Text: None
TextAlign: MiddleLeft
Label lblQ08 AutoSize: False
Font: Consolas, 10pt
Location: 10, 275
Size: 360, 16
Text: None
TextAlign: MiddleLeft
Label lblQ09 AutoSize: False
Font: Consolas, 10pt
Location: 10, 306
Size: 360, 16
Text: None
TextAlign: MiddleLeft
Label lblQ10 AutoSize: False
Font: Consolas, 10pt
Location: 10, 337
Size: 360, 16
Text: None
TextAlign: MiddleLeft
PictureBox picRes01 Location: 389, 58
Size: 14, 14
PictureBox picRes02 Location: 389, 89
Size: 14, 14
PictureBox picRes03 Location: 389, 120
Size: 14, 14
PictureBox picRes04 Location: 389, 151
Size: 14, 14
PictureBox picRes05 Location: 389, 182
Size: 14, 14
PictureBox picRes06 Location: 389, 215
Size: 14, 14
PictureBox picRes07 Location: 389, 246
Size: 14, 14
PictureBox picRes08 Location: 389, 277
Size: 14, 14
PictureBox picRes09 Location: 389, 308
Size: 14, 14
PictureBox picRes10 Location: 389, 339
Size: 14, 14
236
frmTestResults Form Controls

Control Name Additional Properties

Label lblCAns01 AutoSize: False


Font: Consolas, 10pt
Location: 423, 58
Size: 170, 16
Text: None
TextAlign: MiddleLeft
Label lblCAns02 AutoSize: False
Font: Consolas, 10pt
Location: 423, 89
Size: 170, 16
Text: None
TextAlign: MiddleLeft
Label lblCAns03 AutoSize: False
Font: Consolas, 10pt
Location: 423, 120
Size: 170, 16
Text: None
TextAlign: MiddleLeft
Label lblCAns04 AutoSize: False
Font: Consolas, 10pt
Location: 423, 151
Size: 170, 16
Text: None
TextAlign: MiddleLeft
Label lblCAns05 AutoSize: False
Font: Consolas, 10pt
Location: 423, 182
Size: 170, 16
Text: None
TextAlign: MiddleLeft
Label lblCAns06 AutoSize: False
Font: Consolas, 10pt
Location: 423, 213
Size: 170, 16
Text: None
TextAlign: MiddleLeft
Label lblCAns07 AutoSize: False
Font: Consolas, 10pt
Location: 423, 244
Size: 170, 16
Text: None
TextAlign: MiddleLeft
Label lblCAns08 AutoSize: False
Font: Consolas, 10pt

237
frmTestResults Form Controls

Control Name Additional Properties

Location: 423, 275


Size: 170, 16
Text: None
TextAlign: MiddleLeft
Label lblCAns09 AutoSize: False
Font: Consolas, 10pt
Location: 423, 306
Size: 170, 16
Text: None
TextAlign: MiddleLeft
Label lblCAns10 AutoSize: False
Font: Consolas, 10pt
Location: 423, 337
Size: 170, 16
Text: None
TextAlign: MiddleLeft
Label lblResultMessage AutoSize: False
Location: 10, 400
Size: 375, 18
Text: None
TextAlign: MiddleRight
PictureBox picSymbol Location: 395, 389 Size: 40, 40
Label lblScore AutoSize: False
Location: 450, 400
Size: 60, 18
Text: "Test score:"
TextAlign: MiddleRight
Label lblTestScore AutoSize: False
BorderStyle: FixedSingle
Font: Microsoft Sans Serif, 10pt
Location: 514, 398
Size: 69, 23
Text: None
TextAlign: MiddleCenter
Button cmdResultsOK Location: 514, 447
Size: 69, 23
Text: "OK"

20. Click on the Project menu and select Add Windows Form...
21. Name the form "frmPreviousScores"
22. Create an interface like the one illustrated below.

238
The MathTest test status form interface

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

frmPreviousScores Form Controls

Control Name Additional Properties

Form frmPreviousScores Icon: mathtest.ico


Size: 330, 295
Text: "MathTest - Test Status"
Label lblHeader Font: Microsoft Sans Serif, 16pt, style=Bold
Location: 10, 10
Text: "Previous Test Results:"
Label lblType AutoSize: False
Font: Arial, 10pt, style=Bold
Location: 11, 57
Size: 120, 16
Text: "Test Type"
TextAlign: MiddleLeft
Label lblLevel AutoSize: False
Font: Arial, 10pt, style=Bold
Location: 137, 57
Size: 80, 16
Text: "Level"
TextAlign: MiddleCenter
Label lblScore AutoSize: False
Font: Arial, 10pt, style=Bold
239
frmPreviousScores Form Controls

Control Name Additional Properties

Location: 223, 57
Size: 80, 16
Text: "Score"
TextAlign: MiddleCenter
Label lblAdd AutoSize: False
Font: Arial, 9pt
Location: 10, 88
Size: 120, 16
Text: "Addition"
TextAlign: MiddleLeft
Label lblSub AutoSize: False
Font: Arial, 9pt
Location: 10, 119
Size: 120, 16
Text: "Subtraction"
TextAlign: MiddleLeft
Label lblMul AutoSize: False
Font: Arial, 9pt
Location: 10, 150
Size: 120, 16
Text: "Multiplication"
TextAlign: MiddleLeft
Label lblDiv AutoSize: False
Font: Arial, 9pt
Location: 10, 181
Size: 120, 16
Text: "Division"
TextAlign: MiddleLeft
Label lblAddLevel AutoSize: False
Font: Arial, 9pt
Location: 137, 88
Size: 80, 16
Text: None
TextAlign: MiddleCenter
Label lblSubLevel AutoSize: False
Font: Arial, 9pt
Location: 137, 119
Size: 80, 16
Text: None
TextAlign: MiddleCenter
Label lblMulLevel AutoSize: False
Font: Arial, 9pt
Location: 137, 150
Size: 80, 16

240
frmPreviousScores Form Controls

Control Name Additional Properties

Text: None
TextAlign: MiddleCenter
Label lblDivLevel AutoSize: False
Font: Arial, 9pt
Location: 137, 181
Size: 80, 16
Text: None
TextAlign: MiddleCenter
Label lblAddScore AutoSize: False
Font: Arial, 9pt
Location: 223, 88
Size: 80, 16
Text: None
TextAlign: MiddleCenter
Label lblSubScore AutoSize: False
Font: Arial, 9pt
Location: 223, 119
Size: 80, 16
Text: None
TextAlign: MiddleCenter
Label lblMulScore AutoSize: False
Font: Arial, 9pt
Location: 223, 150
Size: 80, 16
Text: None
TextAlign: MiddleCenter
Label lblDivScore AutoSize: False
Font: Arial, 9pt
Location: 223, 181
Size: 80, 16
Text: None
TextAlign: MiddleCenter
Button cmdOK Location: 120, 221
Text: "OK"

24. Click on the Project menu and select Add Windows Form...
25. Name the form "frmAdmin"

26. Create an interface like the one illustrated below.

241
The MathTest admin form interface

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

frmAdmin Form Controls

Control Name Additional Properties

Form frmAdmin Icon: mathtest.ico


Size: 248, 205
Text: "MathTest - Admin"
Button cmdAddUser Location: 12, 12
Text: "Add User"
Button cmdDelUser Location: 12, 41
Text: "Delete User"
Button cmdExit ForeColor: Red
Location: 145, 132
Text: "Exit"

28. 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()

242
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)
243
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

29. 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).
30. 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) _

244
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
245
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
246
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()
247
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

31. 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) _

248
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
249
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
250
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
251
'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

32. 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 = _

252
"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
253
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
254
'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
255
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

33. 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.
34. 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 frmPreviousScoresand add the
following code in the body of the form's class definition:

256
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

257
35. 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"

The current selection is Subtraction (Easy)

258
The questions at this level are relatively easy

Even I can get ten out of ten!

259
So far so good . . .

36. 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
260
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
261
End If
End Sub

Private Sub cmdExit_Click(sender As Object, e As EventArgs) _


Handles cmdExit.Click
End 'Exit the program immediately
End Sub

37. 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.

262
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).

263
Paint Program

Visual Basic has an extensive range of features for creating graphics. Using the appropriate commands, it is
possible to draw shapes and lines, to create and manipulate images, to intermingle text and graphics, and to
utilise a vast range of colours. The subject of computer graphics, and of the graphics capabilities of Visual
Basic in particular, is worthy of a book or website in its own right.

We will not delve too deeply into the capabilities of Visual Basic's graphics library in these pages. We will
however explore some of its useful features by developing a fully functional application for creating graphic
images. The program is based on the Microsoft Windows Paint program. It allows the user to create simple
graphic images and save them to a file.

The functionality of the program has been limited in order to simplify the design of the interface and reduce the
amount of coding required, so there is plenty of scope for further experimentation. Even so, the application is
far from trivial, so we have a fair amount of work to do.

1. Open a new project called "EasyPaint".


2. In the Solution Explorer window, rename the file "Form1.vb" to "frmEasyPaint.vb".
3. Set the form's properties as follows:

Main Form Properties

Property Value

Text "EasyPaint"
Size 640, 600

4. In the toolbox, expand the Menus & Toolbars pane, and double-click the MenuStrip item to create a
menu strip control on your form (by default, the menu strip will dock with the top of your form).
5. In the control's Properties window, rename the menu strip control from "MenuStrip1" to
"mnuEasyPaint".
6. Click on the menu strip (where it says "Type Here") and type the word "File" to create the
application's File menu.
7. As you type, you will see a second text box appear below and slightly to the right of the first one. In this
box, type the word "New" to add this item to the application's File menu.

264
8. Repeat the previous step two more times to add the items "Save" and "Exit" to the File menu (don't
worry about the code for these menu items just yet - we will be adding it later).
9. In the toolbox, expand the Dialogs pane and double-click the FontDialog item. When when the font
dialog control appears below your form, rename it from "FontDialog1" to "dlgFont".
10. Repeat the previous step to create a SaveFileDialog control, and rename it from "SaveFileDialog1" to
"sfdSavePic".
11. In the toolbox, expand the Containers pane and double-click the Panel item (you will find this under All
Windows Forms) to place a panel control on your form.
12. Set the properties of the panel control as shown in the table below.

pnlMain Control Properties

Property Value

Name pnlMain
Enabled False
Location 0, 24
Size 624, 538

13. Create a second panel control and set its properties as shown below (Caution: before you create the
panel, make sure that the object currently selected in the form designer is the application's main form -
this is important to ensure that the new panel becomes a child of the main form, not the first panel (a
control's location is always specified relative to its parent control).

pnlFrame Control Properties

Property Value

Name pnlFrame
BorderStyle FixedSingle
Location 9, 49
Size 482, 482

14. Now create a PictureBox control on your form (see the cautionary note above) with the following
properties:

265
picCanvas Control Properties

Property Value

Name picCanvas
BackColor Gray
Location 10, 50
Size 480, 480

Your form should now look like the illustration below.

266
The EasyPaint program interface

The form does not yet have much in the way of controls, but we have created a drawing area. The drawing
area is is defined by the PictureBox control picCanvas, which will be assigned a bitmap image. It is the
bitmap image, rather than the control itself, that we will draw on. This means that turning the images we create
into files relatively easy (we will elaborate on how this is done later). The PictureBox control serves both to
define the boundaries of the drawing area and to provide a background for the lines, shapes, text, and
freehand brush strokes drawn by the user.

267
The pnlMain control sits behind all of the other controls on the form (including those not yet created) except for
the mnuEasyPaint control, and effectively becomes the parent object for them. We have intentionally disabled
this panel so that when the user opens the application the only control that will be available to them is
the File menu.

The pnlFrame control sits behind picCanvas, and is larger by two pixels in each direction. Its sole purpose is
to provide a frame for picCanvas, and its BorderStyle property is set to FixedSingle, whereas that
of picCanvas is set to None (we will explain the reason for this later).

We will now create controls to allow the user to choose the text size and font for their drawings (note that the
remaining controls will be assigned coordinates relative to the pnlMain control).

14. With the pnlMain control selected, go to the toolbox and double-click on the Label item.
15. Set the Label control's properties as follows:

lblEditText Control Properties

Property Value

Name lblEditText
Location 12, 6
Text Insert text:

16. Make sure the pnlMain control is selected, then go to the toolbox and double-click the TextBoxitem.
17. Set the TextBox control's properties as follows:

txtInsertText Control Properties

Property Value

Name txtInsertText
Location 75, 3
Size 225, 20

18. With pnlMain selected, go to the toolbox and double-click the Label item.
19. Set the Label control's properties as follows:

268
lblFontDetails Control Properties

Property Value

Name lblFontDetails
Location 317, 6
Text (Arial 12pt, Regular)

20. With pnlMain selected, go to the toolbox and double-click the Button item.
21. Set the Button control's properties as follows:

cmdFont Control Properties

Property Value

Name cmdFont
Location 512, 485
Size 97, 23
Text Select Font

Later, we will be adding some code to the cmdFont button that opens the FontDialog control to allow the user
to select from the fonts available on their computer. The selection will be reflected in the contents of the label
control lblFontDetails, which is set to display "(Arial 12pt, Regular)" by default.

The text box will be used to enter any text that the user wants to insert into their image. The programming can
wait for the moment, however. Next, we will create the controls that allow the user to select a colour for
whatever tool they are currently using. For this application, we have limited the palette to sixteen colours, but
there is no reason it could not be extended to the full range of available colours (up to a little over sixteen
million colours, in fact!) with a little more work.

22. With the pnlMain control selected, go to the toolbox and double-click the Label item.
23. Set the Label control's properties as follows:

269
lblBlack Control Properties

Property Value

Name lblBlack
AutoSize False
BackColor Black
BorderStyle FixedSingle
Location 499, 47
Size 23, 23
Text None

This control is the first of sixteen colour selector controls. The remaining controls will share many of the same
property values, with the only differences between them being the Name, BackColor and Locationproperties.

24. Copy and paste the lblBlack control onto the picCanvas PictureBox control to create fifteen new
colour selector controls and drag them into approximately the right position (see the illustration further
down this page). Set the properties as specified in the table below. Note: make sure
the picCanvas control is selected before you paste each copy, or you will probably find it has been
pasted behind the picCanvas control and you will not be able to drag it into position.

Colour Selector Control Properties

Name BackColor Location

lblNavy Navy 528, 47


lblGreen Green 557, 47
lblTeal Teal 586, 47
lblMaroon Maroon 499, 82
lblPurple Purple 528, 82
lblOlive Olive 557, 82
lblSilver Silver 586, 82
lblGray Gray 499, 117
lblBlue Blue 528, 117
lblLime Lime 557, 117
lblCyan Cyan 586, 117
lblRed Red 499, 152
lblFuchsia Fuchsia 528, 152
lblYellow Yellow 557, 152
270
Colour Selector Control Properties

Name BackColor Location

lblWhite White 586, 152

25. With the pnlMain control selected, go to the toolbox and double-click the Label item.
26. Set the Label control's properties as follows:

lblColor Control Properties

Property Value

Name lblColor
AutoSize False
BackColor Black
BorderStyle FixedSingle
Location 499, 185
Size 110, 45
Text None

27. With the pnlMain control selected, go to the toolbox and double-click the Label item.
28. Set the Label control's properties as follows:

lblColourSelected Control Properties

Property Value

Name lblColorSelected
Location 497, 27
Text Colour: Black

Your application's user interface should now look like the illustration below.

271
The EasyPaint program interface with its colour selection controls

Once we have coded the event handler for the colour selector controls, the user will be able to click on any one
of the sixteen available selections to choose a colour, which will then be used by whatever drawing tool is
chosen (including text). The large colour box underneath the colour selector controls will change to whatever
the currently selected colour is, and the label above the colour selector controls will display the colour's name.
The name of the colour selected will also be stored in a global program variable so that the various tools can
be coded to output text or graphics in the correct colour.

272
The next task is to create controls to allow the user to select a drawing tool. The tool selection controls will
again be labels, each of which will display an image, allowing the user to see at a glance what kind of tools are
available. We have created a zipped file called easy_paint.zip that contains the images for the tool selection
controls. The file can be downloaded from the link below.

Download the easy_paint.zip file here.


29. If you haven't already done so, save your project to create the project directory.
30. Download the tool selector control images using the link above and unzip them into your project
directory.
31. With the pnlMain control selected, go to the toolbox and double-click the Label item.
32. Set the Label control's properties as shown below. To select an image for the Image property, click on
the property's browse button, select the radio button labelled Local Resource:, click on
the Import button, and browse to the file "brush.gif" (this should be located in your project folder).

lblBrush Control Properties

Property Value

Name lblBrush
AutoSize False
BorderStyle Fixed3D
Image brush.gif
Location 499, 262
Size 36, 36
Tag Brush
Text None

This control is the first of eleven tool selector controls. The remaining controls will share many of the same
property values, with the only differences between them being the Name, Image, Location and Tagproperties.

33. Copy and paste the lblBrush control onto the picCanvas PictureBox control to create the rest of the
tool selector controls (there will be ten more in all). Drag them approximately into position (see the
illustration further down this page), then assign the properties specified below.

Note: make sure the picCanvas control is selected before you paste each copy, or you will probably find
it has been pasted behind the picCanvas control and you will not be able to drag it into position.

273
Tool Selector Control Properties

Name Image Location Tag

lblLine line.gif 536, 262 Line


lblText text.gif 573, 262 Text
lblSqOut square_outline.gif 499, 302 Square
lblSqFill square_filled.gif 536, 302 SquareFilled
lblRecOut rectangle_outline.gif 573, 302 Rect
lblRecFill rectangle_filled.gif 499, 342 RectFilled
lblCircOut circle_outline.gif 536, 342 Circ
lblCircFill circle_filled.gif 573, 342 CircFilled
lblEllOut ellipse_outline.gif 499, 382 Ellipse
lblEllFill ellipse_filled.gif 536, 382 EllipseFilled

34. With the pnlMain control selected, go to the toolbox and double-click on the Label item.
35. Set the Label control's properties as follows:

lblToolSelected Control Properties

Property Value

Name lblToolSelected
Location 497, 243
Text Tool: none selected

Your application's user interface should now look like the illustration below.

274
The EasyPaint program interface with its tool selection controls

The remaining controls will allow the user to select the brush size (for freehand painting) and the pen width (for
drawing lines and outline shapes) . Both controls consist of a combo box that allows the user to choose from a
range of values (1 to 32 for the brush size, and 1 to 12 for the pen width).

36. With the pnlMain control selected, go to the toolbox and double-click the Label item.
37. Set the Label control's properties as follows:

275
lblBrushSize Control Properties

Property Value

Name lblBrushSize
AutoSize False
Location 496, 435
Size 71, 13
Text Brush size:
TextAlign MiddleRight

38. With the pnlMain control selected, go to the toolbox and double-click on the ComboBox item.
39. Set the ComboBox control's properties as follows:

cmbBrushSize Control Properties

Property Value

Name cmbBrushSize
Items 1-32*
Location 573, 432
Size 36, 21
Text 6

*In the Items property box, click on the browse button. You will see a dialog box like the one illustrated below.
Type in the values (1-32) , one per line, Then click OK.

276
The ComboBox control's String Collection Editor

40. With the pnlMain control selected, go to the toolbox and double-click the Label item.
41. Set the Label control's properties as follows:

lblPenWidth Control Properties

Property Value

Name lblPenWidth
AutoSize False
Location 496, 462
Size 71, 13
Text Pen Width:
TextAlign MiddleRight

42. With the pnlMain control selected, go to the toolbox and double-click the ComboBox item.
43. Set the ComboBox control's properties as follows:

277
cmbPenWidth Control Properties

Property Value

Name cmbPenWidth
Items 1-12
Location 573, 459
Size 36, 21
Text 2

That's it for the interface design! We can now add the code. First, run the application to see what it looks like.
You should see something very similar to the illustration below. Note that the controls (apart from
the File menu) are disabled when the program first opens. That is as it should be.

278
The completed EasyPaint program interface

44. Switch to the code editor window and enter these declarations for module variables within the body of
the frmEasyPaint class:

Dim G As Drawing.Graphics
Dim drawFlag As Boolean = False
Dim xDown, yDown, xUp, yUp As Integer
Dim intL, intR, intT, intB, intW, intH As Integer
Dim clrSelected As Color = Color.Black
Dim intToolSelected As Integer = 0
Dim intBrushSize As Integer = 6
279
Dim intPenWidth As Integer = 2
Dim intFontSize As Integer = 12
Dim strText As String
Dim strFont As String = "Arial"
Dim styFontStyle As FontStyle
Dim strFontStyleArray() As String = {"Regular", "Bold", "Italic", "Bold
Italic", "Unknown", _
"Unknown", "Unknown", "Unknown", "Regular Strikeout", "Bold Strikeout", _
"Italic Strikeout", "Bold Italic Strikeout", "Regular Underline Strikeout",
_
"Bold Underline Strikeout", "Italic Underline Strikeout", _
"Bold Italic Underline Strikeout"}
Dim bmpPic As Bitmap

Some explanations are required here. The first line of code declares a variable (G) of type Graphics.
The Graphics class defines an incredible number of methods for drawing and manipulating gaphic objects. An
object of this type inherits those methods, allowing us to call on them for the purposes of drawing graphics in
our application. We can associate a control (for example a form or a picture box) with this variable, and by so
doing effectively turn the control into a canvas on which to draw.

As you will see, we are going to create a bitmap image and draw graphics to it, which will make the task of
saving our completed artwork to a file relatively easy. The next variable is a Boolean variable called drawFlag,
which is set to True when the mouse is down and False when the mouse is up. Essentially, whatever drawing
tool we are currently using can only have an effect when the mouse is down (i.e. the user is holding down the
left mouse button).

The next four integer variables (xDown, yDown, xUp, and yUp) are used to track the screen positions
(relative to the drawing surface) at which a mouse button is pressed down and released respectively. Note that
for freehand drawing, we are interested in tracking the mouse in real time as it moves around the canvas. For
all other controls, we require a snapshot of the points at which the mouse button is clicked and subsequently
released.

The six integer variables declared on the next line (intL, intR, intT, intB, intW and intH) define the bounding
rectangle for all of the geometric shapes we will draw (and also define the start and end point for lines). They
represent the left and right x coordinates, the top and bottom y coordinates, and the width and height of the
rectangle respectively.

280
The clrSelected variable has the type Color, which stores the RGB values for a specific colour (although
many common colours are referenced using names like "Black", "Red", "Blue" etc.) This variable will be used
to keep track of the colour currently selected by the user, and when the program starts is set
to Color.Black (black) by default.

The next four variables are relatively self-explanatory. The integer variable intToolSelected tracks which
drawing tool the user has selected at any given time (a value of 0 means no tool is selected).
The intBrushSize and intPenWidth integer variables keep track of the brush size and pen width selected by
the user, and intFontSize keeps track of the font size selected for drawing text.

The string variable strText will hold text entered by the user, which will be added to the image if the user clicks
on the drawing surface while the text tool is selected. The strFont string variable holds the name of the
selected font face, and is initially set to "Arial". The styFontStyle variable is a specialised numeric data type
(FontStyle) that is used to enumerate the various style characteristics
(e.g. Italic, bold, underline, strikethrough etc.) and combinations thereof.

The last module variable (strFontStyleArray()) is an array of type String that holds text descriptions of the
various font styles and style combinations. It has displayed correct font information in the (admittedly limited)
testing carried out on the application, although documentation relating to the various font styles and their
enumerations has proved somewhat elusive (the information to hand has been gleaned through
experimentation with various fonts).

45. The New item in the File menu enables the user interface and allows the user to start selecting colours
and using the drawing tools. In the form design window, click once on the File menu to expose
the New menu item, then click once on New.
46. In the Properties window, change the name of the control to mnuFileNew.
47. Double-click on the New menu item to create its Click event handler, and enter the following code at the
cursor:

pnlMain.Enabled = True
picCanvas.BackColor = Color.White
bmpPic = New Bitmap(picCanvas.Width, picCanvas.Height)
picCanvas.Image = bmpPic
G = Graphics.FromImage(bmpPic)
picCanvas.DrawToBitmap(bmpPic, picCanvas.ClientRectangle)

281
This relatively short piece of code activates the application by setting the Enabled property of the panel control
(pnlMain) to True. It also sets the background colour of the picture box control to white, setting up a "fresh
canvas" for the user's artistic endeavours. The third line of code creates a bitmap image (bmpPic) of the same
size as the picture box, the fourth line assigns the bitmap image it to the Image property of picCanvas, and
the fifth line of code links it with the graphics object (G), which means that any drawing operations invoked
with G will draw directly to the bitmap image bmpPic.

The last line looks rather cryptic, but what it does is take the white rectangle that was drawn on picCanvasby
the system when we set its background colour to white, and draw it into the the bitmap image (which is initially
empty). That is why we elected not to display a border for our PictureBox control, since this would also have
appeared in the bitmap image.

To see the effect of this code, run the application, open the File menu, and click on the New menu item. You
should see something like the screenshot below. All of the controls are now enabled (although most of them
will not do anything until we have added some more code).

282
The File►New menu item activates the interface controls

48. Before we can do any drawing, we need to get the palette, tool, and font selection controls working. We
will start with the palette selection controls. In the code editor, enter the following subroutine code in its
entirety:

Private Sub lblPalette_Click(sender As Object, e As EventArgs) _


Handles lblBlack.Click, lblNavy.Click, lblGreen.Click, lblTeal.Click, _
lblMaroon.Click, lblPurple.Click, lblOlive.Click, lblSilver.Click, _
lblGray.Click, lblBlue.Click, lblLime.Click, lblCyan.Click, _
lblRed.Click, lblFuchsia.Click, lblYellow.Click, lblWhite.Click
lblColor.BackColor = sender.BackColor

283
clrSelected = sender.BackColor
Select Case clrSelected
Case Color.Black
lblColorSelected.Text = "Colour: Black"
Case Color.Navy
lblColorSelected.Text = "Colour: Navy"
Case Color.Green
lblColorSelected.Text = "Colour: Green"
Case Color.Teal
lblColorSelected.Text = "Colour: Teal"
Case Color.Maroon
lblColorSelected.Text = "Colour: Maroon"
Case Color.Purple
lblColorSelected.Text = "Colour: Purple"
Case Color.Olive
lblColorSelected.Text = "Colour: Olive"
Case Color.Silver
lblColorSelected.Text = "Colour: Silver"
Case Color.Gray
lblColorSelected.Text = "Colour: Gray"
Case Color.Blue
lblColorSelected.Text = "Colour: Blue"
Case Color.Lime
lblColorSelected.Text = "Colour: Lime"
Case Color.Cyan
lblColorSelected.Text = "Colour: Cyan"
Case Color.Red
lblColorSelected.Text = "Colour: Red"
Case Color.Fuchsia
lblColorSelected.Text = "Colour: Fuchsia"
Case Color.Yellow
lblColorSelected.Text = "Colour: Yellow"
Case Color.White
lblColorSelected.Text = "Colour: White"
End Select
End Sub

284
This event handler handles the Click event for all sixteen colour selection controls, and differentiates between
them by looking at the BackColor property. The first line of code within the body of the event handler sets the
background colour of the lblColor label control to the same colour as that of the colour selector control clicked
by the user. The second line of code sets the value of the global variable clrSelected (which is of type Color)
to that colour also.

The remainder of the code consists of a Select Case statement that tests to see which colour was selected by
the user and changes the text displayed by the lblColorSelected control accordingly. To see the effect of this
code, run the application, open the File menu, and click on the New menu item, then click on various colour
selector controls.

The next task is to write the event handler for the drawing tool selection controls. As with the colour selector
controls, a single event handler serves all of them.

49. In the code editor, enter the following subroutine code in its entirety:

Private Sub lblTool_Click(sender As Object, e As EventArgs) _


Handles lblBrush.Click, lblLine.Click, lblText.Click, lblSqOut.Click, _
lblSqFill.Click, lblRecOut.Click, lblRecFill.Click, lblCircOut.Click, _
lblCircFill.Click, lblEllOut.Click, lblEllFill.Click
resetTools()
Select Case sender.Tag
Case "Brush"
intToolSelected = 1
lblBrush.BorderStyle = BorderStyle.FixedSingle
lblToolSelected.Text = "Tool: Brush"
Case "Line"
intToolSelected = 2
lblLine.BorderStyle = BorderStyle.FixedSingle
lblToolSelected.Text = "Tool: Line"
Case "Text"
intToolSelected = 3
lblText.BorderStyle = BorderStyle.FixedSingle
lblToolSelected.Text = "Tool: Text"
Case "Square"
intToolSelected = 4
lblSqOut.BorderStyle = BorderStyle.FixedSingle

285
lblToolSelected.Text = "Tool: Square Outlined"
Case "SquareFilled"
intToolSelected = 5
lblSqFill.BorderStyle = BorderStyle.FixedSingle
lblToolSelected.Text = "Tool: Square Filled"
Case "Rect"
intToolSelected = 6
lblRecOut.BorderStyle = BorderStyle.FixedSingle
lblToolSelected.Text = "Tool: Rectangle Outlined"
Case "RectFilled"
intToolSelected = 7
lblRecFill.BorderStyle = BorderStyle.FixedSingle
lblToolSelected.Text = "Tool: Rectangle Filled"
Case "Circ"
intToolSelected = 8
lblCircOut.BorderStyle = BorderStyle.FixedSingle
lblToolSelected.Text = "Tool: Circle Outlined"
Case "CircFilled"
intToolSelected = 9
lblCircFill.BorderStyle = BorderStyle.FixedSingle
lblToolSelected.Text = "Tool: Circle Filled"
Case "Ellipse"
intToolSelected = 10
lblEllOut.BorderStyle = BorderStyle.FixedSingle
lblToolSelected.Text = "Tool: Ellipse Outlined"
Case "EllipseFilled"
intToolSelected = 11
lblEllFill.BorderStyle = BorderStyle.FixedSingle
lblToolSelected.Text = "Tool: Ellipse Filled"
End Select
End Sub

50. Note that the above subroutine calls another procedure called resetTools() which we have not yet
created, and will therefore flag up an error. To rectify this, add the following code to the form:

Sub resetTools()
lblBrush.BorderStyle = BorderStyle.Fixed3D
lblLine.BorderStyle = BorderStyle.Fixed3D
lblText.BorderStyle = BorderStyle.Fixed3D

286
lblSqOut.BorderStyle = BorderStyle.Fixed3D
lblSqFill.BorderStyle = BorderStyle.Fixed3D
lblRecOut.BorderStyle = BorderStyle.Fixed3D
lblRecFill.BorderStyle = BorderStyle.Fixed3D
lblCircFill.BorderStyle = BorderStyle.Fixed3D
lblCircFill.BorderStyle = BorderStyle.Fixed3D
lblEllOut.BorderStyle = BorderStyle.Fixed3D
lblEllFill.BorderStyle = BorderStyle.Fixed3D
End Sub

51. We will now code the controls that allow the user to select the brush size and pen width. In the code
editor, select the cmbBrushSize control using the drop-down list at the top centre of the code editor
window, then select the SelectedValueChanged event using the drop-down list at the top right-hand
side of the code editor window.
52. Enter the following command at the cursor (this command is executed when the user selects a new
value using the combo box and sets the module variable intBrushSize to the integer value represented
by the selection):

intBrushSize = CInt(cmbBrushSize.Text)

53. In the code editor, select the cmbPenWidth control using the drop-down list at the top centre of the
code editor window, then select the SelectedValueChanged event using the drop-down list at the top
right-hand side of the code editor window.
54. Enter the following command at the cursor (this command is executed when the user selects a new
value using the combo box and sets the module variable intPenWidth to the integer value represented
by the selection):

intPenWidth = CInt(cmbPenWidth.Text)

55. We will deal with the font selection next. In the form designer window, double click on the Select
Font button and enter the following code at the cursor:

dlgFont.ShowDialog()
strFont = dlgFont.Font.Name
styFontStyle = dlgFont.Font.Style
intFontSize = dlgFont.Font.Size
lblFontDetails.Text = "(" & strFont & " " & intFontSize & "pt, " &
strFontStyleArray(styFontStyle) & ")"

The above code opens the dlgFont dialog box to allow the user to select a font. Of the values returned by the
dialog box, the font name (a string value) is assigned to the string variable strFontName, the style (an
enumeration) is assigned to the style variable styFontStyle, and the font size (an integer value) is assigned to

287
the integer variable intFontSize. All of these variables have module scope, and can be accessed by any
procedure.

The information is then assembled into a string, which is assigned to the Text property of the
control lblFontDetails, which informs the user which font face, font size and font style are currently selected.
The text for the font style is selected from the string array strFontStyleArray() using the
numeric styFontStylevalue returned by the dialog box (you might recall we discussed the lack of
documentation on this topic earlier).

56. The next two procedures will enable us to actually do something useful with the application. In the code
editor, select the picCanvas control using the drop-down list at the top centre of the code editor window,
then select the MouseDown event using the drop-down list at the top right-hand side of the code editor
window.
57. Enter the code below at the cursor. This code sets the drawFlag variable to True to signal that drawing
commands may be executed, and records the location of the cursor on the drawing surface at the
moment when the mouse button is pressed.

drawFlag = True
xDown = e.X
yDown = e.Y

58. In the code editor, select the picCanvas control again using the drop-down list at the top centre of the
code editor window, then select the MouseMove event using the drop-down list at the top right-hand
side of the code editor window.
59. Enter the following code at the cursor.

If intToolSelected = 1 And drawFlag = True Then


xDown = e.X
yDown = e.Y
G.FillEllipse (New SolidBrush (clrSelected) , xDown, yDown, intBrushSize,
intBrushSize)
picCanvas.Refresh()
End If

The second of these two procedures only applies to freehand drawing with the brush tool. Like the first
procedure, it records the location of the cursor on the drawing surface at the moment when the mouse button
is pressed, but because the procedure executes whenever the mouse is moved (providing the brush tool is
selected and the drawFlag variable is set), then the mouse position is effectively tracked in real time.

288
As the mouse moves over the canvas, an ellipse is drawn at each new position recorded to create the
impression of a paintbrush moving across the drawing surface. The FillEllipse method of the Graphicsobject
uses the values stored in the clrSelected and intBrushSize variables to determine the colour and size
respectively of the ellipse drawn (which is in fact a circle because it uses intBrushSize for both dimensions of
the ellipse), and the xDown and yDown variables to determine where to draw each ellipse as the mouse
moves.

Run the application, select New from the File menu and try out the brush tool to see how it works. Note that we
have not yet written any code for the MouseUp event, so you will find that the only way to stop painting with
the brush is to select another tool!

The MouseUp event does most of the useful work of the application in that it draws shapes, lines and text
strings on the drawing surface. Before we write the MousUp event handler however, we need to consider the
behaviour associated with the various shape tools (the tools that draw squares, rectangles, circles and
ellipses) and how we want to implement them, since they will each require a slightly different approach.

Visual Basic draws shapes within a bounding rectangle defined by its top left hand corner, width and height.
The top left-hand corner is defined as a pixel offset from the top left-hand corner of the drawing surface (in our
case, this is the PictureBox object picCanvas). The width and height are also defined using pixel values. We
will create subroutines to set up the coordinates for the rectangle within which the different shapes will be
drawn, starting with the square.

60. Somewhere within the form's class definition, insert the following subroutine:

Sub dimSquare()
If xUp < 0 Then xUp = 0
If xUp > 480 Then xUp = 480
If yUp < 0 Then yUp = 0
If yUp > 480 Then yUp = 480
intW = Math.Abs(xUp - xDown)
intH = Math.Abs(yUp - yDown)
If intW > intH Then intW = intH
If xUp < xDown Then intL = xDown - intW Else intL = xDown
If yUp < yDown Then intT = yDown - intW Else intT = yDown
End Sub

289
The first four lines here are concerned with restricting the xUp and yUp coordinates to a point within the
bounding rectangle of the drawing surface. The next two lines call the Math.Abs() function to set absolute
values of intW and intH respectively (saving us the bother of having to work out whether to
subtract xUpfrom xDown, or the other way round, and so on).

The next line of code determines how big the square will be when it is drawn. Since it is a square, then by
definition the sides will be equal in length. We will (somewhat arbitrarily) limit the width and height to whichever
is the shorter of intW and intH. The square will be drawn within a bounding rectangle (or bounding square, in
this case) that extends left or right, up or down from the xDown and yDowncoordinates, depending on
whether the user drags the mouse left or right, up or down, after they have pressed the mouse button.

The last two lines of code determine where the top left hand corner of the bounding rectangle should be (this
will have the x and y coordinates intL and intT respectively).

61. The rectangle shape differs from the square slightly in that its width and height can have different values,
but the subroutine to find the bounding rectangle is very similar to that for the square. Somewhere within
the form's class definition, insert the following subroutine:

Sub dimRectangle()
If xUp < 0 Then xUp = 0
If xUp > 480 Then xUp = 480
If yUp < 0 Then yUp = 0
If yUp > 480 Then yUp = 480
intW = Math.Abs (xUp - xDown)
intH = Math.Abs (yUp - yDown)
If xUp < xDown Then intL = xDown - intW Else intL = xDown
If yUp < yDown Then intT = yDown - intH Else intT = yDown
End Sub

The first six lines of code are identical to the previous procedure. The only real differences are that the line of
code that reduces the value of intW to that of intH (if the latter is smaller) in the dimSquare() procedure has
been removed, and that the final line of code uses intH instead of intW to determine the y coordinate for the
top of the bounding rectangle.

62. For the circle shape, we have decided (another arbitrary decision!) to use
the xDown and yDowncoordinates as the centre of the circle, and the value of intW or intH (whichever
is greater) to determine the circle's diameter. Somewhere within the form's class definition, insert the
following subroutine:

290
Sub dimCircle()
If xUp < 0 Then xUp = 0
If xUp > 480 Then xUp = 480
If yUp < 0 Then yUp = 0
If yUp > 480 Then yUp = 480
intW = Math.Abs (xUp - xDown)
intH = Math.Abs (yUp - yDown)
If intW < intH Then intW = intH
intL = xDown - intW
intT = yDown - intW
intW *= 2
End Sub

The first six lines of the subroutine are identical to those used in the subroutines used for the square and the
rectangle (advocates of re-usable code will be straining at the leash to remove this code to a separate
subroutine - feel free to optimise the code!).

We are only going to use one of the dimensions (intW) to draw the circle, so the next line of code checks to
see if intW is smaller than intH. If so the value of intW is increased to match the value of intH. This dimension
represents the circle's radius, so the coordinates for the top left-hand corner of the bounding rectangle will
be intW pixels to the left of the point defined by the xDown and yDown coordinates, and the same distance
above it.

The penultimate two lines of code subtract intW from xDown and yDown to find intL and intY respectively.
The last line of code doubles the value of intW, so that the bounding rectangle (again, read bounding square)
for the circle extends for the same distance on either side, and above and below, the point defined as the
centre of the circle (xDown, yDown).

63. The ellipse shape differs from the circle only in that its width and height can have different values, so the
subroutine to find the bounding rectangle is very similar to that for the circle. Somewhere within the
form's class definition, insert the following subroutine:

Sub dimEllipse()
If xUp < 0 Then xUp = 0
If xUp > 480 Then xUp = 480
If yUp < 0 Then yUp = 0
If yUp > 480 Then yUp = 480
intW = Math.Abs (xUp - xDown)
291
intH = Math.Abs (yUp - yDown)
intL = xDown - intW
intT = yDown - intH
intW *= 2
intH *= 2
End Sub

The only real difference between dimCircle() and dimEllipse() are that dimEllipse() uses the values of both
the intW and intH variables to determine the position of the top left-hand corner of the bounding rectangle and
its size. Note that the dimensioning of shapes works the same regardless of whether the shapes are outlined
or filled. The real difference between the two types of shape is in the methods used to draw them by
the Graphics object.

The next piece of code we need to write is the MouseUp event handler for the picCanvas control, which will
enable us to demonstrate the various shape tool methods.

64. In the code editor, select the picCanvas control using the drop-down list at the top centre of the code
editor window, then select the MouseUp event using the drop-down list at the top right-hand side of the
code editor window.
65. Enter the following code at the cursor:

Dim brushFill As SolidBrush = New Drawing.SolidBrush(clrSelected)


Dim penLine As New Pen(clrSelected, intPenWidth)

drawFlag = False
xUp = e.X
yUp = e.Y

Select Case intToolSelected


Case 2
G.DrawLine(penLine, xDown, yDown, xUp, yUp)
Case 3
strText = txtInsertText.Text
G.DrawString(strText, New System.Drawing.Font(strFont, intFontSize,
styFontStyle), _
brushFill, xUp, yUp)
Case 4
dimSquare()
292
G.DrawRectangle(penLine, intL, intT, intW, intW)
Case 5
dimSquare()
G.FillRectangle(brushFill, intL, intT, intW, intW)
Case 6
dimRectangle()
G.DrawRectangle(penLine, intL, intT, intW, intH)
Case 7
dimRectangle()
G.FillRectangle(brushFill, intL, intT, intW, intH)
Case 8
dimCircle()
G.DrawEllipse(penLine, intL, intT, intW, intW)
Case 9
dimCircle()
G.FillEllipse(brushFill, intL, intT, intW, intW)
Case 10
dimEllipse()
G.DrawEllipse(penLine, intL, intT, intW, intH)
Case 11
dimEllipse()
G.FillEllipse(brushFill, intL, intT, intW, intH)
End Select
picCanvas.Refresh()

Here we see the tools used to draw shapes - the SolidBrush and the Pen. The SolidBrush is used to draw
the filled shapes, while the Pen is used to draw lines and outline shapes. Both accept a Colorargument, which
is set to clrSelected at the beginning of the procedure. The Pen method also accepts a Width argument that
determines the thickness of lines or the borders of any outline shapes drawn.

In our MouseUp() procedure, we use the value of intPenWidth. Although they have default values,
both clrSelected and intPenWidth can be set by the user at run time. The next three lines of code set
the drawFlag variable to False (signalling that no drawing tools are currently active), and set
the xUp and yUpvariables to the x and y coordinates (relative to the drawing surface) of the point at which the
mouse button was released. The remaining code mostly consists of a Select Case statement that checks to
see if a drawing tool is currently selected, and if so takes the appropriate action.

293
If the Line tool has been selected, the Drawline() method of the Graphics object draws the line with
the penLine object (which, if you remember, has been set to the currently selected colour and line width)
between the coordinates defined by xDown, yDown, and xUp, yUp.

If the Text tool is selected, whatever text is currently in the txtInsertText control's Text property (if any) is
drawn using the font face, font style, and font size selected by the user, starting at the position defined
by xUp, yUp.

If a shape tool is selected then - depending on which shape tool has been chosen - the appropriate subroutine
(dimSquare(), dimRectangle(), dimCircle() or dimEllipse()) will be called to set the bounding rectangle.

For outline squares or rectangles, the Graphic object's DrawRectangle() method is used with
the penLineobject, the only difference being that the width and height dimensions used for drawing a square
will be identical, whereas those used for drawing a rectangle will (usually) be different. For solid squares or
rectangles, the method used will be FillRectangle() and the object used to draw will be brushFill.

Circles and ellipses follow a similar patter, with the DrawEllipse() method used for the outline shapes
and FillEllipse() used for their solid counterparts.

The final line of code calls the Graphic object's <Refresh() method to make sure the changes made by the
program are displayed to the screen immediately.

66. The last task we must undertake is to finalise the File menu items and write the code that makes them
do something. The Save item in the File menu enables the user to save their artwork to a file. In the
form design window, click once on the File menu to expose the Save menu item, then click once
on Save.
67. In the Properties window, change the name of the control to mnuFileSave.
68. Double-click on the Save menu item to create its Click event handler, and enter the following code at
the cursor:

sfdSavePic.Filter = "Bitmap |*.bmp"


If sfdSavePic.ShowDialog = DialogResult.OK Then
picCanvas.Image.Save(sfdSavePic.FileName, Drawing.Imaging.ImageFormat.Bmp)
MsgBox("File saved.")
End If

294
This code uses the sfdSavePic SaveFileDialog object that we added to the form earlier, and sets up a filter
that will only allow the user to choose the bitmap file format. It will also add the ".bmp" file extension to any
files saved. What is actually being saved here is the picCanvas PictureBox control's Image, which is a
bitmap object, and to which all our graphics commands draw. To complete the application, there is one more
very trivial task:

69. In the form design window, click once on the File menu to expose the Exit menu item, then click once
on Exit.
70. In the Properties window, change the name of the control to mnuFileExit.
71. Double-click on the Exit menu item to create its Click event handler, and enter the following command
at the cursor:

End

295
The completed program in action

296

You might also like