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.
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:
A single value greater than 0 contained on a line is a command to draw a polygon.
That single value contains the number of x,y coordinate pairs contained in the polygon.
The line following this single value contains the x,y coordinate pairs of the polygon.
A single value equaling -1 contained on a line is a command to paint the previous polygon.
The line following this -1 value contains the x,y coordinate pair where the paint color resides and the red, green, and blue values of the paint color.
A single value equaling 0 contained on a line means the end of the data file has been reached.
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
New commands introduced in this lesson:
_DIREXISTS
_FILEEXISTS
CHDIR
MKDIR
RMDIR
KILL
NAME...AS
OPEN
INPUT (file statement)
OUTPUT
APPEND
BINARY
RANDOM
PRINT (file statement)
WRITE (file statement)
LINE INPUT (file statement)
FREEFILE
LOF
EOF
GET #
PUT #
New concepts introduced in this lesson:
sequential file
Comma Separated Values file (CSV)
records
database