Task 14  Task 14: File Input and Output  Task 14


Files are an important part of any game, whether to hold high scores, player information and stats or configuration settings the player has set. Let's start off with the file manipulation commands and move into creating our own files from there. This task is going to assume that you have a basic understanding of drive directory and file structure. If you do not know what "root", "directory", "sub-directory", "tree" or "path" mean in terms of drive structure you'll need to go here to get that basic understanding or as a refresher before continuing on.

- Testing For Existance -

Before you can use a file you may first want to verify that the directory (called a folder in Windows) that contains the file exists. QB64 offers a new function to BASIC programmers called _DIREXISTS. If _DIREXISTS finds the specified directory a value of -1 (TRUE) will be returned, otherwise a value of 0 (FALSE) signifies that the directory does not exist.

IF _DIREXISTS("C:\QB64\MYGAME\") THEN '              does the subdirectory exist?
    CHDIR "C:\QB64\MYGAME\" '                        yes, go into subdirectory
ELSE '                                               no
    PRINT "The specified folder does not exist!" '   inform user of this
END IF


QB64 offers another function to test for the existance of a file called _FILEEXISTS. If _FILEEXISTS finds the specified file a value of -1 (TRUE) will be returned.

IF _DIREXISTS("C:\QB64\MYGAME\") THEN '              does the subdirectory exist?
    CHDIR "C:\QB64\MYGAME\" '                        yes, go into subdirectory
    IF _FILEEXISTS("MYFILE.TXT") THEN '              does the file exist?
        OPEN "MYFILE.TXT" FOR INPUT AS #1 '          open the file for reading
    ELSE '                                           no
        PRINT "The specified file does not exist!" ' inform user of this
    END IF
ELSE '                                               no
    PRINT "The specified folder does not exist!" '   inform user of this
END IF


It is not mandatory to check for the existance of folders or files before working with them, but it's a good idea to do so just in case the file or directory were somehow deleted or modified. Attempting to open a file, or change to a directory, that does not exist will cause a run-time error crashing your program's execution.

- File & Directory Manipulation -

Navigating around a drive and creating and deleting files and directories is a common practice in all programming. The drive is where you'll store program information to be retrieved the next time it is executed. QB64 offers a rich set of commands to the programmer for working with drives and files.

- CHDIR -

The CHDIR statement is used to navigate around a drive's directory structure, called the tree. To move to the root, or the top of the drive's tree structure you can issue the statement:

CHDIR "C:\"

Simply replace the C: portion of the statement above with the letter of the drive you wish to work with. You can also move to any other directory within the drive by assigning the entire path to the command:

CHDIR "C:\WINDOWS\SYSTEM32\"

Be aware however that if the directory you are moving to does not exist your program will generate an error. It's good practice to always use _DIREXISTS just to be sure.

- MKDIR -

To ceate a new directory or subdirectory (a directory within a directory) you can use the command MKDIR.

MKDIR "MYFOLDER"

The above statement will create a new directory called MYFOLDER in the current directory or subdirectory. You can also explicitely name a path before the folder name which allows you to make directories anywhere within the drive's structure:

MKDIR "C:\QB64\MYFOLDER\NEWGAME"

This will create a directory called NEWGAME in the C:\QB64\MYFOLDER subdirectory no matter where you currently happen to be within the drive's directory structure.


- RMDIR -

The RMDIR statement allows the programmer to remove, or delete, a directory or subdirectory. However, before a directory can be deleted it must be empty, containing no files or subdirectories within it. If the directory you are attempting to remove is not empty then your program will generate an error.

RMDIR "C:\QB64\MYFOLDER\NEWGAME"

The above statement will delete the NEWGAME directory, as long as no files or subdirectories reside within it.


- KILL -

The KILL statement is used to delete an individual file(s) from a drive. However, there are a few things you should know before deleting files.

- You must remember that files deleted with KILL will NOT be moved to the Windows recycle bin.
- Open files (files in use) can't be deleted. The program using them must close them first.
- Files marked as read-only can't be deleted and therefore must have the read-only attribute removed first.
- KILL can't be used to delete directories. Use RMDIR for this task.
- BE CAREFUL when using wildcards (* and ?) with this command! See first warning listed!

KILL "HISCORES.TXT"

The above statement will delete the file named HISCORES.TXT from existance. If the file does not exist however your program will generate an error, so it's always good practice to test for the existance of a file first using _FILEEXISTS first, just to be sure.

KILL "C:\QB64\GAMES\ASTEROIDS\HISCORE.TXT"

Once again, a file name HISCORE.TXT is being deleted with the above example at the specific path supplied, C:\QB64\GAMES\ASTEROIDS\.


- NAME ... AS -

The NAME ... AS statement is used to rename either a file name or a directory name. NAME ... AS also has a curious feature that allows you to move an entire directory structure from one part of a drive to another or to an entirely different drive all together.

NAME "HISCORE.TXT" AS "HISCORE.OLD"

The above statement is renaming a file called HISCORE.TXT to HISCORE.OLD.

NAME "C:\QB64\GAMES\MYGAME\" AS "C:\GAMES\NEWGAME\"

This statement however is performing two specific tasks. First, the directory MYGAME has been renamed to NEWGAME. Secondly, since the paths are different then entire subdirectory MYGAME and all of the subdirectories and files contained within it will be moved to C:\GAMES\ while being renamed to NEWGAME.

- File Creation & Access -

This task is going to focus on sequential files, or files that must be read from or written to one line at a time in sequential order from the top to the bottom. This type of file is by far the easiest to create and read from, but it's up to the programmer to keep track of which lines contain the data needed for the program. An example of a simple sequential file would be a high score table for a game. For instance, a file named HISCORES.TXT may contain the following lines of text which contains the top three high scores:

Fred Haise
10000
Jim Lovell
9000
John Swigert
8000

The programmer would need to read each line in sequentially from the top to bottom keeping track of each line's content and what it means until the end of the file is reached. For small files this sequential method of file access can be quick, easy and efficient. However, for large sets of data, a town's phonebook for instance, sequential file access would be entirely too slow to achieve acceptable results.

- Working with Sequential Files -

Before we can work with a sequential file it has to be created first. The OPEN statement is used to open files in a predetermined mode of operation.

OPEN Filename$ FOR mode AS #Filenumber%

Filename$ is the name of the file on the disk you wish to open. It can be a literal string contained in quotes, such as "HISCORES.TXT", or a string variable that contains the name of the file.

Mode denotes how the file is to opened, for reading, for writing, or both. Mode can be any of the following:

INPUT  - the ability to read contents from the file only (sequential).
OUTPUT - the ability to write contents to the file only (sequential).
APPEND - the ability to write contents to the end of an exisiting file (sequential).
BINARY - the ability to read and write from a file at any position (records database).
RANDOM - the ability to read and write records from a file at any position (records database).

Filenumber% is used by QB64 to keep track of files that are open through a file handle numbering system. The total number of files allowed to be open at the same time in QuickBasic 4.5 was 255 (1 - 255) but QB64 has raised this limit to a long integer, which means over 2 billion files could theoretically be opened at a time.

Let's go ahead and create a high score table by opening a sequential file for output, writing the values to the file and then closing the file.


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


The snippet of code above creates a new file called HISCORES.TXT in the current working directory. Be aware that if HISCORES.TXT already exists it will be wiped out and written over with the new contents. By default, the current working directory for programs compiled and executed from within the IDE is the folder that QB64 is installed in. For compiled .EXEs that you distribute the current working directory is the folder where the .EXE was executed from.

The OPEN statement opened the file in OUTPUT mode and gave the file the handle number of #1. Any statement that follows in the code that wishes to interact with this open file must use the file hande number to do so. As you can see, the PRINT statement was used to write the data to the file because it was told to use the file handle, #1, as the destination to write to. You must close an open file when you are finished with it before terminating a program's execution as to avoid the possibility of file corruption. Here, the CLOSE statement closes the file indentifying it by handle number, CLOSE #1.

Now that we have a sequential file created it's time to use that file and gather the data it contains.


DIM HSname$(3) '  high score player names
DIM HScore%(3) '  high scores
DIM ScoreCount% ' index counter
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
    ScoreCount% = 0 '                               initialize index counter
    WHILE NOT EOF(1) '                              at end of file?
        ScoreCount% = ScoreCount% + 1 '             no, increment index counter
        INPUT #1, HSname$(ScoreCount%) '            get name from file  
        INPUT #1, HScore%(ScoreCount%) '            get score from file
    WEND '                                          loop back to WHILE
    CLOSE #1 '                                      close the file
    PRINT "-- High Score Table --" '                print high scores to screen
    PRINT "----------------------"
    FOR Count% = 1 TO ScoreCount%
        PRINT HSname$(Count%);" -";HScore%(Count%)
    NEXT Count%
ELSE '                                              no, the file does not exist
    PRINT "High score file not found!" '            inform player of the error
END IF

This snippet of code opens the high score file, gathers the contents of it, and then prints the results to the screen for viewing. Two arrays are created to hold the high score names and point values, HSname$() and HScore%(). ScoreCount% is used to increment the index value by one each time a name and score need to be read in. This code here is where the names and scores get read in:

    WHILE NOT EOF(1) '                              at end of file?
        ScoreCount% = ScoreCount% + 1 '             no, increment index counter
        INPUT #1, HSname$(ScoreCount%) '            get name from file  
        INPUT #1, HScore%(ScoreCount%) '            get score from file
    WEND '                                          loop back to WHILE


The EOF() statement, End Of File, checks for the end of a sequential file by referencing the file's handle number, in this case #1 or EOF(1) (the # is not used inside of the EOF() statement, just the file handle number). Remember that sequential files need to have their data read in one line at a time from top to bottom. EOF() detects when the last line has been read in and returns a value of -1 (TRUE) when this has happened indicating there is no more data to be read in. Basically, the WHILE ... WEND loop will continue while EOF() does not detect the end of the file. Inside the loop a counter is incremented to be used as an index number for the arrays to hold the information. The INPUT statement is used to grab a line of information from the file and once again, it's up to the programmer to know what type of data was saved on each line. INPUT is told to gather information from an open file through the use of the file handle #1. Once EOF() reads TRUE (-1) the loop ends and the file is then closed. Finally, the contents of the file, now saved in the arrays, is printed to the screen for the player to see.

The above code works only because we already know there are only three high score name and score pairs stored in the high score file. If there would have been a fourth pair the code would have crashed with a "Subscript Out Of Range" error due to the fact that we only allotted for 3 elements to be stored in each array. The following code is a better way to handle bringing in information from a sequential file:


TYPE HIGH
    Pname AS STRING * 20 ' player name
    Pscore AS INTEGER '    player score
END TYPE

REDIM Scores(0) AS HIGH ' 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 "-- High Score Table --" '                          print high scores to screen
    PRINT "----------------------"
    WHILE NOT EOF(1) '                                        at end of file?
        REDIM _PRESERVE Scores(UBOUND(Scores) + 1) AS HIGH '  increase dynamic array index by one
        INPUT #1, Scores(UBOUND(Scores)).Pname '              get name from file
        INPUT #1, Scores(UBOUND(Scores)).Pscore '             get score from file
        PRINT Scores(UBOUND(Scores)).Pname; " -"; '           display player name
        PRINT Scores(UBOUND(Scores)).Pscore '                 display player score
    WEND '                                                    loop back to WHILE
    CLOSE #1 '                                                close the file
    PRINT UBOUND(Scores); " 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

In this example a TYPE is created to hold the information being brought in from the file. Pname is declared as a string to hold the player's name and Pscore is declared as an integer to hold the player's score. Next, an array called Scores() is created by using the REDIM statement. REDIM creates what is known as a dynamic array, or an array that can have its index size changed by the programmer through code. All of the arrays created in Task 11 were static arrays, meaning that there size is fixed by the use of the DIM statement. Once again, the data is gathered from the file through the use of a WHILE ... WEND loop that keeps looping until the End Of File, or EOF(), is encountered:

    WHILE NOT EOF(1) '                                        at end of file?
        REDIM _PRESERVE Scores(UBOUND(Scores) + 1) AS HIGH '  increase dynamic array index by one
        INPUT #1, Scores(UBOUND(Scores)).Pname '              get name from file
        INPUT #1, Scores(UBOUND(Scores)).Pscore '             get score from file
        PRINT Scores(UBOUND(Scores)).Pname; " -"; '           display player name
        PRINT Scores(UBOUND(Scores)).Pscore '                 display player score
    WEND '                                                    loop back to WHILE


When the Scores() dynamic array was created it was done so with an index value of zero:

REDIM Scores(0) AS HIGH ' dynamic array to hold high scores

This means that the array, as created, can only hold one element of information. Since our sequential file obviously has more than one name and score pair to bring in there needs to be a way to increase the index size of the array to hold the flow of information coming in. This line of code does just that:

        REDIM _PRESERVE Scores(UBOUND(Scores) + 1) AS HIGH '  increase dynamic array index by one

The REDIM statement can be used again on the array to increase its index size. When REDIM is used with the _PRESERVE statement the original contents of the array are saved while the array size is changed. If you do not use the _PRESERVE statement the contents of the array will get erased when REDIM is used. The UBOUND() statement returns the upper boundary value, or the highest index value, currently contained in the array. The first time the WHILE ... WEND loop is encountered the index value of Scores() is zero, so UBOUND() will return a value of zero. Adding 1 to the UBOUND() value, inside the REDIM statement, effectively increases the index value of the dynamic array Scores() by 1:

UBOUND(Scores) + 1

Notice that UBOUND() only needs the name of the array contained inside of it. The use of parenthesis and the TYPE identifier are not needed. UBOUND() is once again used one the two INPUT lines to indicate which array index the data gathered from the file should be stored in. UBOUND() will now return a value of 1 and the gathered data is stored into Scores(1).Pname and Scores(1).Pscore respectively. The next time the loop runs REDIM and UBOUND() once again increase the index size of the dynamic array and the next name and score pair get stored into Scores(2).Pname and Scores(2).Pscore, and so on until the file's EOF() is encountered.

There is an even more efficient way to create and read data from a sequential file using the WRITE statement instead of the PRINT statement. The WRITE statement will allow the programmer to create a CSV , or Comma Separated Value, file that can be read in more efficiently. Let's make a new HISCORES.TXT file using this method:


DIM Ok$

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 entires
    WRITE #1, "Jim Lovell", 9000
    WRITE #1, "John Swigert", 8000
    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

Using PRINT we created a sequential file that contained the following:

Fred Haise
10000
Jim Lovell
9000
John Swigert
8000


However, with the code above using WRITE instead of PRINT, the file now contains this:

"Fred Haise",10000
"Jim Lovell",9000
"John Swigert",8000


In a CSV file strings are surrounded by quotes to denote that they are in fact string values and numeric values are written as is. Most spreadsheet programs can read and write CSV files as well, allowing the programmer a way to create very large CSV files in both QB64 and a spreadsheet program. This can be a very handy feature for a programmer. Years ago I wrote a small QuickBasic program for a local company to monitor temperature and humidity in a room that needed both controlled. The program would take a sample from a temperature and humidity sensor connected to the computer's serial port every minute. The readings were then appended to a CSV file and every hour my program would execute a spreadsheet program to read the CSV file and create a graph that would be saved as a bitmap image to the hard drive and then displayed on screen. This allowed the engineers to get a minute by minute look at the conditions in the room each hour and make adjustments to the room as needed.

Let's rewrite our code once again to use the CSV file we created:


TYPE HIGH
    Pname AS STRING * 20 ' player name
    Pscore AS INTEGER '    player score
END TYPE

REDIM Scores(0) AS HIGH ' 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 "-- High Score Table --" '                          print high scores to screen
    PRINT "----------------------"
    WHILE NOT EOF(1) '                                        at end of file?
        REDIM _PRESERVE Scores(UBOUND(Scores) + 1) AS HIGH '  increase dynamic array index by one
        INPUT #1, Scores(UBOUND(Scores)).Pname, Scores(UBOUND(Scores)).Pscore ' get data from file
        PRINT Scores(UBOUND(Scores)).Pname; " -"; '           display player name
        PRINT Scores(UBOUND(Scores)).Pscore '                 display player score
    WEND '                                                    loop back to WHILE
    CLOSE #1 '                                                close the file
    PRINT UBOUND(Scores); " 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

The only modification needed to our program was changing this:

        INPUT #1, Scores(UBOUND(Scores)).Pname '    get name from file
        INPUT #1, Scores(UBOUND(Scores)).Pscore '   get score from file


to this:

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

Separating variables on the INPUT line with the use of commas allows the entire CSV line of data to be brought in at once. INPUT automatically removes the quotes surrounding strings before they are placed in string variables. Once again however, it's up to the programmer to keep track of how the data was written in the first place so it is brought back in the proper order and placed in the correct variable types.

LINE INPUT can also be used to read data from sequential files, but LINE INPUT will grab an entire line exactly how it appears in a file. Furthermore, the entire line can only be brought in as a string since LINE INPUT only supports string variable types. Try this little snippet of code to see this in action:

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

As you can see each line in the file was printed to the screen exactly as it appears in the file. The programmer would have to perform extra functions to parse the data out to its separate values.

Another important feature of sequential files is that they can be appended, or data added to the end of them using the OPEN statement. Let's add a few more high scores to our HISCORES.TXT file using this method:

OPEN "HISCORES.TXT" FOR APPEND AS #1
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

Our HISCORES.TXT file now contains this:

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


The original three lines of data were retained and the new data was added to the end of the file using the OPEN statement's APPEND mode. If the file you are trying to append to does no exist it will simply be created the same as if you were using the OUTPUT mode of operation.

As was stated before, the major drawback to sequential files is that they must be read from the top down in sequential order. This makes it impossible to insert data in the middle of the file. It can be done, but requires a lot of extra work for the programmer. For instance, let's say our player just achieved a high score and we want to add his/her name to the high score file. However, the score our player achieved was 5500 and this score obviously sits between Buzz Aldrin's and Gus Grissom's score. How would we go about inserting the high score there? One method of performing this task is shown in the code below:


TYPE HIGH
    Pname AS STRING * 20 ' player name
    Pscore AS INTEGER '    player score
END TYPE

REDIM Scores(0) AS HIGH ' dynamic array to hold high scores
DIM PlayerName$ '         new player's name
DIM PlayerScore% '        new player's high score
DIM Count% '              generic counter
DIM Written% '            true when new score saved to file

PlayerName$ = "Edgar Mitchell" '                                                new player's name
PlayerScore% = 5500 '                                                           new player's high score
IF _FILEEXISTS("HISCORES.TXT") THEN '                                           does high score file exist?
    OPEN "HISCORES.TXT" FOR INPUT AS #1 '                                       yes, open sequential file
    WHILE NOT EOF(1) '                                                          at end of file?
        REDIM _PRESERVE Scores(UBOUND(Scores) + 1) AS HIGH '                    increase dynamic array index by one
        INPUT #1, Scores(UBOUND(Scores)).Pname, Scores(UBOUND(Scores)).Pscore ' get data from file
    WEND '                                                                      loop back to WHILE
    CLOSE #1 '                                                                  close the file
    OPEN "HISCORES.TXT" FOR OUTPUT AS #1 '                                      open file to overwrite
    FOR Count% = 1 TO UBOUND(Scores) '                                          cycle through the array
        IF NOT Written% THEN '                                                  has player's score been saved?
            IF PlayerScore% >= Scores(Count%).Pscore THEN '                     no, is player's score higher?
                WRITE #1, PlayerName$, PlayerScore% '                           yes, insert player's info to file
                Written% = -1 '                                                 remember that score was saved
            END IF
        END IF
        WRITE #1, RTRIM$(Scores(Count%).Pname), Scores(Count%).Pscore '         save data back to file
    NEXT Count%
    CLOSE #1 '                                                                  close the file
ELSE '                                                                          no, the file does not exist
    OPEN "HISCORES.TXT" FOR OUTPUT AS #1 '                                      open file for first entry
    WRITE #1, PlayerName$, PlayerScore% '                                       write high score info
    CLOSE #1 '                                                                  close the file
END IF

After running the code our HISCORES.TXT file now contains:

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

The code above retrieves the high score names and scores from the HISCORE.TXT file and places them into the Scores() array, essentially placing the contents of the file into memory. Next, the HISCORE.TXT file is once again opened but this time in an OUTPUT mode to be overwritten. The array contents are then cycled through one at a time and the current array score is compared to the new player's high score. If the new player's high score is greater than or equal to the saved score the new player's name and score are written to the file. A flag called Written% is set to let us know the new player's score has been saved so it won't be saved again. The program also takes note if the HISCORE.TXT file is not present, meaning that this player's score must be the first entry and simply creates the high score file and saves the player's name and score.

Another method commonly used by programmers is to open the original file in an INPUT mode and then a second temporary file in an OUTPUT mode. This is especially useful for very large sequential files that would be impracticle to load their entire contents into memory.


DIM Pname$ '       player name from high score file
DIM Pscore% '      player score from high score file
DIM PlayerName$ '  new player's name
DIM PlayerScore% ' new player's high score
DIM Written% '     true when new score saved to file

PlayerName$ = "Edgar Mitchell" '                      new player's name
PlayerScore% = 5500 '                                 new player's high score
IF _FILEEXISTS("HISCORES.TXT") THEN '                 does high score file exist?
    OPEN "HISCORES.TXT" FOR INPUT AS #1 '             yes, open sequential file
    OPEN "HISCORES.TMP" FOR OUTPUT AS #2 '            open a temporary file
    WHILE NOT EOF(1) '                                at end of file?
        INPUT #1, Pname$, Pscore% '                   no, get data from file
        IF NOT Written% THEN '                        has player's score been saved?
            IF PlayerScore% >= Pscore% THEN '         no, is player's score higher?
                WRITE #2, PlayerName$, PlayerScore% ' yes, insert player's info to file
                Written% = -1 '                       remember that score was saved
            END IF
        END IF
        WRITE #2, Pname$, Pscore% '                   save data back to temp file
    WEND '                                            loop back to WHILE
    CLOSE #1, #2 '                                    close the files
    KILL "HISCORES.TXT" '                             delete the old high score file
    NAME "HISCORES.TMP" AS "HISCORES.TXT" '           rename temp file to new high score file
ELSE '                                                no, the file does not exist
    OPEN "HISCORES.TXT" FOR OUTPUT AS #1 '            open file for first entry
    WRITE #1, PlayerName$, PlayerScore% '             write high score info
    CLOSE #1 '                                        close the file
END IF

With this program the same results are achieved without loading the contents of the file into memory. Instead, a new file opened for OUTPUT called HISCORES.TMP is used to copy the lines of data back to that were originally contained in HISCORES.TXT. The same method is used to insert the new player's name and high score but this time inserted into the temporary file. Once all the data from HISCORES.TXT has been read in and copied to the temporary file the original HISCORES.TXT is deleted. The new temporary file is then renamed to HISCORES.TXT in effect creating our new high score file. As you can imagine, the larger sequential files get the more ineficient they become no matter which method is used. For very large sets of data it's always advantageous to use a database because of their speed and efficiency.

- Working With Random Access and Binary Files -

There are methods that QB64 offers to set up file access that uses records to store data that can be accessed quickly through the use of record numbers. 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 and BINARY modes of file access allow the programmer to store information in a manner that makes retrieval fast no matter where the information is located within the file. However, this method of file access is beyond the scope of these lessons because of their complexity, therefore sequential files will serve our purpose for the most part. If you are interested in persuing database file creation using QB64 then the QB64 Wiki and the following statements and commands are where you need to start.

OPEN Filename$ FOR RANDOM AS #Filenumber%
OPEN Filename$ FOR BINARY AS #Filenumber%
TYPE ... END TYPE
FIELD
LEN
GET
PUT
SEEK()
SEEK(statement)
LOF()

Another possibility for database creation and access in QB64 is to use Steve McNeill's (aka SMcNeill) excellent QBDbase library that puts the power of the Dbase database functions at the fingertips of QB64 programmers. The download and tutorials can be found in this post on the QB64 website and includes the entire source code to learn from as well! Libraries and their use will be discussed in a later task.


- A Few More Useful Functions -

There are a few more useful statements that QB64 offers that are very helpful when dealing with files. The following example program highlights many of these:

'*
'* Text File Inspector V1.0
'*
'* Searches a user provided drive\path for .TXT files and then searches their contents
'* for a user provided search string creating a listing of .TXT files the search
'* string was found in. The results (if any) are then opened in Notepad for viewing.
'*

'--------------------------------
'- Variable Declaration Section -
'--------------------------------

DIM TxtPath$ '       path used to search for text files
DIM DefPath$ '       default path if none provided
DIM FileName$ '      name of each text file to open
DIM CurrentLine% '   current cursor line
DIM CurrentColumn% ' current cursor column
DIM FileLength& '    length of each text file
DIM FileCount& '     total text files found
DIM SearchCount& '   total text files found with matching search term
DIM Count& '         generic counter
DIM Dummy$ '         generic string

'----------------------------
'- Main Program Begins Here -
'----------------------------

DefPath$ = "C:\WINDOWS\" '                                               default path if none provided
IF NOT _DIREXISTS(DefPath$) THEN DefPath$ = "" '                         some computers don't have C: drives
PRINT '                                                                  display program title
PRINT " Text File Inspector"
PRINT " -------------------"
PRINT
PRINT " Enter directory of text files to inspect." '                     display instructions to user
PRINT " All subdirecotries will be included too."
IF DefPath$ <> "" THEN '                                                 is there a default path?
    PRINT " (press ENTER to use C:\WINDOWS as default)" '                yes, display as an option to user
END IF
PRINT " Type in QUIT to leave program."
CurrentLine% = CSRLIN '                                                  save current cursor line
CurrentColumn% = POS(0) '                                                save current cursor column
DO '                                                                     LOOP check for valid files
    DO '                                                                 LOOP check for valid path
        LOCATE CurrentLine%, CurrentColumn% '                            position cursor
        PRINT STRING$(79, 32); '                                         remove anything on this line
        LOCATE CurrentLine%, CurrentColumn% '                            reposition cursor
        LINE INPUT " "; TxtPath$ '                                       get path from user
        IF UCASE$(TxtPath$) = "QUIT" THEN SYSTEM '                       return to Windows if user quits
        IF TxtPath$ = "" AND DefPath$ <> "" THEN '                       user enter nothing and default available?
            TxtPath$ = DefPath$ '                                        yes, user default path
        END IF
        IF NOT _DIREXISTS(TxtPath$) THEN '                               does the path supplied exist?
            LOCATE CurrentLine% + 2, CurrentColumn% '                    yes, position cursor
            PRINT STRING$(79, 32); '                                     remove anything on this line
            LOCATE CurrentLine% + 2, CurrentColumn% '                    reposition cursor
            PRINT " Error: The path "; CHR$(34); TxtPath$; '             inform user of error
            PRINT CHR$(34); " supplied does not exist! Try again."
            TxtPath$ = "" '                                              clear path string
        END IF
    LOOP UNTIL TxtPath$ <> "" '                                          LOOP back if no path entered
    IF RIGHT$(TxtPath$, 1) <> "\" THEN '                                 does path contain backslash at end?
        TxtPath$ = TxtPath$ + "\" '                                      no, add a backslash
    END IF
    FOR Count& = 1 TO 3 '                                                cycle three times
        PRINT STRING$(79, 32) '                                          remove any errors from screen
    NEXT Count&
    LOCATE CurrentLine% + 2, CurrentColumn% '                            position cursor
    PRINT " Searching for text files ... one moment please." '           inform user of what is happening
    SHELL _HIDE "DIR " + TxtPath$ + "*.txt /s /b > \alltxt.log" '        issue DIR command to Windows
    OPEN "\alltxt.log" FOR INPUT AS #1 '                                 open file created by DIR command
    FileLength& = LOF(1) '                                               get length of file
    IF FileLength& = 0 THEN '                                            does it contain data?
        CLOSE #1 '                                                       no, close the file
        LOCATE CurrentLine% + 2, CurrentColumn% '                        position cursor
        PRINT " Error: The supplied path does not contain text files." ' inform user no text file found
    END IF
LOOP UNTIL FileLength& > 0 '                                             LOOP back if no text files found
WHILE NOT EOF(1) '                                                       LOOP while not at end of file
    LINE INPUT #1, Dummy$ '                                              get a path\filename line from file
    FileCount& = FileCount& + 1 '                                        increment file counter
WEND '                                                                   LOOP back until end of file
CLOSE #1 '                                                               close the file
PRINT FileCount&; "text files found." '                                  inform user how many text files found
PRINT '                                                                  display instructions to user
PRINT " Enter word or phrase to search for."
PRINT " Type in QUIT to leave program."
CurrentLine% = CSRLIN '                                                  get current cursor line
CurrentColumn% = POS(0) '                                                get current cursor column
DO '                                                                     LOOP check for valid search string
    LOCATE CurrentLine%, CurrentColumn% '                                position cursor
    PRINT STRING$(79, 32); '                                             remove anything on this line
    LOCATE CurrentLine%, CurrentColumn% '                                reposition cursor
    LINE INPUT " "; Search$ '                                            get search string from user
    IF UCASE$(Search$) = "QUIT" THEN SYSTEM '                            return to Windows if user quits
LOOP UNTIL Search$ <> "" '                                               LOOP back until valid search string given
PRINT
OPEN "\alltxt.log" FOR INPUT AS #1 '                                     open file containing list of text files
OPEN "\alltxtresults.log" FOR OUTPUT AS #2 '                             open file to contain text files with search string
FOR Count& = 1 TO FileCount& '                                           cycle through all the path\filenames
    LOCATE CurrentLine% + 2, CurrentColumn% '                            position the cursor
    PRINT " Processing file"; Count&; "of"; FileCount& '                 inform user which file is being worked on
    LINE INPUT #1, FileName$ '                                           get path\filename from text file
    IF INSPECT%(FileName$, Search$) THEN '                               was search string found in file?
        SearchCount& = SearchCount& + 1 '                                yes, increment search counter
        PRINT #2, FileName$ '                                            save path\filename in results file
        LOCATE CurrentLine% + 3, CurrentColumn% '                        position cursor
        PRINT SearchCount&; "text files contain search phrase so far." ' update user with number of matches found
    END IF
NEXT Count&
IF LOF(2) = 0 THEN '                                                     were any files written to results?
    LOCATE CurrentLine% + 3, CurrentColumn% '                            no, position cursor
    PRINT " Your search yielded no results." '                           inform user of outcome
    CLOSE #2, #1 '                                                       close the files
ELSE '                                                                   yes, results file contains data
    CLOSE #2, #1 '                                                       close the files
    LOCATE CurrentLine% + 4, CurrentColumn% '                            position cursor
    PRINT " Opening results in Notepad.exe ... " '                       inform user of next step
    SLEEP 3 '                                                            wait 3 seconds or for a keystroke
    SHELL _HIDE "cmd /c notepad \alltxtresults.log" '                    tell Windows to open file with Notepad
END IF
KILL "\alltxt.log" '                                                     delete file containing list (cleanup)
KILL "\alltxtresults.log" '                                              delete results file (cleanup)
SYSTEM '                                                                 return to Windows

'-----------------------------------
'- Function and Subroutine section -
'-----------------------------------

FUNCTION INSPECT% (FileName$, Search$)

'*
'* Opens a file as one big record and then searches the record for a string
'* that matches Search$. If a match is found a value of -1 (true) is passed
'* back to calling routine.
'*

DIM SearchFile% ' free file handle
DIM Contents$ '   entire contents of file

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
    END IF
END IF

END FUNCTION

- FREEFILE -

The FREEFILE statement returns an unused file handle number to be used when opening files. The INSPECT%() function contained in the program listing above uses FREEFILE to obtain an unused file handle number before opening text files for searching:

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
    END IF
END IF


It's always good programming practice to use FREEFILE inside of called functions or subroutines to get a file handle. If many subroutines or functions are called upon that each open files of their own it can quickly become a nightmare to track these file handle numbers individually by the programmer. FREEFILE ensures that a file handle number will be returned that is not currently in use by another file statement elsewhere in the program.

- LOF() -

LOF() returns the size in bytes of a file's contents. If LOF() returns the value of zero this indicates that the file contains no data and is useful for determining if sequential files contain useful data. The LOF() function can also be used to determine how many records a BINARY or RANDOM access file contains by dividing the value returned by LOF() with a single record size. The program listing above uses LOF() to determine if any .TXT files were found in the path supplied by the user:

    SHELL _HIDE "DIR " + TxtPath$ + "*.txt /s /b > \alltxt.log" '        issue DIR command to Windows
    OPEN "\alltxt.log" FOR INPUT AS #1 '                                 open file created by DIR command
    FileLength& = LOF(1) '                                               get length of file
    IF FileLength& = 0 THEN '                                            does it contain data?
        CLOSE #1 '                                                       no, close the file
        LOCATE CurrentLine% + 2, CurrentColumn% '                        position cursor
        PRINT " Error: The supplied path does not contain text files." ' inform user no text file found
    END IF


The SHELL statement issues a DIR command to Windows asking for a listing of all .TXT files in the supplied path and then places that listing into a file called alltxt.log. This file is then opened and LOF() is used to get the size of the Windows created file. If zero is returned as the size of the file this means that no listing was placed in the file hence no .TXT files were found. LOF() is again used to determine if the search string was found in any of the .TXT files found:

IF LOF(2) = 0 THEN '                                                     were any files written to results?
    LOCATE CurrentLine% + 3, CurrentColumn% '                            no, position cursor
    PRINT " Your search yielded no results." '                           inform user of outcome
    CLOSE #2, #1 '                                                       close the files

- SHELL -

The SHELL statement is used to perform command line (CLI) operations using the program's host operating system, in our case Windows. For instance, QB64 has a throwback statement from the QuickBasic days called FILES which gives a directory listing of files in the current path. The problem with FILES is that it simply lists the files on the screen with no way to redirect the output of the command to something useful, such as a file or an array. However, the DIR command offered by Windows and LS command by Linux offer a wealth of options when retrieving a directory's listing. The SHELL statement allows QB64 to call upon the operating system to perform file related tasks otherwise impossible or incredibly hard to perform with QB64's command set. In the previous program listing we use the SHELL statement to ask Windows to get a directory listing of all .TXT files found in a given path:

    SHELL _HIDE "DIR " + TxtPath$ + "*.txt /s /b > \alltxt.log" '        issue DIR command to Windows

TxtPath$ contains the user supplied path to look in for the .TXT files. Let's assume the user didn't enter a path and the default path of C:\WINDOWS\ was used instead. The command issued to the command line by SHELL would look like so:

DIR C:\WINDOWS\*.txt /s /b > \alltxt.log

Here, we are asking Windows for a directory listing of all .TXT files ( *.txt ) in the C:\WINDOWS\ directory to include all subdirectories ( /s ) in bare format ( /b ) and direct the output to a file on the root of the current drive called alltxt.log ( > \alltxt.log ). This command will create a file for us with a listing of all .TXT files found.

The _HIDE statement instructs the SHELL command to hide the console window from view when performing the operating system command. If you were to omit the _HIDE statement the user would see the command window appear as the operating system task is being performed.

At another point in the previous program listing the SHELL command is again used to open the results of the search string in the Notepad program provided with Windows:

    SHELL _HIDE "cmd /c notepad \alltxtresults.log" '                    tell Windows to open file with Notepad

The command instructs Notepad to open the file called alltxtresults.log located on the root of the current drive. The cmd /c portion of the command initiates a second command line session for Notepad to run in ( cmd ) and then instructs the command line session to terminate once Notepad has been closed ( /c ). QB64 automatically places cmd /c in front of all commands sent using the SHELL statement. However, some programs, such as Notepad, need to explicitely see this on the command line, so it's good practice to use cmd /c on the command line all the time just to be sure your SHELL statements will not stall your program's execution.

-- Your Turn --










-- COMMAND REFERENCE --


Commands learned in previous tasks:

PRINT
INPUT
DIM
REM or '
CONST
TIME$
VAL()
IF...THEN
AND
END
GOTO
SELECT CASE...END SELECT
CASE
TO
IS
CASE ELSE
colon ( : )
FOR...NEXT
STEP
CLS
SLEEP
_DELAY
SYSTEM
DO...LOOP UNTIL (and variations)
WHILE...WEND
SCREEN
LINE
CIRCLE
PAINT
PSET
SUB
END SUB
FUNCTION
END FUNCTION
SHARED
SQR()
^ (exponent)
INPUT$
LINE INPUT
INKEY$
CHR$()
_KEYDOWN
_KEYHIT
_MOUSEX
_MOUSEY
_MOUSEINPUT
_MOUSEHIDE
_MOUSESHOW
_MOUSEMOVE
_MOUSEBUTTON
DIM
TYPE
END TYPE
AS
UCASE$()
LCASE$()
LTRIM$()
RTRIM$()
INSTR()
STR$()
DATE$
LEFT$()
RIGHT$()
MID$()
ASC()
STRING$()
SPACE$()
SWAP
AND
OR
XOR
NOT
\ (integer division)
MOD
INT()
CINT()
CLNG()
CSNG()
CDBL()
_ROUND()
FIX()
ABS()
SGN()
SIN()
COS()
TAN()
ATN()

New commands introduced in this task:

_DIREXISTS
_FILEEXISTS
CHDIR
MKDIR
RMDIR
KILL
NAME ... AS
OPEN
INPUT (file mode)
OUTPUT (file mode)
APPEND (file mode)
BINARY (file mode)
RANDOM (file mode)
PRINT (file statement)
CLOSE
EOF()
INPUT (file statement)
REDIM
_PRESERVE
UBOUND()
WRITE (file statement)
LINE INPUT (file statement)
FREEFILE
LOF()
SHELL
FILES
_HIDE

Concepts learned in previous tasks:

execute
statement
expression
literal string
syntax error
variables
type declarations
literal strings
concatenation
integers
long integers
single precision
double precision
strings
null strings
reserved words
operators ( +, -, *, / )
declare
constant
relational operators
functions
nesting
order of operations
Boolean
conditions
indenting
loops
labels
frame rate
controlled loops
conditional loops
Monochrome
Grayscale
pixels
bits per pixel
radian
counter-clockwise
aspect ratio
algorithm
subroutine
function
local variables
RAM
global variables
Pythagorean Theorem
BIOS
ASCII
buffer
CMOS
ROM
ASCII chart
array
one dimensional array
element
index
two dimensional array
three dimensional array
parse
geometry
algebra
trigonometry
calculus
angular momentum
physics engine
bitmap
polygon
vector math
numbering system
transistor
binary
binary numbering system
memory address
George Boole
Boolean logic
logic gate
bitwise math
flags
Banker's Rounding
sine
radian
cosine
tangent
arctangent

New concepts introduced in this task:

sequential file
dynamic array
Comma Separated Value (CSV) file
records
database