VB:Tutorials:Custom Resource Files
Have you ever tried to use the standard resource file methodology included with Visual Basic? You likely found the utilities somewhat lacking (since the good ones are only included with VC++!) and difficult to use. Also, the result of using the standard resource methodology is that you end up with a HUGE FAT EXE file. Not very pleasant or professional, in my mind :)
Alternatives? Well you could store your bitmaps, waves, etc, as is in a separate directory... this doesn't sound so bad, but it does leave things vulnerable and somewhat messy.
But fear not! Your knight in shining armour is here! This tutorial will teach you how to create your own stand-alone resource files that need not be included in an EXE. Advantages to making your own resource files:
- They are harder to tamper with
- You can encrypt them
- You can modify them at runtime with code (make your own editors!)
- You can store any type of file you wish
- You don't need to recompile your project just to change your data
- They are nerdish good fun!
Files and Binary Access Lessons
Enough fluff, down to business. There are a few lessons that you need to learn regarding files and binary access before we can continue. First of all, you'll have to be constantly aware of the byte size of the various data types. Each data type is described by a set number of bytes, and as a result, they will take up a specified amount of space in your file:
- BYTE - one byte
- STRING - one byte per character
- BOOLEAN - two bytes
- INTEGER - two bytes
- LONG - four bytes
- SINGLE - four bytes
- DOUBLES - eight bytes
If you're using any other data types (in a game) you're probably being silly! Ok, now the interesting thing is when you have an ARRAY of a specific type, when you store it in a file it takes up space according to the size of the data type and the number of elements in the array (SIZE*ELEMENTS). So an array of 4 Integers would take up 8 bytes. Note that the values would be stored in the same order as they were in the array.
User Defined Types (UDT's) act similarly. A UDT containing one Byte and 3 Longs, for example, would take up 13 bytes in a file. Note, the values will be stored in the order in which the UDT was defined. So if your UDT declaration looked like this:
Private Type NEWTYPE
bytTest as Byte intTest as Integer lngTest as Long
Dim udtNewType as NEWTYPE
Then the Byte (bytTest) would be stored first, then the Integer (intTest), and finally the Long (lngTest). You can also have ARRAYS of UDT's, which would result in orderly storage of UDT data (as just described) according to the order of elements in the array (see above).
Confused yet? I thought so :)
In order to access a binary file we need to use the Open command with the Binary keyword:
Open App.Path & "\TEST.BMP" For Binary Access Read Lock Write As #1
This would open a file in the application's directory called TEST.BMP. If this file did not already exist, it would be created. The Read Lock Write portion of the statement indicates that we wish to read from this file and stop others from writing to it. You can alter these as you wish, even doubling up, Read Write Lock Read Write would allow you to read and write to a file while locking any other program from doing so.
The As #1 portion describes the number by which we would like to refer to this file. This number is used in Get and Put statements as well as in the Close command. If you would rather not hardcode the number you can use the FreeFile() function to give you a file handle (it'll be an Integer by the way) that's not currently in use:
Dim intFileNum as Integer
intFileNum = FreeFile Open App.Path & "\TEST.BMP" For Binary Access Read Lock Write As intFileNum
You can then use intFileNum later in code to refer to the file you've opened. Now you know how to open a file, next you need to learn how to close it. It is important that you remember to do so, otherwise errors may arise later in your code when you try to access a file that is still open and locked from a previous call. All you have to do is use the Close command followed by the file handle which you would like to close:
Simple as that! Ok, now just a little bit more to learn before you can start building your own file format. The last thing you need to know is how to use the Dir, Put, Get, Seek, and LOF functions... "is THAT all?" you ask? :P
Use the Dir function if you want to find out if a specific file or directory is currently in existence. For example, if your user wants to open a binary file for reading, you'd probably want to ensure that the file is actually there before proceeding!
retval = Dir(App.Path & "\TEST.BMP")
The Dir function will return a zero-length string ("") if the path specified is not valid. If the path is valid, a filename string will be returned.
The Put statement will take a supplied piece of data and store it at the specified location within the given (open) file. Observe:
Put intFileNum, 1, udtNewType
This code would store the variable udtNewType in the file referenced by intFileNum at the first available location within the file (byte number 1). Now to do the reverse of this, we use the Get statement:
Get intFileNum, 1, udtNewType
This code would extract data to fill the variable udtNewType from the file referenced by intFileNum starting at byte number 1. Now, you have to be careful, the Get and Put statements are none too smart, they'll do only exactly what you tell them. If you do not enter the correct byte at which to start, you will obtain unexpected results. Your variable will be filled with whatever data happens to be at that location in the file, there is no data-type checking here. If you Get an Integer, whatever 2 bytes happen to be at the location you specify, those are the two bytes you're a-gonna get! It doesn't matter if they "used to be" a String or part of a Long or whatever, they'll now be treated as an integer.
It is possible to leave the second parameter in a Put or Get statement blank. If you do this, the data will be stored/extracted from the location at which the LAST Put or Get statement left off. So if you do something like this:
Put intFileNum, 1, udtNewType Put intFileNum, , udtNewType
You'll end up with the first UDT stored at the beginning of the file, and the second one stored immediately after. This is kinda handy since it takes your mind off of what-goes-where in situations where you are reading/writing sequentially. Now, if you need to find out WHERE the current read/write location is, you can use the Seek function:
retval = Seek(intFileNum)
This will return a Long describing the current read/write location for the given file handle (intFileNum). If you were to perform a Put or Get without specifying a specific byte location, the value returned by Seek is the location at which the Put/Get would occur.
Lastly, we have the LOF function. Simply pass a file handle to the LOF function and it will spit out a Long describing the current size of the file in bytes:
retval = LOF(intFileNum)
This can be useful in many situations, as I'm sure you can imagine. Ok, now you know all of the functions you need to set up your own file format. Take a deep breath, cuz here we go!
Creating a Custom Resource File Format
Storing data in binary format is not terribly difficult. All you'd have to do is read the data from a file and write it to another file. But, once you start compiling multiple files into a single binary, you run into referencing problems. Where does one file start and the other end? How do I retrieve only one of the files at a time? How do I ensure that my file has not been tampered with? .. all of these concerns can be addressed through judicious use of HEADER structures.
The first structure I like to use is what I call my FILEHEADER (similar to the bitmap style file format):
Private Type FILEHEADER
intNumFiles As Integer lngFileSize As Long
This UDT contains data on the file in general. The overall size of the file is stored in the lngFileSize variable for use in validity checking. When you open a binary file, you should check the size of it (using the LOF function) against the size stored in the lngFileSize member, if they are conflicting, then you know that someone has tampered with your file! There are other validity checking methods, but I won't go into them here.
The intNumFiles variable will describe the total number of original files that are now stored within this binary. This is very useful in the next step, the INFOHEADER:
Private Type INFOHEADER
lngFileSize As Long lngFileStart As Long strFileName As String * 16
You will need one INFOHEADER variable for each file you store in your binary. The INFOHEADER describes HOW the file is stored within the binary and what the file's reference name is.
lngFileSize is the size of the stored file, lngFileStart is the starting byte location within the binary at which this file was Put. strFileName is some sort of string handle that you can use to retrieve files from the binary.
So now we have our header structures, all we need is some data. Our example files will be called SAMPLE1.BMP, SAMPLE2.WAV, and SAMPLE3.TXT. With this information we can create and define our header structures. First we'll need to create an INFOHEADER array containing 3 elements (for our 3 files) and open the files to determine their size (and store this in the lngFileSize member):
Dim intSample1File As Integer
Dim intSample2File As Integer
Dim intSample3File As Integer
Dim FileHead As FILEHEADER
Dim InfoHead() As INFOHEADER
intSample1File = FreeFile Open App.Path & "\SAMPLE1.BMP" For Binary Access Read Lock Write As intSample1File intSample2File = FreeFile Open App.Path & "\SAMPLE2.WAV" For Binary Access Read Lock Write As intSample2File intSample3File = FreeFile Open App.Path & "\SAMPLE3.TXT" For Binary Access Read Lock Write As intSample3File ReDim InfoHead(2) InfoHead(0).lngFileSize = LOF(intSample1File) InfoHead(1).lngFileSize = LOF(intSample2File) InfoHead(2).lngFileSize = LOF(intSample3File)
Now that we've stored the file sizes we can continue and store their names:
InfoHead(0).strFileName = "SAMPLE1.BMP" InfoHead(1).strFileName = "SAMPLE2.WAV" InfoHead(2).strFileName = "SAMPLE3.TXT"
Things get a tad tricky here. In order to fill out the lngFileStart member of the INFOHEADER structure we have to first determine the amount of space that will be taken up by the INFOHEADER and the FILEHEADER:
Dim lngFileStart as Long
lngFileStart = (6) + (3 * 24) + 1
Our first file (SAMPLE1.BMP) will be stored at the location given by this variable, lngFileStart. To calculate lngFileStart we have to determine how many bytes our headers will take up, and how many of them there are. The FILEHEADER is made up of one Integer and one Long, that's 6 bytes. The INFOHEADER is made up of two Longs and one 16 character String, that's 24 bytes. So we add 6 bytes plus 24 times the number of INFOHEADERS we're using (in this case, 3). We then add 1 since we want to start at the byte immediately after the last byte of the INFOHEADER.
InfoHead(0).lngFileStart = lngFileStart lngFileStart = lngFileStart + InfoHead(0).lngFileSize InfoHead(1).lngFileStart = lngFileStart lngFileStart = lngFileStart + InfoHead(1).lngFileSize
This code stores the lngFileStart member for each of the INFOHEADER structures. It increments the lngFileStart variable after each INFOHEADER index by adding the size of the file. In this way, the first file will be stored immediately following the headers, and each subsequent file will be stored linearly thereafter.
So, now our INFOHEADER is filled, we just need to fill out the FILEHEADER and then store the data. First, the FILEHEADER:
FileHead.intNumFiles = 3 FileHead.lngFileSize = (InfoHead(0).lngFileSize) + (InfoHead(1).lngFileSize) +
(InfoHead(2).lngFileSize) + (6) + (3 * 24)
This adds up the size of the headers and of the data we're going to store and places it into the lngFileSize member. Now we Get the data from our three files using appropriately sized byte arrays and close the files:
Dim bytSample1Data() As Byte
Dim bytSample2Data() As Byte
Dim bytSample3Data() As Byte
ReDim bytSample1Data(LOF(intSample1File) - 1) ReDim bytSample2Data(LOF(intSample2File) - 1) ReDim bytSample3Data(LOF(intSample3File) - 1) Get intSample1File, 1, bytSample1Data Get intSample2File, 1, bytSample2Data Get intSample3File, 1, bytSample3Data Close intSample1File Close intSample2File Close intSample3File
Almost done! Now we just open a new file for writing, and Put all of our data:
Dim intBinaryFile as Integer
intBinaryFile = FreeFile Open App.Path & "\BINARY.DAT" For Binary Access Write Lock Write As intBinaryFile
Put intBinaryFile, 1, FileHead Put intBinaryFile, , InfoHead Put intBinaryFile, , bytSample1Data Put intBinaryFile, , bytSample2Data Put intBinaryFile, , bytSample3Data
That's all there is to it! You've now stored these three files in a new binary file called BINARY.DAT. To extract the data, simply reverse the process using the data stored in the FILEHEADER and INFOHEADER structures stored at the start of the binary file. To see this put to use, click here for sample source code.