Lesson 11: File Input and Output
Including a side lesson on the Command Line Interface

Files are an important part of any game whether to hold high scores, player information, stats and configuration settings, or assets such as sounds and images. This lesson is going to assume you already have a working knowledge of drive and directory structure. If you know what the following terms mean and how they pertain to drive and directory structure then continue on with this lesson.

Root, directory, subdirectory, tree, command line interface, shell, extension, and path.

If you are unfamiliar with these terms I highly suggest you view this side lesson that was created that will instruct you on how to use the command prompt of your computer and how these skills relate to QB64.

View the side-lesson on the Command Line Interface and how it relates to QB64

The programs you write communicate with the underlying operating system. In order to speak correctly to the operating system's drive and directory structure the programmer needs to know how to format the parameter structures sent to it. This is true for any programming language and operating system combination you may decide to use. If you are using Linux or MacOS with this course then find a good tutorial on your operating system's drive and directory structure and how to navigate it at the command prompt. In Linux and MacOS the command prompt is often referred to as the Terminal.

Directory Structure: The _STARTDIR$ Statement

The _STARTDIR$ statement returns the path where the program resides. User's may decide to install your program anywhere on their computer. The _STARTDIR$ statement let's your program know exactly where it resides on the user's hard drive.

PRINT _STARTDIR$ ' display the path where the program resides

If you followed the directions in the Install QB64 section and installed the qb64 folder onto your computer's desktop then:

C:\Users\<your login name>\Desktop\qb64

should have been printed to your screen (or very similar). The <your login name> area should contain the account name you use on your computer.

Directory Structure: The CWD$ Statement

The _CWD$ statement returns the path of the current working directory.

PRINT _CWD$ ' display the current working directory (folder)

If you followed the directions in the Install QB64 section and installed the qb64 folder onto your computer's desktop then:

C:\Users\<your login name>\Desktop\qb64

should have been printed to your screen. The <your login name> area should contain the account name you use on your computer.

As you navigate around the hard drive the path contained in _CWD$ will change accordingly.

The current working directory is where files will be opened from and new files created. If you need to change the current working directory then use the CHDIR statement.

Directory Structure: The CHDIR Statement

Before files can be opened or created they need a place to reside within the drive's directory structure. The CHDIR statement allows you to navigate the directory structure by changing the current working directory. The asset file you downloaded and placed into your qb64 directory contains a "Lesson11" subdirectory located at the following path:

.\tutorial\Lesson11

To navigate to this folder you would issue the command:

CHDIR ".\tutorial\Lesson11" ' navigate to Lesson11 subdirectory (folder) from the current working directory

The ".\" signifies the current working directory, "tutorial" is the subdirectory you are navigating to and from there you are navigating to another subdirectory contained in "tutorial" named "Lesson11". Your current working directory is now "Lesson11" and any files you want to open need to be located here, and likewise, any files you create will also be created here. At the command prompt you get a nice visual path indicator of this:

C:\users\< your login name>\desktop\qb64\tutorial\Lesson11>

However in QB64 there is no visual indication of what your current working directory is. You'll need to keep track of this through the code you write.

You can also use parent directory notation ( .. ) to move back one level just like at the command prompt. To navigate back to the qb64 directory you could issue the command:

CHDIR "..\.." ' move down the tree two parent directories (folders)

and this would move you down the tree two levels back to the qb64 directory.

To change to a different drive such as the D: drive issue:

CHDIR "D:\" ' move to a different drive and directory (folder)

If you want to go directly to the root of the current drive you are on issue the command:

CHDIR "\" ' move to the root directory (folder) of the current drive

As you can see CHDIR is basically QB64's version of the Windows and Linux command line's CD command.

Directory Structure: The MKDIR Statement

The MKDIR statement is used to create a directory or subdirectory in the current working directory. The following command will create a directory called "MyFolder" inside the qb64 folder:

MKDIR "MyFolder" ' create directory or subdirectory (folder) in the current working directory

You can also supply a path to a valid subdirectory to create a directory anywhere on your drive regardless of your current working directory.

MKDIR "\RootFolder" '                   create a directory (folder) at the root of the current drive
MKDIR ".\tutorial\Lesson11\NewFolder" ' at the path from the current working directory
MKDIR "D:\MyFolder" '                   at the root of another drive

MKDIR is basically QB64's version of the Windows command line's MD command and Linux's terminal command MKDIR.

Directory Structure: The RMDIR Statement

The RMDIR statement is used to remove, or delete, a directory or subdirectory. Before a directory can be deleted it must be empty; no files or subdirectories can exist within it. If you attempt to remove a directory that is not empty you'll get the error shown in Figure 1 below:

Figure 1: Directory not empty

The following lines of code will create and then remove a directory from the current working directory.

MKDIR "MyFolder" '                                  create folder on the drive
PRINT "Folder created. Press a key to delete it." ' inform user
SLEEP '                                             wait for a key press
RMDIR "MyFolder" '                                  delete folder
PRINT "The folder has been deleted." '              inform user

RMDIR is basically QB64's version of the Windows command line's RD command and Linux's command line's RMDIR command.

Note: A deleted directory will not get placed into the Windows Recycle Bin.   <<--   READ THIS 3 TIMES!
Once a directory (folder) is removed it's gone forever.

Directory Structure: The _DIREXISTS Statement

If you attempt to navigate to a directory that does not exist QB64 will issue an error as seen in Figure 2 below:

Figure 2: Non-existent directory

The _DIREXISTS statement will test for the existence of a directory and return a numeric value of -1 (true) if the directory is found or a numeric value of 0 (false) if the directory is not found. It's always a good idea to test for the existence of a directory before attempting any other directory or file related statements with it. The following lines of code will test for the existence of the "Lesson11" subdirectory within the "tutorial" subdirectory.

IF _DIREXISTS(".\tutorial\lesson11") THEN ' does the folder exist?
   PRINT "Folder found!" '                  yes, inform the user
ELSE
   PRINT "Folder not found!" '              no, inform the user
END IF

Directory Structure: The NAME...AS Statement

The NAME...AS statement is used to rename either a file or directory name.

NAME "HighScores.txt" AS "HighScores.old" ' rename a file
NAME "C:\GAMES" AS "C:\MYGAMES" '           rename a directory (folder)

The NAME...AS statement can also be used to move a directory and its contents from one location to another.

NAME "C:\Users\Terry\Desktop\qb64\Project\" AS "C:\Games\Finished\NewGame\" ' move a directory (folder)

The directory named "Project" is getting renamed to "NewGame" and since the paths are different it and all of the files contained within it are getting moved from one location to another.

Directory Structure: The KILL Statement

The KILL statement is used to delete a file(s) from the drive. The use of wildcards is also acceptable.

KILL "HiScore.txt" '              delete a file from the current working directory
KILL "*.tmp" '                    delete all files with the .tmp extension
KILL ".\SupportFiles\Image.bak" ' delete a file in a specified path
KILL ".\backup\??04.*" '          delete all files in a specified path with 3rd and 4th characters of 04

There are a few things you should be aware of when using the KILL statement.

- Files that are deleted are not moved to the Windows Recycle Bin.   <<--   READ THIS THREE TIMES!
- Open files (files in use) can't be deleted. The program using the file must close it first.
- Files marked as read only can't be deleted. They must first have the read only attribute disabled.
- The KILL statement can't be used to delete directories. Use RMDIR instead.

Directory Structure: The _FILEEXISTS Statement

If you attempt to work with a file that does not exist QB64 will issue an error message as seen in Figure 2 above outlined in the _DIREXISTS statement. The _FILEEXISTS statement will test for the existence of a file and return a numeric value of -1 (true) if the file is found or a numeric value of 0 (false) if the file is not found. It is always a good idea to test for the existence of a file before attempting any file related statements with it. The following lines of code will test for the existence of the "I_Exist.txt" file within the ".\tutorial\Lesson11\" path. (The file is included with the tutorial asset file.)

IF _FILEEXISTS(".\tutorial\Lesson11\I_Exist.txt") THEN ' does the file exist?
   PRINT "File Found!" '                                 yes, inform the user
ELSE
   PRINT "File not found!" '                             no, inform the user
END IF

File Access: The OPEN Statement

The OPEN statement is used to open a file in a predetermined mode of operation.

OPEN Filename$ FOR mode AS #Filenumber&

Filename$ is the name of the file you wish to open and can be a literal string in quotes or a string variable that contains the name and path to the file.

The mode parameter denotes how the file is to be opened. The available OPEN modes are:

INPUT  - read contents from a file (sequential access)
OUTPUT - write contents to a file (sequential access)
APPEND - write contents to a file beginning at the end of the file (sequential access)
BINARY - read and write to a file at any position (database access)
RANDOM - read and write records from a file at any position (database access)

The Filenumber& parameter is used to give the file a handle number to reference. It's possible to have more than one file open at a time (over 2 billion if necessary) and the value given to Filenumber& is used to reference the individual file.

Sequential access to a file means that when a file is opened it can only be read from or written to in sequential, or a line by line style. Here we are opening a file for sequential read access:

OPEN ".\tutorial\Lesson11\I_Exist.txt" FOR INPUT AS #1 ' open for sequential read

When lines of data are read from this file the first line read will be at the top and last line read will be the bottom. There is no control of which line is read next. After a line is read in the following line becomes the next default line to be read in.

Here the file is being opened for sequential write access:

OPEN ".\tutorial\Lesson11\NewFile.txt" FOR OUTPUT AS #1 ' open for sequential write

When lines of data are written to this file the lines will be written from top to bottom. There is no control of which line is written to next. After a line is written the following line becomes the next default line to be written.

Note: If opening a file for OUTPUT and the file already exists the existing contents will be deleted to make room for the new content.

The final sequential method of opening a file is for appending, or adding to the end of a file:

OPEN ".\tutorial\Lesson11\I_Exist.txt" FOR APPEND AS #1 ' open for sequential appending

This mode is the same as OUTPUT accept that the file's contents are preserved and the writing of new lines begins at the end of the file.

We'll discuss the other two modes BINARY and RANDOM in a bit. For now let's discuss files that can be opened for sequential access. We'll start by creating a simple high score table that could be used in any game.

( This code can be found at .\tutorial\Lesson11\OutputDemo.bas )

OPEN "HISCORES.TXT" FOR OUTPUT AS #1 ' open for sequential writing
PRINT #1, "Fred Haise" '               write individual lines of data to the open file
PRINT #1, 10000
PRINT #1, "Jim Lovell"
PRINT #1, 9000
PRINT #1, "John Swigert"
PRINT #1, 8000
CLOSE #1 '                             close the sequential file

This code example creates a file called HISCORES.TXT in the current working directory. Keep in mind that if HISCORES.TXT already exists it will be overwritten and any existing contents will be destroyed. The handle number given to the file is #1 and from this point on is used to reference this open file. Line 2 of the code uses the PRINT statement to place information into the file:

PRINT #1, "Fred Haise"

By referencing the file handle of #1 the PRINT statement is instructed to place the string into the open file instead of to the screen. Opening the file in Windows Notepad reveals the file structure:

Figure 3: HISCORES.TXT opened in Notepad

The PRINT statements placed each piece of data on separate lines sequentially. When you are finished working with a file you need to close it and the CLOSE statement as seen in line 8 accomplishes this:

CLOSE #1

The file HISCORES.TXT opened with the handle of #1 is closed. If you do not close a file before your program ends execution there is the possibility of file corruption due to the fact that the file is left in an open state.

The next piece of code will be used to retrieve the data from the HISCORES.TXT file we just created.

( This code can be found at .\tutorial\Lesson11\ReadHiScores.bas )

DIM HSname$(3) '  high score player names
DIM HScore%(3) '  high scores
DIM Count% '      generic counter

IF _FILEEXISTS("HISCORES.TXT") THEN '                 does high score file exist?
   OPEN "HISCORES.TXT" FOR INPUT AS #1 '              yes, open sequential file
   PRINT "----------------------"
   PRINT "-- High Score Table --" '                   print high score table header
   PRINT "----------------------"
   FOR Count% = 1 TO 3 '                              get 3 names and values
       INPUT #1, HSname$(Count%) '                    get name from file
       INPUT #1, HScore%(Count%) '                    get score from file
       PRINT HSname$(Count%), " -"; HScore%(Count%) ' print the name and score
   NEXT Count%
   CLOSE #1 '                                         close the file
ELSE '                                                no, the file does not exist
   PRINT "High score file not found!" '               inform player of the error
END IF

Figure 4: High score data retrieved

In this example line 6 of the code opens the file for INPUT and gives it a file handle of #1. Lines 11 and 12 of the code use the INPUT statement to read data from the file:

INPUT #1, HSname$(Count%) '                    get name from file
INPUT #1, HScore%(Count%) '                    get score from file

By referencing the file handle of #1 the INPUT statement is told to get data from the open file instead of the keyboard.

Data can be appended, or added, to the file by opening it in APPEND mode. The following code will open the HISCORES.TXT file and add more lines of data to the end of it.

( This code can be found at .\tutorial\Lesson11\AppendDemo.bas )

OPEN "HISCORES.TXT" FOR APPEND AS #1 ' add to the file
PRINT #1, "Neil Armstrong"
PRINT #1, 7000
PRINT #1, "Buzz Aldrin"
PRINT #1, 6000
PRINT #1, "Gus Grissom"
PRINT #1, 5000
PRINT #1, "Michael Collins"
PRINT #1, 4000
PRINT #1, "Alan Shepard"
PRINT #1, 3000
PRINT #1, "Ken Mattingly"
PRINT #1, 2000
PRINT #1, "Edward White"
PRINT #1, 1000
CLOSE #1

Figure 5: Appended data

Opening the file once again in Notepad shows that the lines of data were added to the existing lines of data in the file.

File Access: The EOF Statement

In the code examples so far it is known how many pieces of data have been written to the HISCORE.TXT file. However, it's not always possible to know the exact number of lines contained within a file that need to be read in. The EOF statement can tell code when it has reached the end of a file. The following code will read the data contained in HISCORE.TXT but instead of setting up a loop with a known value it will use EOF to determine when the end of the file has been reached.

( This code can be found at .\tutorial\Lesson11\EOFDemo.bas )

REDIM HSname$(0) '  high score player names
REDIM HScore%(0) '  high scores
DIM Count% '        index counter

IF _FILEEXISTS("HISCORES.TXT") THEN '                 does high score file exist?
   OPEN "HISCORES.TXT" FOR INPUT AS #1 '              yes, open sequential file
   PRINT "----------------------"
   PRINT "-- High Score Table --" '                   print high scores to screen
   PRINT "----------------------"
    Count% = 0 '                                      initialize index counter
   WHILE NOT EOF(1) '                                 at end of file?
        Count% = Count% + 1 '                         no, increment index counter
       REDIM _PRESERVE HSname$(Count%) '              increase size of name array
       REDIM _PRESERVE HScore%(Count%) '              increase size of score array
       INPUT #1, HSname$(Count%) '                    get name from file
       INPUT #1, HScore%(Count%) '                    get score from file
       PRINT HSname$(Count%); " -"; HScore%(Count%) ' print the name and score
   WEND '                                             loop back to WHILE
   CLOSE #1 '                                         close the file
ELSE '                                                no, the file does not exist
   PRINT "High score file not found!" '               inform player of the error
END IF

In this example we use dynamic arrays that grow in size depending on the amount of data read in from the file. Line 11 of the example:

WHILE NOT EOF(1) '                                 at end of file?

only allows entry into the loop if the end of the file has not been reached. The EOF statement will return a value of -1 (true) when the end of the file has been reached and a value of 0 (false) when not at the end of the file. EOF(1) refers to the file that has been opened with a file handle of #1. Once inside the loop the dynamic arrays are increased in size and the data read in using the INPUT statements.

File Access: The WRITE Statement

There is a more efficient way to read and write data to and from a sequential file using the WRITE statement instead of the PRINT statement. The WRITE statement creates a Comma Separated Values, or CSV, file that can be read in more efficiently. First, we need to create a new HISCORES.TXT file written as a CSV file.

( This code can be found at .\tutorial\Lesson11\WriteDemo.bas )

DIM Ok$ ' response from user asking to overwrite file

IF _FILEEXISTS("HISCORES.TXT") THEN '              does file already exist?
   PRINT '                                         yes, ask permission to overwrite
   PRINT " HISCORES.TXT already exists!"
   PRINT
   LINE INPUT " Ok to overwrite? (yes/no): ", Ok$
   PRINT
END IF
IF UCASE$(LEFT$(Ok$, 1)) = "Y" THEN '              OK to overwrite?
   OPEN "HISCORES.TXT" FOR OUTPUT AS #1 '          yes, open sequential file for writing
   WRITE #1, "Fred Haise", 10000 '                 create CSV file entries
   WRITE #1, "Jim Lovell", 9000
   WRITE #1, "John Swigert", 8000
   WRITE #1, "Neil Armstrong", 7000
   WRITE #1, "Buzz Aldrin", 6000
   WRITE #1, "Gus Grissom", 5000
   WRITE #1, "Michael Collins", 4000
   WRITE #1, "Alan Shepard", 3000
   WRITE #1, "Ken Mattingly", 2000
   WRITE #1, "Edward White", 1000
   CLOSE #1 '                                      close the sequential file
   PRINT " New HISCORES.TXT file created." '       inform user
ELSE '                                             no
   PRINT " Original HISCORES.TXT file retained." ' inform user
END IF

Figure 6: Comma separated values

When opened in Notepad as seen in Figure 6 above strings are surrounded in quotes and numeric values are written as is. Most spreadsheet programs can read and write CSV files allowing the programmer a way to create very large CSV files for use with QB64 and other software.  The example code below shows how a CSV file's data can be read and make the code as efficient as possible.

( This code can be found at .\tutorial\Lesson11\CSVReadDemo.bas )

TYPE HIGHSCORE '              type definition defining high scores
    Pname AS STRING * 15 '    player name
    Pscore AS INTEGER '       player score
END TYPE

REDIM Score(0) AS HIGHSCORE ' dynamic array to hold high scores

IF _FILEEXISTS("HISCORES.TXT") THEN '                          does high score file exist?
   OPEN "HISCORES.TXT" FOR INPUT AS #1 '                       yes, open sequential file
   PRINT "----------------------"
   PRINT "-- High Score Table --" '                            print high scores to screen
   PRINT "----------------------"
   WHILE NOT EOF(1) '                                          at end of file?
       REDIM _PRESERVE Score(UBOUND(Score) + 1) AS HIGHSCORE ' increase dynamic array index by one
       INPUT #1, Score(UBOUND(Score)).Pname, Score(UBOUND(Score)).Pscore ' get data from file
       PRINT Score(UBOUND(Score)).Pname; " -"; '               display player name
       PRINT Score(UBOUND(Score)).Pscore '                     display player score
   WEND '                                                      loop back to WHILE
   CLOSE #1 '                                                  close the file
   PRINT UBOUND(Score); " total scores found" '                display number of scores found
ELSE '                                                         no, the file does not exist
   PRINT "High score file not found!" '                        inform player of the error
END IF

Figure 7: CSV file loaded

In line 15 of the example:

INPUT #1, Score(UBOUND(Score)).Pname, Score(UBOUND(Score)).Pscore ' get data from file

two variables were read in using a single INPUT statement. You can keep adding commas and variables as needed to read entire lines of CSV values in. The quotes surrounding the strings are automatically removed before the values are placed into string variables.

LINE INPUT can also be used to read data from sequential files but it's best not to use it with comma separated data. LINE INPUT will grab a line exactly as it appears in the file and place it into a string. Remember that LINE INPUT can only accept a string as a variable parameter. Try this little example program that uses LINE INPUT to read in the HISCORES.TXT CSV file that was just created.

( This code can be found at .\tutorial\Lesson11\LineInputDemo.bas )

OPEN "HISCORES.TXT" FOR INPUT AS #1
WHILE NOT EOF(1)
   LINE INPUT #1, a$
   PRINT a$
WEND
CLOSE #1

Figure 8: LINE INPUT reading a CSV file

As Figure 8 illustrates the data brought in mirrors what was seen in Notepad when viewing the file. The programmer would have to perform extra string manipulation steps to parse the data into something meaningful.

File Access: The FREEFILE Statement

The FREEFILE statement returns an unused file handle number to be used when opening files. Up to this point we have been manually assigning #1 as the file handle in the example code listings. If another file would happen to be using that same file handle then your program would generate an error. For small programs assigning file handle values manually is probably alright. However, as your programs get larger and especially if you have subroutines and/or functions opening files then the possibility using the same file handle number twice greatly increases. It's considered good programming practice to use the FREEFILE statement to return an unused file handle value for you.

( This code can be found at .\tutorial\Lesson11\FreefileDemo.bas )

DIM FileHandle& ' file handle value
DIM MyData$ '     data retrieved from file

IF _FILEEXISTS("HISCORES.TXT") THEN '              file exist?
    FileHandle& = FREEFILE '                       yes, get a file handle
   OPEN "HISCORES.TXT" FOR INPUT AS #FileHandle& ' open file using handle
   WHILE NOT EOF(FileHandle&) '                    end of file?
       INPUT #FileHandle&, MyData$ '               no, get data from file
       PRINT MyData$ '                             print data to screen
   WEND
   CLOSE #FileHandle& '                            close file
END IF

File Access: The LOF Statement

The LOF statement returns the size in bytes of a file's contents, or the length of the file. LOF will return the numeric value of zero if no data is present in the file. The following example shows how to use LOF to load an entire file into a string for searching. Notice also that the file was opened in BINARY mode which will be discussed later.

( This code can be found at .\tutorial\Lesson11\LOFDemo.bas )

FileName$ = "HISCORES.TXT"
Search$ = "5000"

IF _FILEEXISTS(FileName$) THEN '               does file exist?
    SearchFile% = FREEFILE '                   yes, get a free handle number
   OPEN FileName$ FOR BINARY AS #SearchFile% ' open the file for binary input
    Contents$ = SPACE$(LOF(SearchFile%)) '     create a record the same length as file
   GET #SearchFile%, , Contents$ '             get the record (the entire file!)
   CLOSE #SearchFile% '                        close the file
   IF INSTR(Contents$, Search$) THEN '         was search term found?
        Inspect% = -1 '                        yes, return true
       PRINT "Found!"
   END IF
END IF

Updating a Sequential File

The benefit of sequential files is their ease of use but the one major drawback is their sequential nature. You must read and write data from the top to bottom. This makes it very difficult to insert data somewhere in the middle of a sequential file. This can be done in a number of different ways but always entails extra work on the programmer's part. The example code below shows one method in which this can be done.

( This code can be found at .\tutorial\Lesson11\InsertDemo.bas )

TYPE HIGHSCORE '           type definition defining high scores
    Pname AS STRING * 15 ' player name
    Pscore AS INTEGER '    player score
END TYPE

DIM Score AS HIGHSCORE '   a score line from CSV file
DIM NewName$ '             name of new player to add to high score list
DIM NewScore% '            new player's score to add to high score list

NewName$ = "Edgar Mitchell" '                          new player's name
NewScore% = 5500 '                                     new player's score
IF _FILEEXISTS("HISCORES.TXT") THEN '                  high score file exist?
   OPEN "HISCORES.TXT" FOR INPUT AS #1 '               yes, open high score file for INPUT
   OPEN "TEMP.TXT" FOR OUTPUT AS #2 '                  open temporary file for OUTPUT
   WHILE NOT EOF(1) '                                  end of high score file?
       INPUT #1, Score.Pname, Score.Pscore '           no, read a line from file
       IF Score.Pscore <= NewScore% THEN '             is this score less than the new score?
           WRITE #2, NewName$, NewScore% '             yes, insert the new player here
           PRINT NewName$, NewScore%; " <-- Insert"
            NewScore% = 0 '                            reset new score so IF can't happen again
       END IF
       WRITE #2, _TRIM$(Score.Pname), Score.Pscore '   write the original file's score line
       PRINT Score.Pname, Score.Pscore
   WEND
   CLOSE #2, #1 '                                      close both files
   KILL "HISCORES.TXT" '                               delete the original file
   NAME "TEMP.TXT" AS "HISCORES.TXT" '                 rename the temp file to the original name
ELSE '                                                 no, high score file not present
   PRINT "High score file not found!" '                inform player
END IF

Figure 9: New player and score inserted

Figure 10: HISCORE.TXT with new information inserted

The example code shows a method of opening the original file for INPUT and another temporary file for OUTPUT. As each line of data read in from the original file is copied over to the temporary file. Line 17 of the code:

IF Score.Pscore <= NewScore% THEN '             is this score less than the new score?

chooses the right time to insert the new values into the temporary file. Once all the original lines of data have been copied from the original file to the temporary file the original file is deleted. The temporary file that was created is then renamed to the original file.

Creating a Random Access File (Database)

There is a method that QB64 offers to set up file access that uses records to store data that can be accessed quickly through the use of a record number. A file record has a set length and by knowing the length a simple calculation can be made to determine where any record is stored within this type of file, known as a database. The RANDOM mode of file access allows the programmer to store information in a manner that makes retrieval fast no matter where the information is located within the file.

Let's start by creating a random access database file and then discussing each component of the code in detail. Load or type in the following code and then execute it to create the HISCORES.DAT records database file.

( This code can be found at .\tutorial\Lesson11\MakeDatabase.bas )

'
' Demo: Create a database of high scores
'     : MakeDatabase.bas
'     : QB64 tutorial - Lesson 11
'
'+------------------------------------------------+
'| Each record will use 34 bytes of storage space |
'+------------------------------------------------+

TYPE HISCORE '                High score record structure
    Player AS STRING * 30 '   30 bytes: the player's name
    HiScore AS LONG '         4 bytes : the player's high score
END TYPE

DIM Record AS HISCORE '       an individual database record
DIM RecordLength AS INTEGER ' length of an individual record (in bytes)
DIM FileNumber AS LONG '      the file handle number
DIM RecordNumber AS INTEGER ' record number counter
DIM FileSize AS INTEGER '     size of database file (in bytes)

'+--------------------+
'| Set initial values |
'+--------------------+

RecordLength = LEN(Record) '                                       get the length of each record in the database
FileNumber = FREEFILE '                                            get a free file handle
OPEN "HISCORES.DAT" FOR RANDOM AS #FileNumber LEN = RecordLength ' open database for reading/writing

'+----------------------+
'| Begin record writing |
'+----------------------+

PRINT
RecordNumber = 0 '                                                 reset record number counter
DO '                                                               begin database write loop
   READ Record.Player, Record.HiScore '                            read next 2 values from DATA statements below
   IF Record.HiScore THEN '                                        is value greater than 0?
        RecordNumber = RecordNumber + 1 '                          yes, increment record number
       PRINT " Writing record number"; RecordNumber; " -> "; '     show data being written
       PRINT Record.HiScore; Record.Player
       PUT FileNumber, RecordNumber, Record '                      write record to database
   END IF
LOOP UNTIL Record.HiScore = 0 '                                    leave when all DATA statements READ in
FileSize = LOF(FileNumber) '                                       get total size of file
PRINT
PRINT
FileSize \ RecordLength; "records created" '                 report number of records created
PRINT " Each record took"; RecordLength; "of bytes of storage" '   report number of bytes in each record
PRINT " for a total of file size of"; FileSize; "bytes" '          report size of database file
CLOSE FileNumber '                                                 close the database file
SLEEP '                                                            wait for key stroke
SYSTEM '                                                           return to operating system

'+-----------------------------+
'| Values to store in database |
'+-----------------------------+

DATA "Fred Haise",10000
DATA "Jim Lovell",9000
DATA "John Swigert",8000
DATA "Neil Armstrong",7000
DATA "Buzz Aldrin",6000
DATA "Gus Grissom",5000
DATA "Michael Collins",4000
DATA "Alan Shepard",3000
DATA "Ken Mattingly",2000
DATA "Edward White",1000
DATA "",0

Line 27 of the code above:

OPEN "HISCORES.DAT" FOR RANDOM AS #FileNumber LEN = RecordLength ' open database for reading/writing

opens the file HISCORES.DAT for random access by using the RANDOM mode. Note that the file will be overwritten if it already exists. When a file is opened for random access the length of each record must also be specified as seen in the line of code above:

LEN = RecordLength

The length of an individual record is determined by the data that is to be stored within it. With this program a user defined TYPE structure has been used to identify the values contained within the record:

TYPE HISCORE '                High score record structure
    Player AS STRING * 30 '   30 bytes: the player's name
    HiScore AS LONG '         4 bytes : the player's high score
END TYPE

DIM Record AS HISCORE '       an individual database record

Every record in a random access file must be the same length, therefore strings must always be explicitly given a length as seen in the line of code below:

Player AS STRING * 30 '   30 bytes: the player's name

Each record in this program will be 34 bytes in length, 30 bytes for the string, plus another 4 bytes for the LONG integer. The variables types chart in the QB64pe Wiki lists each data type and how many bytes are needed to store it. You can either add all of these byte counts up manually or simply use the LEN statement to get the length of each record as seen in line 25 of the code:

RecordLength = LEN(Record) '                                       get the length of each record in the database

The record length must be identified to allow QB64 the ability to access any record location within the file. The records are stored sequentially within the file as shown in Figure 11 below:

Figure 11: The anatomy of a records database

Line 44 uses the LOF statement to get the current size of the open file:

FileSize = LOF(FileNumber) '                                       get total size of file

Line 46 of the code uses the math function in Figure 11 above to determine the number of records that are contained within the file after the data has been written to the file:

PRINT FileSize \ RecordLength; "records created" '                 report number of records created

The PUT statement is used to place or update a record within the file.

File Access: The PUT Statement

The PUT statement is used to write data to a specific record location within a RANDOM mode file as seen in line 41 of the code above:

PUT FileNumber, RecordNumber, Record '                      write record to database

The PUT statement requires the open file handle, FileNumber, the record number to write, RecordNumber, and the record data itself, Record. As seen in Figure 11 above the record number is used by QB64 to determine the location within the file to write the record data. Once the location has been calculated the data is written, or overwritten, to the file's location.

Accessing a Random Access File (Database)

Now that we have a random access file created, HISCORES.DAT, it's time to write some code that accesses the data contained within the file. Load or type in the following program and then execute it to read the data from the file created.

( This code can be found at .\tutorial\Lesson11\ReadDatabase.bas )

'
' Demo: Read database of high scores
'     : ReadDatabase.bas
'     : QB64 tutorial - Lesson 11
'
'+------------------------------------------------+
'| Each record will use 34 bytes of storage space |
'+------------------------------------------------+

TYPE HISCORE '                High score record structure
    Player AS STRING * 30 '   30 bytes: the player's name
    HiScore AS LONG '         4 bytes : the player's high score
END TYPE

DIM Record AS HISCORE '       an individual database record
DIM RecordLength AS INTEGER ' length of an individual record (in bytes)
DIM FileNumber AS LONG '      the file handle number
DIM RecordNumber AS INTEGER ' record number counter
DIM FileSize AS INTEGER '     total size of file
DIM TotalRecords AS INTEGER ' number of records in database

'+--------------------+
'| Set initial values |
'+--------------------+

RecordLength = LEN(Record) '                                         get the length of each record
FileNumber = FREEFILE '                                              get a free file handle
IF _FILEEXISTS("HISCORES.DAT") THEN '                                does the database file exist?
   OPEN "HISCORES.DAT" FOR RANDOM AS FileNumber LEN = RecordLength ' yes, open database for reading/writing
ELSE '                                                               no
   PRINT '                                                           inform user of the error
   PRINT " HISCORES.DAT file not found." '                           and how to correct
   PRINT
   PRINT " Run "; CHR$(34); "MakeDatabase.bas"; CHR$(34); " to create the database file."
   END
END IF
FileSize = LOF(FileNumber) '                                         get total size of file
TotalRecords = FileSize \ RecordLength '                             calculate number of records in database
RecordNumber = 0 '                                                   reset record number

'+---------------------+
'| Begin database read |
'+---------------------+

PRINT '                                                              print table header
PRINT " +-----------------------------------------+"
PRINT " |            HIGH SCORE TABLE             |"
PRINT " +--------+--------------------------------+"
PRINT " | SCORE  |             PLAYER             |"
PRINT " +--------+--------------------------------+"
DO '                                                                 begin read loop
    RecordNumber = RecordNumber + 1 '                                increment record number counter
   GET FileNumber, RecordNumber, Record '                            get record from database
   PRINT " |"; USING " ##,###"; Record.HiScore; '                    print high score
   PRINT " | "; Record.Player; " |" '                                print player
LOOP UNTIL RecordNumber = TotalRecords '                             leave when all records read
CLOSE FileNumber '                                                   close database file
PRINT " +-----------------------------------------+" '               end table with footer
SLEEP '                                                              wait for keystroke
SYSTEM '                                                             return to operating system

Figure 12: The result of reading in the records

File Access: The GET Statement

The GET statement is used to read data at a specific record location within a RANDOM mode file as seen in line 53 of the code above:

GET FileNumber, RecordNumber, Record '                            get record from database

The GET statement requires the open file handle, FileNumber, the record number to access, RecordNumber, and the record data to retrieve, Record. As seen in Figure 11 above the record number is used by QB64 to determine the location within the file to read the record data. Once the location has been calculated the data is read from the file's location.

A Random Access Database Example

 Included in the .\tutorial\Lesson11 directory is a program named "Groceries.bas" that gives an example of how a database application can be written using QB64 and Random files. The program maintains a database of grocery items keyed by category and allows the user to create a grocery shopping list from the items within the database.

The user can manipulate the database by adding. editing, and deleting the items and categories within the database. The program has been written in "old school" style ( SCREEN 0 ) as seen in Figure 13 below.

Figure 13: Time to go grocery shopping

The program manages two database files, "CATEGORY.DAT" and "GROCERY.DAT", using the first one as a key for the second one. These two files are also included in the .\tutorial\Lesson11 directory and contains over 100 items to play around with. If the program does not find these two database files it will create new empty database files for you to add items and categories to. At the end of the source code are instructions on how to use the database program. It's fairly self-explanatory though, as options appear and disappear depending on the list and task you are currently performing. Figure 14 below shows an example of a grocery list created with the program.

Figure 14: Shopping list in hand

Your Turn

You were recently hired by the FBI as a computer programmer attached to the code cypher group. A code was recently intercepted on the Internet that the team believes they have decrypted but need you to write code to verify that their analysis is correct. They have supplied you with the data in a file named "POINTDATA.XYP" and have concluded that it contains polygon coordinates that spells out a hidden message. Your job is to write a program that takes this data and displays the hidden message to the screen. Here is the information the cypher team has given you.

The coded file "POINTDATA.XYP" is in the ".\tutorial\Lesson11" directory. You may want to open it in Notepad to view the file's structure while reading the next statements.

Within the file the team believes:

Given the above information the team has asked that you draw the polygon lines in white and to use white as the border color when painting the polygons in. Hurry! Time is critical! This message must get to the White House as soon as possible for critical analysis.

The team has asked that you save this critical program as MyQB64.BAS when finished. Don't fail in your task! The country is counting on you.

Click here to see the solution. (Don't cheat! Remember, your country needs you!)

Commands and Concepts Learned