VB:Tutorials:Custom Resource Files

From GPWiki
Jump to: navigation, search

Introduction

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

End Type

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:

   Close intFileNum

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

End Type

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

End Type

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
   Close intBinaryFile

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.