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


' ***********   End of skeleton code

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 doen'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 written by: Brosco.
 Comments, requests or corrections mailto:brosco@orac.net.au

 Translated from Australian to English by an American:

 Alyce Watson -  Chief Editor.  Thanks Alyce.

--------------------------------------------------------------
 