INTRODUCTION:
Welcome to this second installment on file manipulation in QuickBasic and FreeBasic. In the first installment, we covered sequential file access (Quick note, Sequential file examples should work fine in FreeBasic too). Even today I can say that sequential files have their purpose. However, when it comes to handling a bigger load of data and information, sequential files quickly become annoying as you have to load the whole file and write the whole file. It becomes quite a chore just to get the row of information you're really looking for.
This is where Random Access Files come to play. With Random Access Files you read a record and write a record, not a whole file, at a time. This is what this installment will be about. Random Access Files, how you can use them, why they are better than sequential files and how you can put them to good use in your development projects. You'll find I'll be using a rather similar structure in this tutorial as I did in the Sequential File tutor because I believe it's not a bad way to go about learning. Let's get right to it shall we?
WHAT ARE RANDOM ACCESS FILES:
To best describe what random access files are you need to understand one basic principle. All random access files are fixed length record files. This means that for one given file, all records (or rows) of information contain the same datatypes, in the same order and of the same length. The reason these are the way they are is quite simple. It allows to calculate where we are in the file and where we can go. In comparison, if you use a fixed length sequential file (like my example in the sequential file tutor) you'd know that since each row has the same format (columns of data start at the same place) then if you count the number of rows you need to read to get to the 5th record, you'd have read in X number of characters.
In sequential files, you're the one that calculates these things. In Random access files, it's already there for you, to some point. Another advantage of random access files is that when you open the file, you can open it to be both readable and writable at the same time. As you can see, this can become quite an advantage especially when dealing with larger amounts of information.
When programming for Random Access Files, I like to use User Defined Types for this purpose. Ultimately you could use independent sets of variables, calculate the length of everything and use that, but I find User Defined Types go so well hand in hand with Random Access files.
WORKING WITH RANDOM ACCESS FILES:
To work with Random Access Files, the commands are different than those used for sequential files. The OPEN statement changes as well but not by much.
The OPEN Statement works as follows:
OPEN "FILENAME.EXT" FOR RANDOM AS #FileNumber LEN = LengthOfRecord
- "FILENAME.EXT" Is the name of the file to be opened. And can have any standard file name or path and file name.
- #FileNumber is the standard File Handle to Assign. Here too the FREEFILE statement works great.
- LEN = LengthOfRecord Self explanatory. The length a record should be.
To read a record from the file you need the GET command. It's syntax is as follows:
GET #FileNumber, RecordNumber, Variable
- #FileNumber Is the filehandle number used in the OPEN statement.
- RecordNumber is optional but allows to specify which record to read next. If it's ommited, the next available record is read in.
- Variable is the variable in which the values of a read record get transfer to.
To write a record to a random Access File, you'll need to use the
PUT command. The syntax for the PUT command is as follows:
PUT #FileNumber, RecordNumber, Variable
- #FileNumber Is the filehandle number used in the OPEN statement.
- RecordNumber is optional but allows to specify which record number to write the information to in the file. If ommited, the next available record number is use. if it's at the end of the file, a new record is added.
- Variable is the variable in which the values of a read record get transfer to.
To locate a given record, hence to move the record pointer around in the Random Access file, you'll need the SEEK statement. It's syntax is:
SEEK #FileNumber, Position
- #FileNumber is the File Handle used in the open statement.
- Position is the record number you wish to move to.
EXAMPLE PROGRAM
With all this theory at hand, I think now is a great time to throw in an example program to help sink things in. This example will create a random access file, insert 2 records, read them back in and display the records to the screen: This example should compile and run under FreeBasic as well.
' -------------------------------------------------------------
' ContactInformation will hold data gotten from the data file
' It will also allow us to write data to the file
' -------------------------------------------------------------
TYPE ContactInformation
Status AS STRING * 1
ContactID AS LONG
ContactName AS STRING * 40
PhoneNumber AS STRING * 14
Email AS STRING * 70 END TYPE
' ----------------------------------- ' Variables we'll need to work with ' ----------------------------------- DIM FileHandle AS INTEGER DIM RecordNumber AS LONG DIM Contact AS ContactInformation
' ----------------------------------------------------------- ' Get next available Handle and Open the Random Access File ' If the file doesn't exist, one is created ' ----------------------------------------------------------- FileHandle = FREEFILE OPEN "TEST.DAT" FOR RANDOM AS #FileHandle LEN = LEN(Contact)
' --------------------------------------------------- ' Assuming there are no records, we'll insert a few ' --------------------------------------------------- ' --------------------------------------------- ' Fill Information into the Contact Structure ' --------------------------------------------- Contact.Status = " " Contact.ContactID = 1 Contact.ContactName = "Stephane Richard" Contact.PhoneNumber = "(123) 323-3344" Contact.Email = "srichard@adaworld.com" ' -------------------------------------- ' Insert the record into the data file ' -------------------------------------- PUT #FileHandle, , Contact ' --------------------------------------------- ' Fill Information into the Contact Structure ' --------------------------------------------- Contact.Status = " " Contact.ContactID = 2 Contact.ContactName = "Some Other Name" Contact.PhoneNumber = "(666) 555-4444" Contact.Email = "notsure@whatever.com" ' -------------------------------------- ' Insert the record into the data file ' -------------------------------------- PUT #FileHandle, , Contact ' -------------------------------------------- ' And we'll now read and display the records ' -------------------------------------------- RecordNumber = 1 GET #FileHandle, RecordNumber, Contact PRINT Contact.ContactID; " "; Contact.ContactName; " "; Contact.PhoneNumber ' ----------------------------------------------------------- ' If no record number is specified, the next one is read in ' ----------------------------------------------------------- GET #FileHandle, , Contact PRINT Contact.ContactID; " "; Contact.ContactName; " "; Contact.PhoneNumber ' ------------------------------------- ' Of course, we close the file handle ' ------------------------------------- CLOSE #FileHandle
NOTES:
I've commented this example program and I think it's a pretty clear example However A few notes are in order.
- I use the FREEFILE statement here to, just like in sequential files it plays the same role, gets the next available file handle, and as I said before, it really is good practice, for Random access files or another other file you'll open with the OPEN statement.
- Like I mentionned above, I use a User Defined Type called ContactInformation for this example, as you can see, because a Random Access File's record length must be fixed, a User Defined Type really help keep the code clear. It's the main reason why I Used them. Of course, of you only have one field, then a simple variable of the data type you need will suffice.
- For this first example, I created only 2 records (to keep the code short and clear) and to quickly show the typical usage of Random access files. As you've surely noticed, I didn't use the SEEK statement yet because I only had 2 records.
- Right now, the Status Field in our ContactInformation User Defined Type has a space as it's value. Since there's no direct method for deleting a record per se, this Status Field will contain "D" when marked for deletion. This is a widely used method for simulating the deletion of records. DBase and other popular databases use a method similar to this one for their own deletion of records.
- When physically deleting records in a random access file, a full rewrite of the file is in order. Basically, you copy the file, open it, browse through it saving record by record to a new file skipping the records marked for deletion. Then renaming the file to the standard filename it should have deleting the original file first, of course. Needless to say this is usually something you'll want to do only when necessary (as in when you know you have a lot of records that are marked for deletion and you know speed would be improved if such an operation was performed on the data file.
A MORE ADVANCED AND COMPLETE EXAMPLE:
The next example coming up will go a few steps further. It will Allow to modify a given record, and to add new records as well. For the sake of a more complete example, I will split this example up in 2 seperate programs. The record manager itself which will allow to add, modify and delete records. and the second part will be a sample program that will create some data so we have a couple records more to work with. I will also include a routine that will pack the data file (hence do the physical deletion of records marked for delete) so you'll also have an example of that as well.
You need to keep in mind a few things as you dwell deeper into File Manipulation techniques. First off there's a certain order of things that need to be specified in order to work properly with Random Access Files. The first of these order of things is of course:
- You open the file
- You read and/or write data to the file
- You close the file
The next order is about modifying and/or deleting records.
- You open the file
- You position yourself on the record that needs changing
- You load the record into your variable structure
- You edit the different variables.
- You reposition yourself on the record you're editing
- You save the information to the file (or mark the record for deletion)
- You close the file.
This next one is for adding a record (always at the end of the file)
- You open the file
- You create an empty data structure to be edited.
- You edit the data that is to be saved
- You determine the end of the file
- You write your record
- You close the file
NOTE: Many issues have risen in the past about how often to open a file versus how long to open the file. This is especially true for a file that may be shared by more than one use. However to me, in regular circumstances I just open the file at the beginning and close it at the end. At least for the sake of this tutorial. But if you keep variables handy to know where you should be in the file, you could just open it when you need to get a record from the file.
This second and last example of this tutorial is a much more complete application compared to the first example, it's much longer as well. It's a contact management program designed to show you all aspects of Random Access File management. I put more effort in this one to design something that's functional. It's main flaw is no data validation on data entry so just cooperate with what it's asking. It also has no error management as it's not the goal of this example. It covers everything you can do to a Random Access File in what I like to call a good application design where everything can be quickly changed to accomodate different types of structures. Data statements are used to position fields on the screen, this way fields can be added if needed. I put a special effort in documenting this source code too so that everything is as clear as it can.
' ========================================================================
' NAME: Contacts Pro
' FILE NAME: RAFEx2.bas
' ---------------------------------------------------------------------------
' AUTHOR: Stephane Richard
' LICENSE: Public domain
' COPYRIGHT: Copyright (c) 2004 - Stephane Richard
' DISCLAIMER: You are free to use, distribute, copy in any way you wish.
' ---------------------------------------------------------------------------
' DESCRIPTION: This example program is the 2nd in the Random Access File
' tutorial written by the same author. It illustrates all
' functions described in the tutorial in a small but complete
' random access file management application.
' ========================================================================
' ------------------------------- ' Sub and Function Declarations ' ------------------------------- DECLARE SUB DrawMainScreen () DECLARE SUB MainMenu () DECLARE SUB AddAContact () DECLARE SUB UpdateAContact () DECLARE SUB DeleteAContact () DECLARE SUB ListAllContacts () DECLARE SUB PackContactFile () DECLARE SUB ClearViewPrint () DECLARE SUB DisplayEmptyFields () DECLARE SUB DisplayContactFields () DECLARE SUB MoveToNext () DECLARE SUB MoveToPrevious ()
' ---------------------- ' Constant Definitions ' ---------------------- CONST TRUE = -1 CONST FALSE = NOT TRUE
CONST ADDCONTACT = 0 CONST UPDATECONTACT = 1 CONST DELETECONTACT = 2 CONST LOOKUPCONTACT = 3 CONST LISTCONTACTS = 4 CONST PACKDATAFILE = 5 CONST EXITCONTACTS = 6
' ------------------------------------ ' FieldInformation User defined type ' ------------------------------------ TYPE Fieldinformation
FieldNumber AS LONG
FieldName AS STRING * 12
YPosition AS INTEGER
XPosition AS INTEGER
FieldLength AS INTEGER END TYPE
' ------------------------------------------------ ' User Defined Type used for Record Manipulation ' ------------------------------------------------ TYPE ContactInformation
Status AS STRING * 1 ' Record Status Field "D" -Marked Deleted
ContactID AS LONG
CompanyName AS STRING * 50
ContactName AS STRING * 40
Address1 AS STRING * 60
Address2 AS STRING * 60
City AS STRING * 40
State AS STRING * 40
ZipCode AS STRING * 10
HomePhone AS STRING * 14
WorkPhone AS STRING * 14
CellPhone AS STRING * 14
PagerNumber AS STRING * 14
Email AS STRING * 60
Website AS STRING * 60 END TYPE
' ----------------------------------- ' Variables we'll need to work with ' ----------------------------------- DIM SHARED FileHandle AS INTEGER DIM SHARED RecordCount AS LONG DIM SHARED RecordNumber AS LONG DIM SHARED ContactIDCounter AS LONG DIM SHARED Contact AS ContactInformation DIM SHARED FieldInfo(1 TO 14) AS Fieldinformation DIM Counter AS INTEGER
' ------------------------------------------------- ' Read field information from the data statements ' ------------------------------------------------- FOR Counter = 1 TO 14
READ FieldInfo(Counter).FieldNumber
READ FieldInfo(Counter).FieldName
READ FieldInfo(Counter).YPosition
READ FieldInfo(Counter).XPosition
READ FieldInfo(Counter).FieldLength NEXT Counter
' ------------------------------------------------------ ' We get the next FileHandle and we open the data file ' ------------------------------------------------------ FileHandle = FREEFILE OPEN "CONTACTS.DAT" FOR RANDOM AS #FileHandle LEN = LEN(Contact) RecordNumber = 1 RecordCount = LOF(FileHandle) / LEN(Contact)
' ---------------------- ' Draw the main screen ' ---------------------- WIDTH 80, 25 COLOR 14, 1 CALL DrawMainScreen IF RecordCount > 0 THEN
SEEK #FileHandle, RecordNumber
CALL DisplayContactFields ELSE
CALL DisplayEmptyFields END IF
' ------------------------------ ' Present the menu to the user ' ------------------------------ CALL MainMenu COLOR 7, 0 CLS CLOSE #FileHandle
' ----------------------------------------------------------- ' DATA STATEMENTS FOR FIELD POSITION AND LENGTH INFORMATION ' STRUCTURE: FieldNumber, "Field Name", Row, Col, Length ' ----------------------------------------------------------- DATA 1, "ContactID", 6, 71, 8 DATA 2, "CompanyName", 8, 29, 50 DATA 3, "ContactName", 9, 39, 40 DATA 4, "Address1", 11, 19, 60 DATA 5, "Address2", 12, 19, 60 DATA 6, "City", 13, 39, 40 DATA 7, "State", 14, 39, 40 DATA 8, "ZipCode", 15, 69, 10 DATA 9, "HomePhone", 17, 65, 14 DATA 10, "WorkPhone", 18, 65, 14 DATA 11, "CellPhone", 19, 65, 14 DATA 12, "PagerNumber", 20, 65, 14 DATA 13, "Email", 22, 19, 60 DATA 14, "Website", 23, 19, 60
' ========================================== ' NAME: AddAContact() ' PARAMETERS: None ' CALLED FROM: Main Section of the program ' ASSUMES: Nothing ' --------------------------------------------- ' DESCRIPTION: This Subroutine requests the ' information from the user ' about the contact and adds it ' to the end of the file. ' ========================================== SUB AddAContact
DIM WorkContact AS ContactInformation
DIM WorkString AS STRING
' -------------------------------------
' First Step - Empty Fields on screen
' -------------------------------------
CALL DisplayEmptyFields
' --------------------------------------
' Next we start the data entry process
' --------------------------------------
LOCATE FieldInfo(1).YPosition, FieldInfo(1).XPosition: COLOR 14, 7
INPUT Contact.ContactID
LOCATE FieldInfo(1).YPosition, FieldInfo(1).XPosition: COLOR 14, 3
PRINT USING "######"; Contact.ContactID
LOCATE FieldInfo(2).YPosition, FieldInfo(2).XPosition: COLOR 14, 7
INPUT Contact.CompanyName
LOCATE FieldInfo(2).YPosition, FieldInfo(2).XPosition: COLOR 14, 3
PRINT Contact.CompanyName
LOCATE FieldInfo(3).YPosition, FieldInfo(3).XPosition: COLOR 14, 7
INPUT Contact.ContactName
LOCATE FieldInfo(3).YPosition, FieldInfo(3).XPosition: COLOR 14, 3
PRINT Contact.ContactName
LOCATE FieldInfo(4).YPosition, FieldInfo(4).XPosition: COLOR 14, 7
INPUT Contact.Address1
LOCATE FieldInfo(4).YPosition, FieldInfo(4).XPosition: COLOR 14, 3
PRINT Contact.Address1
LOCATE FieldInfo(5).YPosition, FieldInfo(5).XPosition: COLOR 14, 7
INPUT Contact.Address2
LOCATE FieldInfo(5).YPosition, FieldInfo(5).XPosition: COLOR 14, 3
PRINT Contact.Address2
LOCATE FieldInfo(6).YPosition, FieldInfo(6).XPosition: COLOR 14, 7
INPUT Contact.City
LOCATE FieldInfo(6).YPosition, FieldInfo(6).XPosition: COLOR 14, 3
PRINT Contact.City
LOCATE FieldInfo(7).YPosition, FieldInfo(7).XPosition: COLOR 14, 7
INPUT Contact.State
LOCATE FieldInfo(7).YPosition, FieldInfo(7).XPosition: COLOR 14, 3
PRINT Contact.State
LOCATE FieldInfo(8).YPosition, FieldInfo(8).XPosition: COLOR 14, 7
INPUT Contact.ZipCode
LOCATE FieldInfo(8).YPosition, FieldInfo(8).XPosition: COLOR 14, 3
PRINT Contact.ZipCode
LOCATE FieldInfo(9).YPosition, FieldInfo(9).XPosition: COLOR 14, 7
INPUT Contact.HomePhone
LOCATE FieldInfo(9).YPosition, FieldInfo(9).XPosition: COLOR 14, 3
PRINT Contact.HomePhone
LOCATE FieldInfo(10).YPosition, FieldInfo(10).XPosition: COLOR 14, 7
INPUT Contact.WorkPhone
LOCATE FieldInfo(10).YPosition, FieldInfo(10).XPosition: COLOR 14, 3
PRINT Contact.WorkPhone
LOCATE FieldInfo(11).YPosition, FieldInfo(11).XPosition: COLOR 14, 7
INPUT Contact.CellPhone
LOCATE FieldInfo(11).YPosition, FieldInfo(11).XPosition: COLOR 14, 3
PRINT Contact.CellPhone
LOCATE FieldInfo(12).YPosition, FieldInfo(12).XPosition: COLOR 14, 7
INPUT Contact.PagerNumber
LOCATE FieldInfo(12).YPosition, FieldInfo(12).XPosition: COLOR 14, 3
PRINT Contact.PagerNumber
LOCATE FieldInfo(13).YPosition, FieldInfo(13).XPosition: COLOR 14, 7
INPUT Contact.Email
LOCATE FieldInfo(13).YPosition, FieldInfo(13).XPosition: COLOR 14, 3
PRINT Contact.Email
LOCATE FieldInfo(14).YPosition, FieldInfo(14).XPosition: COLOR 14, 7
INPUT Contact.Website
LOCATE FieldInfo(14).YPosition, FieldInfo(14).XPosition: COLOR 14, 3
PRINT Contact.Website
' -----------------------------------
' We add the record in the datafile
' ----------------------------------- ' SEEK #FileHandle, RecordCount
PUT #FileHandle, RecordCount + 1, Contact
' -------------------------------------
' Update RecordCount and RecordNumber
' -------------------------------------
RecordNumber = RecordCount + 1
RecordCount = LOF(FileHandle) / LEN(Contact)
SEEK #FileHandle, RecordCount
WorkString = "RECORD: " + STR$(RecordNumber) + " OF " + STR$(RecordCount)
LOCATE 4, 79 - LEN(WorkString): COLOR 7, 1: PRINT WorkString
END SUB
' ============================================== ' NAME: DeleteAContact() ' PARAMETERS: None ' RETURNS: Integer for the selected menu ' CALLED FROM: Main Section of the program ' ASSUMES: Nothing ' ------------------------------------------------- ' DESCRIPTION: This Ask for confirmation to ' delete and awaits the user's input ' Then Marks the current contact for ' deletion. ' ============================================== SUB DeleteAContact
DIM WorkString AS STRING
LOCATE 4, 2: COLOR 0, 4: PRINT SPACE$(78);
LOCATE 4, 2: COLOR 14, 4: INPUT "Are you sure you want to delete this contact? (Y/N) :", Confirm$
IF UCASE$(Confirm$) = "Y" THEN
Contact.Status = "D"
PUT #FileHandle, RecordNumber, Contact
END IF
LOCATE 4, 1: COLOR 7, 1: PRINT CHR$(186); SPACE$(78); CHR$(186)
LOCATE 4, 3: COLOR 7, 1: PRINT "CURRENT CONTACT INFORMATION"
WorkString = "RECORD: " + STR$(RecordNumber) + " OF " + STR$(RecordCount)
LOCATE 4, 79 - LEN(WorkString): PRINT WorkString
END SUB
' ============================================ ' NAME: DisplayContactFields() ' PARAMETERS: None ' ASSUMES: Contact Variable Filled ' CALLED FROM: MainMenu Subroutine ' ----------------------------------------------- ' DESCRIPTION: Empties the currently displayed ' fields to display the current ' contact information. Called ' whenever the record pointer in ' the data file is moved. ' ============================================ SUB DisplayContactFields
DIM WorkString AS STRING
CALL DisplayEmptyFields
IF Contact.ContactID = 0 THEN
GET #FileHandle, RecordNumber, Contact
END IF
LOCATE 4, 40
COLOR 7, 1
IF Contact.Status = "D" THEN
PRINT "DELETED"
ELSE
PRINT " "
END IF
LOCATE 4, 50: COLOR 7, 1: PRINT " "
WorkString = "RECORD: " + STR$(RecordNumber) + " OF " + STR$(RecordCount)
LOCATE 4, 79 - LEN(WorkString): PRINT WorkString
LOCATE FieldInfo(1).YPosition, FieldInfo(1).XPosition
COLOR 15, 3: PRINT Contact.ContactID
LOCATE FieldInfo(2).YPosition, FieldInfo(2).XPosition
COLOR 15, 3: PRINT Contact.CompanyName
LOCATE FieldInfo(3).YPosition, FieldInfo(3).XPosition
COLOR 15, 3: PRINT Contact.ContactName
LOCATE FieldInfo(4).YPosition, FieldInfo(4).XPosition
COLOR 15, 3: PRINT Contact.Address1
LOCATE FieldInfo(5).YPosition, FieldInfo(5).XPosition
COLOR 15, 3: PRINT Contact.Address2
LOCATE FieldInfo(6).YPosition, FieldInfo(6).XPosition
COLOR 15, 3: PRINT Contact.City
LOCATE FieldInfo(7).YPosition, FieldInfo(7).XPosition
COLOR 15, 3: PRINT Contact.State
LOCATE FieldInfo(8).YPosition, FieldInfo(8).XPosition
COLOR 15, 3: PRINT Contact.ZipCode
LOCATE FieldInfo(9).YPosition, FieldInfo(9).XPosition
COLOR 15, 3: PRINT Contact.HomePhone
LOCATE FieldInfo(10).YPosition, FieldInfo(10).XPosition
COLOR 15, 3: PRINT Contact.WorkPhone
LOCATE FieldInfo(11).YPosition, FieldInfo(11).XPosition
COLOR 15, 3: PRINT Contact.CellPhone
LOCATE FieldInfo(12).YPosition, FieldInfo(12).XPosition
COLOR 15, 3: PRINT Contact.PagerNumber
LOCATE FieldInfo(13).YPosition, FieldInfo(13).XPosition
COLOR 15, 3: PRINT Contact.Email
LOCATE FieldInfo(14).YPosition, FieldInfo(14).XPosition
COLOR 15, 3: PRINT Contact.Website
END SUB
' ================================================== ' NAME: DisplayEmptyFields() ' PARAMETERS: None ' ASSUMES: Nothing ' CALLED FROM: Main Program Section ' ----------------------------------------------------- ' DESCRIPTION: This Subroutine loops through the ' defined Field definitions to display ' the set of empty fields on the screen. ' ================================================== SUB DisplayEmptyFields
DIM Counter AS INTEGER
FOR Counter = 1 TO 14
LOCATE FieldInfo(Counter).YPosition, FieldInfo(Counter).XPosition
COLOR 0, 3
PRINT SPACE$(FieldInfo(Counter).FieldLength)
NEXT Counter
END SUB
' =========================================== ' NAME: DrawMainScreen() ' PARAMETERS: None ' ASSUMES: Nothing ' CALLED FROM: Main Section of the program ' ---------------------------------------------- ' DESCRIPTION: This subroutine clears the ' screen and draws the background ' screen of the program. ' =========================================== SUB DrawMainScreen
DIM Counter AS INTEGER
DIM WorkString AS STRING
CLS
LOCATE 1, 1: COLOR 0, 5: PRINT SPACE$(80);
LOCATE 1, 1: COLOR 14, 5: PRINT "Contacts Professional 1.00.000a"
LOCATE 1, 49: COLOR 15, 5: PRINT "Random Access File Demonstration"
LOCATE 2, 1: COLOR 0, 7: PRINT SPACE$(80);
LOCATE 3, 1: COLOR 7, 1: PRINT CHR$(201); STRING$(78, CHR$(205)); CHR$(187)
LOCATE 4, 1: COLOR 7, 1: PRINT CHR$(186); SPACE$(78); CHR$(186)
LOCATE 4, 3: COLOR 7, 1: PRINT "CURRENT CONTACT INFORMATION"
WorkString = "RECORD: " + STR$(RecordNumber) + " OF " + STR$(RecordCount)
LOCATE 4, 79 - LEN(WorkString): PRINT WorkString
LOCATE 5, 1: COLOR 7, 1: PRINT CHR$(204); STRING$(78, CHR$(205)); CHR$(185)
FOR Counter = 6 TO 23
LOCATE Counter, 1: COLOR 7, 1: PRINT CHR$(186); SPACE$(78); CHR$(186)
NEXT Counter
LOCATE 7, 1: COLOR 7, 1: PRINT CHR$(199); STRING$(78, CHR$(196)); CHR$(182)
LOCATE 10, 1: COLOR 7, 1: PRINT CHR$(199); STRING$(78, CHR$(196)); CHR$(182)
LOCATE 16, 1: COLOR 7, 1: PRINT CHR$(199); STRING$(78, CHR$(196)); CHR$(182)
LOCATE 21, 1: COLOR 7, 1: PRINT CHR$(199); STRING$(78, CHR$(196)); CHR$(182)
' -------------------
' Draw field labels
' -------------------
LOCATE 6, 3: COLOR 7, 1: PRINT "Contact ID....:"
LOCATE 8, 3: COLOR 7, 1: PRINT "Company Name..:"
LOCATE 9, 3: COLOR 7, 1: PRINT "Contact Name..:"
LOCATE 11, 3: COLOR 7, 1: PRINT "Address One...:"
LOCATE 12, 3: COLOR 7, 1: PRINT "Address Two...:"
LOCATE 13, 3: COLOR 7, 1: PRINT "City..........:"
LOCATE 14, 3: COLOR 7, 1: PRINT "State.........:"
LOCATE 15, 3: COLOR 7, 1: PRINT "Zip Code......:"
LOCATE 17, 3: COLOR 7, 1: PRINT "Home Phone....:"
LOCATE 18, 3: COLOR 7, 1: PRINT "Work Phone....:"
LOCATE 19, 3: COLOR 7, 1: PRINT "Cell Phone....:"
LOCATE 20, 3: COLOR 7, 1: PRINT "Pager Number..:"
LOCATE 22, 3: COLOR 7, 1: PRINT "Email Address.:"
LOCATE 23, 3: COLOR 7, 1: PRINT "Website URL...:"
LOCATE 24, 1: COLOR 7, 1: PRINT CHR$(200); STRING$(78, CHR$(205)); CHR$(188);
LOCATE 25, 1: COLOR 0, 5: PRINT SPACE$(80);
LOCATE 25, 1: COLOR 7, 5: PRINT " <"; CHR$(17) + CHR$(16) + "=Menu><PgUp=Prev><PgDn=Next><Ins=New><Del=Delete><Enter=Edit>";
END SUB
SUB GetContactFields
END SUB
' ========================================== ' NAME: ListAllContacts() ' PARAMETERS: None ' RETURNS: Integer for the selected menu ' CALLED FROM: Main Section of the program ' ASSUMES: Nothing ' --------------------------------------------- ' DESCRIPTION: This subroutine goes about ' listing all the contact data ' that is currently present in ' the data file, one after the ' other. ' ========================================== SUB ListAllContacts
' -----------------------------------------------
' Work Variables we'll need to perform the list
' -----------------------------------------------
DIM WorkContact AS ContactInformation
DIM WorkRecord AS LONG
DIM WorkKey AS STRING
' ------------------------------------------------------
' Perform the list of contacts only if we have records
' ------------------------------------------------------
IF RecordCount > 0 THEN
CLS
' ----------------------------------------
' Position ourselves on the first record
' ----------------------------------------
SEEK #FileHandle, 1
' ------------------------------------------------
' Loop through the records and print record data
' ------------------------------------------------
DO WHILE NOT EOF(FileHandle)
GET #FileHandle, , WorkContact
PRINT WorkContact.ContactID; " "; WorkContact.ContactName; " "; WorkContact.HomePhone
LOOP
' ---------------------
' Await User KeyPress
' ---------------------
PRINT "Press Any Key To Go Back To Contact Screen."
DO WHILE WorkKey = ""
WorkKey = INKEY$
LOOP
' ------------------------------------
' Go back to the main contact screen
' ------------------------------------
CALL DrawMainScreen
SEEK #FileHandle, RecordNumber
CALL DisplayContactFields
END IF
END SUB
' ========================================== ' NAME: MainMenu() ' PARAMETERS: None ' CALLED FROM: Main Section of the program ' ASSUMES: Nothing ' --------------------------------------------- ' DESCRIPTION: This Displays a main menu ' and awaits the user's input ' Calls the proper subroutine ' depending on the selected ' menu option. ' ========================================== SUB MainMenu
' ---------------------------------------
' Local variables used in this function
' ---------------------------------------
DIM Menus(6) AS STRING
DIM Counter AS INTEGER
DIM Current AS INTEGER
DIM Keyboard AS STRING
' ------------------------------------------
' Assign the Menu Items to the Menus Array
' ------------------------------------------
Menus(0) = "New"
Menus(1) = "Edit"
Menus(2) = "Delete"
Menus(3) = "List"
Menus(4) = "Pack Data"
Menus(5) = "Exit"
Current = 0
DO WHILE TRUE
' ------------------------
' Display the menu items
' ------------------------
LOCATE 2, 16
FOR Counter = 0 TO 5
IF Counter = Current THEN
COLOR 7, 0
ELSE
COLOR 0, 7
END IF
PRINT Menus(Counter); : COLOR 0, 7: PRINT " ";
NEXT Counter
' --------------------------------------------------
' Handle the keyboard to allow for menu selection
' --------------------------------------------------
Keyboard = INKEY$
SELECT CASE Keyboard
CASE CHR$(0) + CHR$(75) ' <- Left Arrow
Current = Current - 1
IF Current < 0 THEN
Current = 5
END IF
CASE CHR$(0) + CHR$(77) ' <- Right Arrow
Current = Current + 1
IF Current > 5 THEN
Current = 0
END IF
CASE CHR$(0) + CHR$(73) ' <- Page Up
CALL MoveToPrevious
CASE CHR$(0) + CHR$(81) ' <- Page Down
CALL MoveToNext
CASE CHR$(13)
' ---------------------------------------------
' Call appropriate subroutine based on choice
' ---------------------------------------------
SELECT CASE Current
CASE 0
CALL AddAContact
CASE 1
CALL UpdateAContact
CASE 2
CALL DeleteAContact
CASE 3
CALL ListAllContacts
CASE 4
CALL PackContactFile
CASE 5
CanExit = TRUE
EXIT DO
END SELECT
END SELECT
LOOP
END SUB
' ============================================== ' NAME: MoveToNext() ' PARAMETERS: None ' ASSUMES: DataFile for #FileHandle Opened ' CALLED FROM: MainMenu Subroutine ' ------------------------------------------------- ' DESCRIPTION: Adds 1 to the current RecordNumber ' and adjust to the end of file if ' needed, then loads Contact Info ' from the current record and calls ' DisplayContactFields to show the ' loaded contact information. ' ============================================== SUB MoveToNext
RecordNumber = RecordNumber + 1
IF RecordNumber > RecordCount THEN
RecordNumber = RecordCount
END IF
GET #FileHandle, RecordNumber, Contact
CALL DisplayContactFields
END SUB
' ============================================== ' NAME: MoveToPrevious() ' PARAMETERS: None ' ASSUMES: DataFile for #FileHandle Opened ' CALLED FROM: MainMenu Subroutine ' ------------------------------------------------- ' DESCRIPTION: takes 1 from RecordNumber variable ' and adjust to the start of file if ' needed, then loads Contact Info ' from the current record and calls ' DisplayContactFields to show the ' loaded contact i`nformation. ' ============================================== SUB MoveToPrevious
RecordNumber = RecordNumber - 1
IF RecordNumber < 1 THEN
RecordNumber = 1
END IF
GET #FileHandle, RecordNumber, Contact
CALL DisplayContactFields
END SUB
' ========================================== ' NAME: PackContactFile() ' PARAMETERS: None ' RETURNS: Integer for the selected menu ' CALLED FROM: Main Section of the program ' ASSUMES: Nothing ' --------------------------------------------- ' DESCRIPTION: This subroutine performs the ' physical deletion of records ' marked for deletion. To do so ' it will create a temporary ' file, copy all records not ' marked for Deletion to the new ' file, once done, it will erase ' the data file, and rename the ' temporary file to the official ' data file name. ' ========================================== SUB PackContactFile
' ---------------------------
' Work Variables we'll need
' ---------------------------
DIM WorkContact AS ContactInformation
DIM Counter AS LONG
DIM SourceHandle AS INTEGER
DIM DestinationHandle AS INTEGER
' -----------------------------------------
' First we get handles and open the files
' -----------------------------------------
CLOSE #FileHandle
SourceHandle = FREEFILE
OPEN "CONTACTS.DAT" FOR RANDOM AS #SourceHandle LEN = LEN(WorkContact)
DestinationHandle = FREEFILE
OPEN "TEMP.DAT" FOR RANDOM AS #DestinationHandle LEN = LEN(WorkContact)
Counter = 0
' ----------------------------------------------------
' Main loop to do the copying of non deleted records
' ----------------------------------------------------
DO WHILE NOT EOF(SourceHandle)
GET #SourceHandle, , WorkContact
IF WorkContact.Status <> "D" THEN
Counter = Counter + 1
' -----------------------------------
' We add the record in the datafile
' -----------------------------------
SEEK #DestinationHandle, Counter
PUT #DestinationHandle, Counter + 1, Contact
END IF
LOOP
' ------------------------
' Close the opened files
' ------------------------
CLOSE #DestinationHandle
CLOSE #SourceHandle
' ----------------------------------------------------------
' Delete contact file and rename temp file to CONTACTS.DAT
' ----------------------------------------------------------
KILL "CONTACTS.DAT"
NAME "TEMP.DAT" AS "CONTACTS.DAT"
OPEN "CONTACTS.DAT" FOR RANDOM AS #FileHandle LEN = LEN(WorkContact)
END SUB
' ============================================= ' NAME: UpdateAContact() ' PARAMETERS: None ' RETURNS: Integer for the selected menu ' CALLED FROM: Main Section of the program ' ASSUMES: Nothing ' ------------------------------------------------ ' DESCRIPTION: This sub start the data entry for ' fields and after saves the ' information to the file. ' ============================================= SUB UpdateAContact
DIM WorkContact AS ContactInformation
' ----------------------------------------
' First we start the data entry process
' Work from a temp stucture so if there
' are changes they are updated and shown
' ----------------------------------------
WorkContact = Contact
LOCATE FieldInfo(1).YPosition, FieldInfo(1).XPosition: COLOR 14, 7
INPUT WorkContact.ContactID
LOCATE FieldInfo(1).YPosition, FieldInfo(1).XPosition: COLOR 14, 3
PRINT USING "######"; Contact.ContactID
LOCATE FieldInfo(2).YPosition, FieldInfo(2).XPosition: COLOR 14, 7
INPUT WorkContact.CompanyName
IF (Contact.CompanyName <> WorkContact.CompanyName) AND (WorkContact.CompanyName <> "") THEN
Contact.CompanyName = WorkContact.CompanyName
END IF
LOCATE FieldInfo(2).YPosition, FieldInfo(2).XPosition: COLOR 14, 3
PRINT Contact.CompanyName
LOCATE FieldInfo(3).YPosition, FieldInfo(3).XPosition: COLOR 14, 7
INPUT WorkContact.ContactName
IF (Contact.ContactName <> WorkContact.ContactName) AND (WorkContact.ContactName <> "") THEN
Contact.ContactName = WorkContact.ContactName
END IF
LOCATE FieldInfo(3).YPosition, FieldInfo(3).XPosition: COLOR 14, 3
PRINT Contact.ContactName
LOCATE FieldInfo(4).YPosition, FieldInfo(4).XPosition: COLOR 14, 7
INPUT Contact.Address1
IF (Contact.Address1 <> WorkContact.Address1) AND (WorkContact.Address1 <> "") THEN
Contact.Address1 = WorkContact.Address1
END IF
LOCATE FieldInfo(4).YPosition, FieldInfo(4).XPosition: COLOR 14, 3
PRINT Contact.Address1
LOCATE FieldInfo(5).YPosition, FieldInfo(5).XPosition: COLOR 14, 7
INPUT Contact.Address2
IF (Contact.Address2 <> WorkContact.Address2) AND (WorkContact.Address2 <> "") THEN
Contact.Address2 = WorkContact.Address2
END IF
LOCATE FieldInfo(5).YPosition, FieldInfo(5).XPosition: COLOR 14, 3
PRINT Contact.Address2
LOCATE FieldInfo(6).YPosition, FieldInfo(6).XPosition: COLOR 14, 7
INPUT Contact.City
IF (Contact.City <> WorkContact.City) AND (WorkContact.City <> "") THEN
Contact.City = WorkContact.City
END IF
LOCATE FieldInfo(6).YPosition, FieldInfo(6).XPosition: COLOR 14, 3
PRINT Contact.City
LOCATE FieldInfo(7).YPosition, FieldInfo(7).XPosition: COLOR 14, 7
INPUT Contact.State
IF (Contact.State <> WorkContact.State) AND (WorkContact.State <> "") THEN
Contact.State = WorkContact.State
END IF
LOCATE FieldInfo(7).YPosition, FieldInfo(7).XPosition: COLOR 14, 3
PRINT Contact.State
LOCATE FieldInfo(8).YPosition, FieldInfo(8).XPosition: COLOR 14, 7
INPUT Contact.ZipCode
IF (Contact.ZipCode <> WorkContact.ZipCode) AND (WorkContact.ZipCode <> "") THEN
Contact.ZipCode = WorkContact.ZipCode
END IF
LOCATE FieldInfo(8).YPosition, FieldInfo(8).XPosition: COLOR 14, 3
PRINT Contact.ZipCode
LOCATE FieldInfo(9).YPosition, FieldInfo(9).XPosition: COLOR 14, 7
INPUT Contact.HomePhone
IF (Contact.HomePhone <> WorkContact.HomePhone) AND (WorkContact.HomePhone <> "") THEN
Contact.HomePhone = WorkContact.HomePhone
END IF
LOCATE FieldInfo(9).YPosition, FieldInfo(9).XPosition: COLOR 14, 3
PRINT Contact.HomePhone
LOCATE FieldInfo(10).YPosition, FieldInfo(10).XPosition: COLOR 14, 7
INPUT Contact.WorkPhone
IF (Contact.WorkPhone <> WorkContact.WorkPhone) AND (WorkContact.WorkPhone <> "") THEN
Contact.WorkPhone = WorkContact.WorkPhone
END IF
LOCATE FieldInfo(10).YPosition, FieldInfo(10).XPosition: COLOR 14, 3
PRINT Contact.WorkPhone
LOCATE FieldInfo(11).YPosition, FieldInfo(11).XPosition: COLOR 14, 7
INPUT Contact.CellPhone
IF (Contact.CellPhone <> WorkContact.CellPhone) AND (WorkContact.CellPhone <> "") THEN
Contact.CellPhone = WorkContact.CellPhone
END IF
LOCATE FieldInfo(11).YPosition, FieldInfo(11).XPosition: COLOR 14, 3
PRINT Contact.CellPhone
LOCATE FieldInfo(12).YPosition, FieldInfo(12).XPosition: COLOR 14, 7
INPUT Contact.PagerNumber
IF (Contact.PagerNumber <> WorkContact.PagerNumber) AND (WorkContact.PagerNumber <> "") THEN
Contact.PagerNumber = WorkContact.PagerNumber
END IF
LOCATE FieldInfo(12).YPosition, FieldInfo(12).XPosition: COLOR 14, 3
PRINT Contact.PagerNumber
LOCATE FieldInfo(13).YPosition, FieldInfo(13).XPosition: COLOR 14, 7
INPUT Contact.Email
IF (Contact.Email <> WorkContact.Email) AND (WorkContact.Email <> "") THEN
Contact.Email = WorkContact.Email
END IF
LOCATE FieldInfo(13).YPosition, FieldInfo(13).XPosition: COLOR 14, 3
PRINT Contact.Email
LOCATE FieldInfo(14).YPosition, FieldInfo(14).XPosition: COLOR 14, 7
INPUT Contact.Website
IF (Contact.Website <> WorkContact.Website) AND (WorkContact.Website <> "") THEN
Contact.Website = WorkContact.Website
END IF
LOCATE FieldInfo(14).YPosition, FieldInfo(14).XPosition: COLOR 14, 3
PRINT Contact.Website
' ------------------------------------
' We save the record in the datafile
' ------------------------------------
PUT #FileHandle, RecordNumber, Contact
' -------------------------------------
' Update RecordCount and RecordNumber
' -------------------------------------
SEEK #FileHandle, RecordNumber
END SUB
NOTES:
- To have this 2nd example work in FreeBasic, you should change the keyboard values that I check (in the MainMenu subroutine). Right now keys are compare with CHR$(0) + CHR$(75) for left arrow. All CHR$(0) should be changed to CHR$(255) to work properly in FreeBasic.
- I've split up all functionalities in documented (commented) subs for the sake of clarity and for the sake of being able to look at one functionality at a time and because I downright hate Gotos. On the good side this is good practice to stay away from the goto statement as much as you can.
- I use FreeFile to get the file handles. Yes here too. Hopefully between my sequential file and this random access file you'll understand that FreeFile is not evil at all.
- I close the file at the end. Again this is good practice even if technically ending a program is supposed to close all opened handles. I just don't trust it enough to omit from closing the file myself.
- The list all contact options is very rudimentary in such that it simply clears the screen and list all contacts. Reporting is not the object of this tutorial, it just does the job.
- As mentionned above, the Pack command will create a temporary file, open the temp file and the data file to perform the actual physical deletion of the records marked for deletion. Then it deletes the datafile, renames the temp file to the datafile's original name and exits.
IN CONCLUSION:
And there you have it. Hopefully, at the end of this tutorial, you'll have a good enough understanding of how Random Access Files work. A good enough understanding so you can "almost fluently" use it in your own programming projects. I used contact information in my examples, remember they could just as easily be highscore tables, list of characters (if your project has a character builder) or whatever else can be consider as tabular data or data that can be saved in a user defined type.
I hope you've enjoyed reading this tutorial at least as much as I've enjoyed writing it for you.
Stephane Richards