Liberty Basic is develeopped by Carl Gundel Original Newsletter compiled by Alyce Watson and Brosco Translation to HTML: Raymond Roumeas
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 ***
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
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:
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.
Comments, requests or corrections: Hit 'REPLY' now!