Project Codes
Project Codes
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
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)
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:
End Sub
The code in your code editor window should now 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:
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.
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:
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!".
2. Open the module's code editor window, delete the existing subroutines, and enter the following code:
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:
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.
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:
End Sub
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:
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:
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.
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):
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:
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:
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:
Now make the following changes to the code for the Calculate button:
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.
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.
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;"
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.
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:
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:
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:
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:
Form Controls
19
Your form should look something like the illustration below.
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
The code so far should look like the illustration below.
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 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 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.
inc = 0
NavigateRecords()
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.
Form Controls
22
Form Controls
Your form should now look something like the illustration below (use the formatting facilities to tidy the form up
if necessary).
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:
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.
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:
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.
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.
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
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
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.
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
30
NewCustomer form
Readonly = True
NumericUpdown DecimalPlaces = 0
Maximum = 5000
Name = numOrderAmount
Name = dtpOrderDate
FillOrCancel form
31
Controls for the FillOrCancel form Properties
Name = dtpFillDate
Readonly = True
RowHeadersVisible = False
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
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
/// <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;
try
{
connection.Open();
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 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();
/// <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
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
/// <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;
}
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";
try
{
connection.Open();
// Display the data from the data table in the data grid
view.
this.dgvCustomerOrders.DataSource = dataTable;
/// <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();
/// <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 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();
/// <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
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:
data_navigator.RecordSet.MoveFirst
data_navigator.RecordSet.MoveLast
data_navigator.RecordSet.MoveNext
data_navigator.RecordSet.Previous
You can also add, save and delete records using the following commands:
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.
data_navigator.Recordset.MoveFirst
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.
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) .
50
2. Set the control names and other properties as shown in the table below.
51
ImageStudio Form Controls
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:
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:
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.
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:
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:
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) .
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:
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:
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:
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.
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:
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:
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:
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:
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:
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:
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) .
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:
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.
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.
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.
'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
'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
'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
'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
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
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:
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:
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:
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
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:
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
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.
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.
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:
108
strRec(5).PadRight(8) & vbNewLine)
Loop
srdFile.Close()
End
Your code editor window should now look like the screenshot below.
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.
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:
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:
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.
Amsterdam 41 53 69 57
Athens 54 67 90 74
115
Maximum Temperatures (°F) in European Cities (2010)
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
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.
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.
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
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:
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
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.
122
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 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.
Control Properties
126
OnlineForm Form Controls
Control Properties
127
OnlineForm Form Controls
Control Properties
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
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 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:
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:
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:
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 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.
141
PizzaDelivery Form Controls
142
PizzaDelivery Form Controls
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
144
PizzaDelivery Form Controls
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.
146
Dim ds As New DataSet
Dim da As OleDb.OleDbDataAdapter
Dim sql As String
Dim cmd As OleDb.OleDbCommand
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
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
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
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
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
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.
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.
166
Pong Form Controls
167
Pong Form Controls
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).
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
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
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
End Class
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:
The following statement declares and populates the array at the same time (note that this time there is no
number within the parentheses) :
You can also declare an array without specifying the number of elements it will hold:
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
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:
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.
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:
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:
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:
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.
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 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.
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
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 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:
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:
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:
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:
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 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:
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:
207
Dim structRecords() As structStudentRecord
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
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).
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.
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.
Control Name
Form frmClockCalendar
215
Clock-Calendar Form Controls
Control Name
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).
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:
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
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.
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:
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:
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
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 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.
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.
'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
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
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.
11. Set the control names and other properties as shown in the table below.
231
frmMenu Form Controls
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.
232
15. Set the control names and other properties as shown in the table below.
233
frmTestPage Form Controls
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.
19. Set the control names and other properties as shown in the table below.
234
frmTestResults Form Controls
235
frmTestResults Form Controls
237
frmTestResults Form Controls
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.
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
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"
241
The MathTest admin form interface
27. Set the control names and other properties as shown in the table below.
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:
242
Exit Sub
ElseIf txtPassword.Text = "" Then
MsgBox("You have not entered a password.")
txtPassword.Focus()
Exit Sub
End If
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:
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
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
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:
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
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:
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
End Sub
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
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.
258
The questions at this level are relatively easy
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:
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.
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.
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).
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
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:
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:
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:
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.
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:
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:
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.
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
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:
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:
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:
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:
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:
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.
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:
drawFlag = False
xUp = e.X
yUp = e.Y
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:
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