Liberty Basic is develeopped by Carl Gundel
Original Newsletter compiled by Alyce Watson and Brosco
Translation to HTML: Raymond Roumeas

  The Liberty Basic Newsletter - Issue #23 - Dec 98

"The only stupid question is the one you don't ask"!

1) Writing a program in LB - Part 2

In Issue #22 we discussed the steps to planning an LB program by writing the specs - then writing the program one function at a time. In this issue we will take some program specs and build a program.

Remember in Newsletter #9-13 we created a Video Cassette Library program. Let's go through the process of creating this. I have ZIPped the program at each stage of the development so that you can see how I have done this. I recommend that you view and run the attached programs while you are working your way through the tutorial.

First of all - the specs:

Video Cassette Library - Program Specifications.

The program will be used to store information about a cassette collection. Each cassette will have a unique ID and only contain one movie title. The program should store the Movie name, main actor's name, duration of the movie and a movie category (drama, action, comedy, etc.). Information about a movie, once entered, must be able to be updated in case errors were made when it was entered.

A) *** File(s) Needed ***

First thing we know is that we must store information. In LB we can do that in two ways: a text file or a Random file. Text files are best for unformatted data - Random files are best for repeating groups of structured information. A Random file is clearly the best choice here.

With a random file we also need to specify the Fields that will be stored in a record - from our specs we can indentify these:

Field #vcl, _
6 as CassID$, _
32 as Title$, _
32 as Star$, _
3 as time, _ ' Duration in minutes
16 as category$, _
39 as spare$

What's that '39 as spare$'???? I find that often I write a program like this and then later realise that I would like to add another Field to the record. If I do this, I lose all the data that I have entered so far, and must start over. Say, for example, that I wanted to add a censorship classification, using the above structure:

Field #vcl, _
6 as CassID$, _
32 as Title$, _
32 as Star$, _
3 as time, _ ' Duration in minutes
16 as category$, _
6 as censor$, _ ' **** Added
33 as spare$ ' **** Reduced by size of added field

 My existing Random file will still be accessable and all I have to do is use the update facility of the program to enter the Censorship classification for each of the movies that I have recorded. And why did I pick a size of 39???? If you add up all the field sizes, you will notice that it adds up to a size of 128. This is just adding a bit of efficiency. DOS buffers are 128 bytes long, so record sizes like 32, 64, 128, 256, etc., are the best sizes to work with if they suit your program.

B) *** Functions needed ***

  1. Code to OPEN the Cassette file (and create it if it doesn't exist).
  2. Code to allow the user to ENTER cassette information
  3. Code to RETRIEVE and VIEW the information. mmmmh, what functions should we give here? - how about: PREVIOUS and NEXT functions to browse through the records on the file.
  4. Code to allow the user to UPDATE any information displayed.
  5. Code to CLOSE the Cassette file.

C) *** The User Interface ***

OK - let's start building the program - we will need a window of some sort to enter, update and display the data. The best type of window for this type of application is a Dialog.

 I have a directory on my system where I keep skeleton code for various window types and subroutines. My skeleton code for a dialog window looks like this:

' Dialog.bas
' Skeleton code for a Dialog Window
nomainwin
WindowWidth = 640
WindowHeight = 480
 
'open USER.DLL to make API calls
open "user.dll" for dll as #user
 
gosub [position.dialog] ' Code to establish window positioning
 
Button #w, "Close", [close.w], UL, 270, 420, 60, 25
open "Window Dialog" for dialog as #w
gosub [restore.cursor] ' Restores cursor to original position
print #w, "trapclose [close.w]"
 
[loop]
input var$
goto [loop]
 
[close.w]
close #user
close #w
end

' Follow code is used to position the Dialog box in the ' centre of the screen ' Courtesy Carl G (from the LB Helpfile) [position.dialog] 'define structures struct winRect, _ orgX as uShort, _ orgY as uShort, _ extentX as uShort, _ extentY as uShort   struct point, _ x as short, _ y as short   'get the current cursor position calldll #user, "GetCursorPos", _ point as struct, _ result as void x = point.x.struct y = point.y.struct   'let's do some math to figure out where our window belongs topLeftX = int((DisplayWidth - WindowWidth) / 2) topLeftY = int((DisplayHeight - WindowHeight) / 2)   'put the cursor where the origin of the window should be calldll #user, "SetCursorPos", _ topLeftX as ushort, _ topLeftY as ushort, _ result as void return   [restore.cursor]   'put the cursor back where it was calldll #user, "SetCursorPos", _ x as ushort, _ y as ushort, _ result as void return


The nice thing about having code like this is that it saves me from writing all this out every time I create a new program. I just load this into the LB editor and can start coding the 'real' stuff. I don't need to test it - I know it works - but I would recommend that you test it so that you are satisfied with what it does. If you are still learning LB and are a little intimidated by API calls, don't worry about them - just trust me that they work - run the program (Dialog.bas) and see it for yourself - this skeleton code puts the dialog window into the center of the screen.

You can get a little more explanation of the [position.dialog] code from the LB helpfile.

At this point I would use FreeForm to map out the data fields - or, if there are just a few - simple code them in by hand. For this exercise we will use a simple layout - (this newsletter is about programming NOT about making fancy window designs).

Here's the program with the fields added to the dialog:

' Dialog1.bas
' Skeleton code for a Dialog Window
nomainwin
WindowWidth = 640
WindowHeight = 480
 
'open USER.DLL to make API calls
open "user.dll" for dll as #user
 
gosub [position.dialog] ' Code to establish window positioning
 
StaticText #w.1, "Cassette ID:", 10, 10, 100, 20
StaticText #w.2, "Movie Title:", 10, 40, 100, 20
StaticText #w.3, "Main Star:", 10, 70, 100, 20
StaticText #w.4, "Duration:", 10, 100, 100, 20
StaticText #w.5, "Category:", 10, 130, 100, 20
 
TextBox #w.id, 120, 6, 50, 25
TextBox #w.title, 120, 36, 250, 25
TextBox #w.star, 120, 76, 250, 25
TextBox #w.duration, 120, 106, 32, 25
TextBox #w.category, 120, 136, 120, 25
 
Button #w, "Close", [close.w], UL, 270, 420, 60, 25
 
open "Window Dialog" for dialog as #w

Now run the program (dialog1.bas) and see how the window looks.(I'll just go a get a cup of coffee while you do that - when I come back, we'll discuss what to do next).............

OK - a couple of little things to tidy up:

  1. I forgot to change the title of the Window.
  2. We clearly don't need a window size of 640x480.
  3. If we change the window size - we will have to change the position of the 'Close' button. While we are doing that, we will add some buttons for 'Previous', 'Next', 'Add' and 'Update'.
  4. The cursor is 'flashing' in the Movie Title Textbox, so we need to add a 'setfocus' so that it starts at the CassID$ field.
  5. I didn't get the spacing right for the TextBox fields.

Let's see how that looks now: Dialog2.bas

' VCL - Video Cassette Library - LB Newsletter #23
nomainwin
WindowWidth = 410
WindowHeight = 240
 
'open USER.DLL to make API calls
open "user.dll" for dll as #user
 
gosub [position.dialog] ' Code to establish window positioning
 
StaticText #w.1, "Cassette ID:", 10, 10, 100, 20
StaticText #w.2, "Movie Title:", 10, 40, 100, 20
StaticText #w.3, "Main Star:", 10, 70, 100, 20
StaticText #w.4, "Duration:", 10, 100, 100, 20
StaticText #w.5, "Category:", 10, 130, 100, 20
 
TextBox #w.id, 120, 6, 50, 25
TextBox #w.title, 120, 36, 250, 25
TextBox #w.star, 120, 66, 250, 25
TextBox #w.duration, 120, 96, 32, 25
TextBox #w.category, 120, 126, 120, 25
 
Button #w.add, "Add", [add.rec], UL, 10, 180, 60, 25
Button #w.upd, "Update", [update.rec], UL, 90, 180, 60, 25
Button #w.prv, "Prev", [prev.rec], UL, 170, 180, 60, 25
Button #w.next, "Next", [next.rec], UL, 250, 180, 60, 25
Button #w.exit, "Close", [close.w], UL, 330, 180, 60, 25
 
open "VCL - Video Cassette Library" for dialog as #w
gosub [restore.cursor] ' Restores cursor to original position
print #w, "trapclose [close.w]"
print #w.id, "!setfocus"
 
[loop]
input var$
goto [loop]
 
[add.rec]
goto [loop]
 
[update.rec]
goto [loop]
 
[prev.rec]
goto [loop]
 
[next.rec]
goto [loop]
  
[close.w]
close #user
close #w
end
.......

Once again - we run the program (dialog2.bas) to see how it looks, also, click on each of the Buttons to ensure that we have coded the [branch.labels] correctly. Not only are we improving the appearance of the program as we go along - we are also removing bugs that could creep in because of typos.

Ok - time to start adding some functions - where to start???

Clearly this program will be hard to test unless we have some data stored in our Random file. So we need to define the file and then code the Add Record function. First of - we add the file:

 .........

open "VCL - Video Cassette Library" for dialog as #w
gosub [restore.cursor] ' Restores cursor to original position
print #w, "trapclose [close.w]"
print #w.id, "!setfocus"
 
open "vclib.dat" for random as #vcl len=128
Field #vcl, _
6 as CassID$, _
32 as Title$, _
32 as Star$, _
3 as time, _ ' Duration in minutes
16 as category$, _
39 as spare$
 
[loop]
input var$

And, of course, don't forget to close the file.

[close.w]
close #user
close #w
close #vcl
end

Run the program again (dialog3.bas). This ensures that the added code doesn't have any typos, the LEN=128 in the Open statement matches our Field statement definition, etc.

OK - no errors - so lets code the ADD function:

[add.rec]
print #w.id, "!contents?"
input #w.id, CassID$
if CassID$ = "" then
Notice "You must enter a valid Cassette ID!"
goto [loop]
end if
 
print #w.title, "!contents?"
input #w.title, Title$
if Title$ = "" then
Notice "The Movie's title must be entered!"
goto [loop]
end if
 
print #w.star, "!contents?"
input #w.star, Star$
 
print #w.duration, "!contents?"
input #w.duration, time
 
print #w.category, "!contents?"
input #w.category, category$
 
recNum = lof(#vcl) / 128 + 1 ' calc location of next record
put #vcl, recNum
 
goto [loop]

All we need to do is to Input the data from each of the dialog Textboxes and then Add the record. I also put in a little data validity checking for some of the fields.

Run this program (dialog4.bas).

First - click 'Add' without entering any info and make sure that the check for a valid ID works.

Next - enter CassetteID (any value will do) and click 'Add'. Did the check for a movie title work???

Enter data into the other fields, and click on 'Add'. (I tend to use data like "aaa" for the first field, "bbb" for the next, "ccc" for the next, etc. You will see why in a moment.

mmmmmmh! - It seemed to work - but there's nothing to tell us (or our user) that the add was a success - we'll fix this when we make the next changes to the program.

Now start up Notepad.exe and view the file. Random files are stored as text - so Notepad is fine for viewing the contents. What does NotePad show us:

aaa bbbb cccc 0 eeee

mmmmmmh again! What's that zero doing there - ah yes - Duration is a numeric field - and we entered "ddd", we better put in a check that its a valid numeric field!

Ok - lets fix up these problems before we proceed:

First we add out Status field to the window:

 StaticText #w.status, "", 10, 210, 250, 20

this line is inserted with our other StaticText controls. And we display a status just after we add the record:

put #vcl, recNum
print #w.status, "Cassette ID: " + CassID$ + " has been added."

And to ensure that the Movie duration is valid:

print #w.duration, "!contents?"
input #w.duration, timeCheck$
valid = 1
for i = 1 to len(timeCheck$)
if mid$(timeCheck$, i,1) < "0" or mid$(timeCheck$, i, 1) > "9" then
valid = 0
end if
next i
if valid = 0 then
notice "Duration is length of movie in Minutes!"
goto [loop]
end if
time = val(timeCheck$)

I could have simply used code like:

input #w.duration, timeCheck$
if val(timeCheck$) = 0 then ......

but I wanted the user to be able to enter a value of zero - at the time that the data is entered, the duration may not be known, so it would be valid to enter zero and update this information at a later date.

Run the updated program (dialog5.bas) and add some data. Keep using valid and invalid data just to make sure that the checks always work. Keep track of the records that you did add. Then 'Close' the program and view the file with Notepad. Check that the valid records have been added - and that the invalid records were not added.

Well - so far so good. It all seems to be working. What would be the next function we should add? We could UPDATE or we could add the PREV/NEXT functions.

Personally, I would pick the PREV/NEXT functions - because then I don't need to keep switching to Notepad to see if the program is working correctly. I will be able to use these functions to view the contents of the file - thus - these functions will assist us in the later stages of development.

Here's the code:

[display.rec]
print #w.id, trim$(CassID$)
print #w.title, trim$(Title$)
print #w.star, Star$
print #w.duration, time
print #w.category, trim$(category$)
return
 
[prev.rec]
if recNum > 1 then
recNum = recNum - 1
get #vcl, recNum
print #w.status, ""
else
print #w.status, "You are at the start of the file."
end if
gosub [display.rec]
goto [loop]
 
[next.rec]
if recNum < lof(#vcl) / 128 then
recNum = recNum + 1
get #vcl, recNum
print #w.status, ""
else
print #w.status, "You are at the end of the file."
end if
gosub [display.rec]
goto [loop]

Run the program (dialog6.bas) and test the new functions, add some more data and test them again.

Yep - seems to be working fine - now to code the "Update" function.

Oh - Update will have to validate the fields in exactly same way as we did in the Add function. We had better make these a subroutine!

So, first we will change the data validation into a subroutine for the Add function - and test it BEFORE we bother with the Update function. Here's the changes:

[validate]
valid = 1
 
print #w.id, "!contents?"
input #w.id, CassID$
if CassID$ = "" then
Notice "You must enter a valid Cassette ID!"
valid = 0
return
end if
 
print #w.title, "!contents?"
input #w.title, Title$
if Title$ = "" then
Notice "The Movie's title must be entered!"
valid = 0
return
end if
 
print #w.star, "!contents?"
input #w.star, Star$
 
print #w.duration, "!contents?"
input #w.duration, timeCheck$
for i = 1 to len(timeCheck$)
if mid$(timeCheck$, i,1) < "0" or mid$(timeCheck$, i, 1) > "9" then
valid = 0
end if
next i
if valid = 0 then
notice "Duration is length of movie in Minutes!"
return
end if
time = val(timeCheck$)
 
print #w.category, "!contents?"
input #w.category, category$
return
 
[add.rec]
gosub [validate]
if valid = 0 then [loop]
 
recNum = lof(#vcl) / 128 + 1 ' calc location of next record
put #vcl, recNum
print #w.status, "Cassette ID: " + CassID$ + " has been added."
 
goto [loop]

Run the updated program (dialog7.bas) to ensure that our Add function still works correctly.

Yep - that seems OK - now for Update. Here's the code:

[update.rec]
gosub [validate]
if valid = 0 then [loop]
 
put #vcl, recNum
print #w.status, "Cassette ID: " + CassID$ + " has been updated."
goto [loop]

Test the program (dialog8.bas) and make sure all functions are working - no matter what order you use them.

And that's all there is to it. We have built a reasonable size program - but by breaking down into small sections, we were able to create a bug free program - because every individual section was tested. And when we made a change to the way a function worked (like puting the data validation into a subroutine) we re-tested the program BEFORE moving onto the next Function.

As I said at the start of this - I wasn't trying to make a fancy user interface - I just wanted to demonstrate the development cycle. But for people who, like me, are not very creative with user interfaces - there is one way to dress it up a bit. We can simple introduce ctl3d.dll to smarten it up for us. We open this DLL at the start of the program and register as a user - (If you want to use this in your own programs - just cut and paste this code - don't worry about what it does):

' VCL - Video Cassette Library - LB Newsletter #23
 
open "ctl3dv2.dll" for dll as #ctl3d
calldll #ctl3d,"Ctl3dRegister",0 as short,result as short
calldll #ctl3d,"Ctl3dAutoSubclass",0 as short,result as short
 
nomainwin
 
And we Unregister and Close the DLL at the end of the program:
 
[close.w]
close #user
close #w
close #vcl
calldll #ctl3d,"Ctl3dUnregister",0 as short,result as short
close #ctl3d
end

Run the program dialog9.bas and see our finished program. And we're DONE!!!!!

Now, do you really believe that I go through all these steps every time I write a program????? You better believe it - I DO! It may look like it only took me 9 goes to get it right - but that's not the case either - every version of the program (dialog1-9) took me several cycles before I was happy with them. It took me 3 or 4 attempts just to get the WindowWidth and Height just right!

Alyce and Tom Watson develop their programs the same way - and yep - its many cycles before you see the end product - BUT - by following this process you have a far higher chance of producing a bug-free program.

In a future issue I will try to find a good example for demonstrating the debugging features of LB. I use these extensively when I am developing a program.


Newsletter compiled and edited by: Brosco and Alyce.

Comments, requests or corrections: Hit 'REPLY' now!