Site hosted by Angelfire.com: Build your free website today!

Home
DelphiZeus
Builing Custom Files with TFileStream
Creating data files with data segments

Home



VCL File Methods
When you want to save something to a Disk File, the Delphi VCL offers you a "SaveToFile" method for many of its Objects that users would normally want to save. A TMemo has it's text as a "Lines" property which is a TStrings Class, that has a "SaveToFile" method. A TBitmap has a "SaveToFile" method, and so do several other VCL Objects.
Most Delphi users are taught how to use the Delphi "File management routines" with AssignFile(var F; FileName: string), , Reset( ), , Readln( ) and CloseFile( ). . . You can create Text, Typed and UnTyped files with these methods and do alot of file read-write operations. According to the Delphi Help the TFileStream is a more recent file component. Alot of the methods used with AssignFile( ) go back to TuboPascal, so TFileStream gives you some more file options and is a wrapper for the API functions like CreateFile( ), WriteFile( ), CloseHandle( ) and other file functions. You can use TFileStream to do many File Writing and Reading operations, most of the Examples given in the Help and Books just do a single Data Type write and read to file. But what if you need to make your own custom data storage file containing several different variable types and program dependent changing amounts of data?
Multi-Data Files
If you need to do your own custom multi-Data file, there is not much to show you how to do that in the Help, Demos and Delphi books. The TFileStream has several methods for writing and reading file bytes (data), with it's Position and Seek you can place or get bytes anywhere in the file. The TFileStream has all of the methods required to make many kinds of files that you might need, but you will have to figure out what you need in the custom file to use in your Program. Once you know what Data you will need to save in your custom file, you will need to place these bytes in your file and be able to read them out later. This page will show to several examples for using a TFileStream to make files with many different kinds of Data in them. You will be shown how write and read fixed size variables like an Integer or TRect, , and variables that can change their byte size, like Strings and Bitmaps. - - -
Some file access information (Help, Books) have a reference to "Text" and "Binary" files, I will not make any such reference here, ALL Data is "Binary" it only becomes "Text" data if it is read out of a file and "Converted" or "Translated" to ASCII text charaters, but it is ALWAYS bit (Binary) data, except when it is shown on your monitor as text charaters.
I have tried to give some extra information about the file access methods of TFileStream, because some Delphi devlopers seem to have a very difficult time understanding the basics of file data, , and what bytes are and how they contain something (data), and that blocks of bytes (memory or file) can have a different position for different data. Some other things like "Type Checking" or "fixed-size memory data variables" and "changable-size memory data variables" may also not be easy for them to understand at first. I have found it neccessary to give some lengthty instructions in this page. If you already know about a subject being explained here, I am sorry you will have to skip over that explanation. This page can seem long and over explained (or boring). In case you just want to go to a certain code example, here are some links to subjects on this page-

Writting and Reading More than One String to a FileStream

Writing and Reading Dynamic Arrays of Non-Fixed Size Variables

Using TGraphic Classes (TBitmap) in a FileStream

File Headers

Using Data Segment IDs to Separate Data

TFileStream

A file is the basic way to reference drive storage, that enables a computer to distinguish one set of information from another on a drive. TFileStream enables programs to read from and write to a file on a drive. It can be used whenever you need to access the bytes of a File. Using the TFileStream will require you to know some about the things (byte Data) that you will want to write to file, such as the number of bytes in that Data and the arrangement of those bytes, in Records and structures. File access is based on exact "File" byte position reference number, TFileStream will do much of the file positioning automatically for writting and reading.

As with all VCL objects, you will need to create it before you use it. The TFileStream Create method will open a file on a drive, and it has two parameters.

TFileStream.Create(const FileName: string; Mode: Word);
Put the name of the file and the way the file should be opened as it's parameters. The FileName parameter has to be a valid file path for the system it's running on, if it is not a valid path then an exception is thrown. The Mode parameter sets how the file is to be opened and consists of an open mode and a share mode. These modes can be set to determine "How" the file is opened, you can open a file to write to it (fmOpenWrite), or to read from it (fmOpenRead), or to do both (fmOpenReadWrite). You can also have the function create the file with mode fmCreate, which will create the file is it does not exist or erase the file if it already exists. There are 5 Share Modes which can restrict access to this file, I will mostly use the fmShareDenyWrite mode here. You should look at the Delphi Help for TFileStream.Create to get more information about these modes and then try different modes to see how they effect the file stream. Once you have a TFileStream created you can write bytes of data to this file with the WriteBuffer( ) method.
procedure TFileStream.WriteBuffer(const Buffer; Count: Integer);
    Or you can Read bytes from this FileStream with it's ReadBuffer( ) method
procedure TFileStream.ReadBuffer(var Buffer; Count: Integer);
    You should read the Delphi Help about these two functions.

Unlike many Delphi fuctions, the first parameter of both of these fuctions (Buffer) is an UnTyped variable, which means that you can put almost any variable in this parameter and the Delphi complier will accept it without type checking (no warings). This is both good and bad, This was nessarary because there are many different data types that can be written to a file, so this variable needs to be very flexible and accept any Type of variable, the bad part is that there is no Type Checking by the compiler, so you can make anykind of misteak using the Buffer parameter and it will compile without warning. So you need to know how to use this UnTyped Variable. If you use a variable of Fixed memory size for it's "Data" Value like Integer, Byte, Char, TPoint, TRect and others. You can just use that variable as the Buffer and put a SizeOf( ) in the Count parameter. The compiler will get the "Value" data bytes of this Buffer variable and write or read it for the file. For variables with changable memory size for it's "Data" value, like String, Pchar (or any Pointer variable), TObject, this method will NOT work. The "Value" data for these changeable memory allocation variables is a 4 Byte (numeric) Pointer address, and has nothing to do with the "Data" (like text in String and PChar) for these variables.

Using the TFileStream Write and Read Methods

    Tech Note - The TFileStream source code uses the standard API File access functions of CreateFile( ), ReadFile( ), WriteFile( ), and CloseHandle( ). So the TFileStream file access methods are like the API functions.

You may want to read the Delphi Help for the TFileStream.Create and the TFileStream.WriteBuffer procedures. The following code is an example for writing four integers to a file, in a procedure. This will create a file named "G:\Test1.file'" and overwrite that file if it already exists without warning. When naming this file, I have used a File extention of ".file" which is an uncommon file extention (compared to-  .txt  .doc  .bmp  .wav  .pas) but you can name your files anyway that is useful for you, with any file extention or no file extention. Be careful if you use a commom file extention - -
procedure WriteToFile;
  var
  FileStream1: TFileStream;
  Int1, Int2, Int3, Int4: Integer;
begin
Int1 := 100;
Int2 := 200;
Int3 := 1918986307;
Int4 := 1702521171;
FileStream1 := TFileStream.Create('G:\Test1.file', 
                 fmCreate or fmOpenWrite or fmShareDenyWrite);
try
  FileStream1.WriteBuffer(Int1, SizeOf(Int1));
  FileStream1.WriteBuffer(Int2, SizeOf(Int2));
  FileStream1.WriteBuffer(Int3, SizeOf(Int3));
  FileStream1.WriteBuffer(Int4, SizeOf(Int4));
  finally
  FileStream1.Free;;
  end;
end;
The FileStream1 is created with the fmCreate or fmOpenWrite open Mode and the fmShareDenyWrite share Mode. This means that a New File is created with write access, and no other program can write to this file while it is open. All of our integer variables are given values in the begining of the procedure, an after we create the FileStream1 we can write to this file. The first FileStream1 write buffer is for the variable Int1, an Integer, notice that we use the SizeOf( ) function in the Count parameter. (Do you know the number of bytes this SizeOf function will return for an Integer?) This will write 4 bytes to the file with the bits set in these 4 bytes for the integer value of 100. But these 4 bytes are all that is saved to the file, there is NO information saved to the file about what this data was (an Integer in this case). Next the second Integer Int2 is written to the file. But where are the 4 bytes for this integer written, at the beginning of the File, over top of the Int1 bytes? No, after each write the FileStream position is Advanced to where the last write ended, so the Int2 is added on the end of the file, which would be byte number 5, Also, when you use the FileStream write the file size is automatically increased to the size needed for that write. So we can just use the WriteBuffer method and put as many fixed size variables in the file as we want to. Two more Integers Int3 and Int4 are added to the file, the values for these integers (1702521171, 1918986307) will be explained later.

the TFileStream.Position - Is a zero based integer value that has the current offset of bytes from the begining of the FileStream. Any operation for reading and writing the FileStream will use the file byte begining at the current FileStream Position. The TFileStream access methods, (Write, WriteBuffer, Read, ReadBuffer, LoadFromStream, SaveToStream, CopyFrom) all start their file operation at the Current FileStream Position and automatically advance the FileStream Position to where that method ends it's operation. You can set the FileStream Position to any location of the file, you can also set the position Past (greater than) the current end of the file, if the FileStream is created with the fmOpenWrite Mode, then the file's Size will be increased to the new FileStream Position past the end of the file. If the FileStream is created with the fmOpenRead mode, then the Position can NOT go higher than the Size of the FileStream.

  IMPORTANT - You must FREE the FileStream to release the file from the system's file handleing. To use operating system resources efficiently, a program should Free a TFileStream as soon as you are finished with it, I usually use a try and finally block to make sure the FileStream is Free.

So now we have made a Muti Data file with 4 integers in it at file path 'G:\Test1.file' . . . Now we need to Read these 4 integers out of the file. Remember, that since we wrote the file, we know what the data Types are, and where the data segments are located (their file Byte position). Here is some code to read the four integers out of the file. - -
procedure ReadFromFile;
  var
  FileStream1: TFileStream;
  Int1, Int2, Int3, Int4: Integer;
  Str1: String;
begin
FileStream1 := TFileStream.Create('G:\Test1.file', 
                 fmOpenRead or fmShareDenyWrite);
try
  FileStream1.ReadBuffer(Int1, SizeOf(Int1));
  FileStream1.ReadBuffer(Int2, SizeOf(Int2));
  FileStream1.ReadBuffer(Int3, SizeOf(Int3));
  FileStream1.ReadBuffer(Int4, SizeOf(Int4));
  finally
  FileStream1.Free;;
  end;
Str1 := 'Int1-'+IntToStr(Int1)+' Int2-'+IntToStr(Int2)+' Int3='
         +IntToStr(Int3)+' Int4'+IntToStr(Int4);
end;
This time the FileStream1 is Created with the fmOpenRead mode so we can read from the file. Remember, that there is NO information in this file about what the bytes in this file are for, or the order or type of Data in this file, so you will need to know what Data is in the file to get data out of this file. Since we wrote this file we know the data in it and it's arrangement, so we just read the Data out of the file in the exact same sequence that we put the data into it. After the file is opened, we just read the file one Integer at a time like we did when we wrote the file. Remember, the ReadBuffer procedure will automatically advance the FileStream Position to the end of the read operation or the end of the file, whichever comes first. It can NOT advance the Position past the end of the file. So after the second integer (Int2) is read, the FileStream Position is 8, after the third integer is read the file Position is 12. I have added a String, Str1, which is used to put the Integer values into text, so you can display this string in a TLabel.Caption or ShowMessage( ) to confirm that it has gotten the correct integer values.

the Bytes in a File do NOT have a Type - As I have said, there is no information about the bytes (data) in the file unless you put this information in the file, and know how to get this information out of the file and use it. I did not put any information into the file about the data of this file, I will deal with that later. The next code examples are meant to show you that the bytes of a file can be read out of the file and used in ANY WAY, without any reguard to what the bytes were used as (their data Type) when the bytes were written to file. The file Size of the "G:\Test1.file" will be 16 bytes (4 bytes for each integer). An Integer is a common variable in Delphi, a TPoint has two integers in it's Record type. So 4 Integers would be the amount of Integers in 2 TPoints. So we can read these 16 file bytes into 2 TPoints, as in the following example - -
procedure ReadFromFile;
  var
  FileStream1: TFileStream;
  Pnt1, Pnt2: TPoint;
  Str1: String;
begin
FileStream1 := TFileStream.Create('G:\Test1.file', 
                 fmOpenRead or fmShareDenyWrite);
try
  FileStream1.ReadBuffer(Pnt1, SizeOf(Pnt1));
  FileStream1.ReadBuffer(Pnt2, SizeOf(Pnt2));
  finally
  FileStream1.Free;;
  end;
Str1 := 'Pnt1.x-'+IntToStr(Pnt1.x)+' Pnt1.y-'+IntToStr(Pnt1.y)+
        ' Pnt2.x='+IntToStr(Pnt2.x)+' Pnt2.y'+IntToStr(Pnt2.y);
end;
This works because the TPoint has 2 integers in it. The integer Values for the Pnt1, Pnt2, x and y values will be the same as the Integer values we got before in the Int1, Int2, Int3, Int4 integers. What about a TRect. . . it has 4 integers in it. . . you could read a TRect from this FileStream1 with -
procedure ReadFromFile;
  var
  FileStream1: TFileStream;
  Rect1: TRect;
  Str1: String;
begin
FileStream1 := TFileStream.Create('G:\Test1.file', 
                 fmOpenRead or fmShareDenyWrite);
try
  FileStream1.ReadBuffer(Rect1, SizeOf(Rect1));
  finally
  FileStream1.Free;;
  end;
Str1 := 'Rect1.Left-'+IntToStr(Rect1.Left)+
        ' Rect1.Top-'+IntToStr(Rect1.Top)+
        ' Rect1.Right='+IntToStr(Rect1.Right)+
        ' Rect1.Bottom'+IntToStr(Rect1.Bottom);
end;
Again we get the same integer values from the file in the Rect1 variable. I am showing you these last two examples, not for a way to read files, but so you will see that the bytes in your file are not restricted in how they are read or used. You should notice that there is NO TYPE-CHECKING by the compiler for the bytes read into the TPoints or the TRect. This is not posible, the program has no information about what is in this file. The ONLY thing that sets how and what is read form this file is YOU, in your code.

Changing File Position with Seek and Position
In the examples above, whenever you wrote or read from the file, the file's position was automatically changed to the location where the write or read ended. You can use the TFileStream.Postion property to get or set where the file is written to or read from. Position gets or sets the Byte location for the file operation to begin with that byte (it is zero based). Position uses the Seek function to change the file position. The function
TFileStream.Seek(Offset: Integer; Origin: Word): Integer;
also resets the current file position, and also has a reference in the Origin parameter for where in the file to apply the Offset parameter. Let's look at a code example that changes the FileStream Position and reads a variable from the new Position. Keep in mind that there are Four Bytes in an Integer variable and that the FileStream Position is zero based -
procedure ReadFromFile;
  var
  FileStream1: TFileStream;
  Int1, Int2, Int3, Int4: Integer;
  Pnt1: TPoint;
begin
FileStream1 := TFileStream.Create('G:\Test1.file',
                 fmOpenRead or fmShareDenyWrite);
try
  FileStream1.ReadBuffer(Int1, SizeOf(Int1));
  FileStream1.ReadBuffer(Int2, SizeOf(Int2));
  FileStream1.ReadBuffer(Int3, SizeOf(Int3));
  FileStream1.ReadBuffer(Int4, SizeOf(Int4));

  FileStream1.Position := 4;
  FileStream1.ReadBuffer(Pnt1, SizeOf(Pnt1));
  finally
  FileStream1.Free;;
  end;
ShowMessage(IntToStr(Int2)+'  '+
            IntToStr(Int3)+'  '+
            IntToStr(Pnt1.x)+'  '+
            IntToStr(Pnt1.y));
end;
The 4 integers are read from the file just like we have done before, and then the FileStream1 Position is set to 4 with
FileStream1.Position := 4;
and the Pnt1 (TPoint) variable is read from the file. You will see in the ShowMessage text that the Int2 and Pnt1.x values are the same, and the Int3 and Pnt1.y values are also the same. If you set the FileStream1 Position to 8 and read the Pnt1 variable, the Pnt1.x will equal the Int3 and the Pnt1.y will equal the Int4.
You can also set the FileStream1 Position with the Seek Method. If you use the soFromBeginning in the Origin parameter, then the Seek function is the same as the Position property. So

FileStream1.Seek(4, soFromBeginning);
will set the file position to the same location as
FileStream1.Position := 4;
and from the End of the file -
FileStream1.Seek(-12, soFromEnd);
will also set it to the same file position offset in this 16 byte file. Notice that the Offset is a negative number, which means that the file position is moved 12 bytes towards the begining of the file, decreasing the Position by 12 bytes from the end of the file. Which will result in a FileStream1.Position of 4.

Reading Different Data Out of File
I have been using integers and integer Records (TPoint, TRect). So as an example for you about data types in files, not as read methods, in this next code lets use some non-integer variables. There are 4 byte variables, 2 Word variables, 4 Char variables, and an Array of 4 Char, which will be read from the same file used before with 4 integers in it. Also, this time I will try to read data past the end of the file to show you what happens - -
procedure ReadFromFile;
  var
  FileStream1: TFileStream;
  Byte1, Byte2, Byte3, Byte4: Byte;
  Word1, Word2: Word;
  Ch1, Ch2, Ch3, Ch4: Char;
  aryChar1: Array[0..3] of Char;
  Int1: Integer;
begin
FileStream1 := TFileStream.Create('G:\Test1.file',
                 fmOpenRead or fmShareDenyWrite);
try
  FileStream1.ReadBuffer(Byte1, SizeOf(Byte1));
  FileStream1.ReadBuffer(Byte2, SizeOf(Byte2));
  FileStream1.ReadBuffer(Byte3, SizeOf(Byte3));
  FileStream1.ReadBuffer(Byte4, SizeOf(Byte4));

  FileStream1.ReadBuffer(Word1, SizeOf(Word1));
  FileStream1.ReadBuffer(Word2, SizeOf(Word2));

  FileStream1.ReadBuffer(Ch1, SizeOf(Ch1));
  FileStream1.ReadBuffer(Ch2, SizeOf(Ch2));
  FileStream1.ReadBuffer(Ch3, SizeOf(Ch3));
  FileStream1.ReadBuffer(Ch4, SizeOf(Ch4));

  FileStream1.ReadBuffer(aryChar1, SizeOf(aryChar1));

  ShowMessage(IntToStr(FileStream1.Position));
  if FileStream1.Read(Int1, SizeOf(Int1)) < SizeOf(Int1) then
    ShowMessage('Could not Read file past the end');
  finally
  FileStream1.Free;;
  end;
ShowMessage(IntToStr(Byte1)+'   '+IntToStr(Byte4)+
            '  '+IntToStr(Word1)+'  '+IntToStr(Word2));
ShowMessage(Ch1+Ch2+Ch3+Ch4+'  '+aryChar1);
end;
All of these non-integer variables are also read out of the integer file without warning from the compiler, because a TFileStream can NOT type check any variable that is read from a file, and the Buffer parameter is an UnTyped variable, so YOUR CODE is the only thing that determines what the bytes read from a file are used for. The Byte and Word variables will read the bytes out of a Part of the integer values in the file, the Char variables will read one byte of the integer value and translate that into a the Char Letter for that byte value, after reading the file, the four Char variables should be set to 'C', 'h', 'a', 'r', which is why the Int3 value was 1918986307 when we wrote the file. The aryChar1 variable is read from the file using the same method as a Char or integer variable, because it is a Fixed Length Array, and the Charaters read from this integer file in the aryChar1 should be 'Size' because the Int4 was written as 1702521171. If a String or Dynamic Array (variable length) is used, then a different method is used to write and read variables with data that is NOT a fixed memory size, like dynamic Arrays, Strings, PChar, and Bitmaps. I will show these methods later in the Using Non-Fixed Size Variables in Files.

Reading past the End of File
In this example above I changed the last file read method to Read( ), when the TFileStream tries to use the Read( ) method past the end of the file, it does NOT throw an exception, as ReadBuffer( ) would, the TfileStream.Read( ) is a function with a Result of the number of bytes acually read from the file, so the test -

if FileStream1.Read(Int1, SizeOf(Int1)) < SizeOf(Int1) then
will be true, because the Result of the Read( ) function will be Zero and the error Message will be shown.

There are two other TFileStream access functions Write( ) and Read( ), they have the exact same parameters as the WriteBuffer and ReadBuffer procedures, but these are Functions, not Procedures, and return the number of Bytes of the file access, but these functions will NOT throw an Exception if the full amount of Count is not written or read. So you should try and change the code above, in the lines -
if FileStream1.Read(Int1, SizeOf(Int1)) < SizeOf(Int1) then
    ShowMessage('Could not Read file past the end');
change them to a single line using ReadBuffer
FileStream1.ReadBuffer(Int1, SizeOf(Int1));
and see what happens when you execute the code (it will throw an Exception). Many wonder what the difference is between the "Write" and "WriteBuffer" methods, , and the "Read" and "ReadBuffer" methods since they have the same parameters, the "Write" and "Read" methods will not block code execution, even if they go past the end of the file, or read or write nothing, so You will have to do checks for file access errors. But the WriteBuffer and ReadBuffer methods will exception out of code progression for any file access error. You can substitute the WriteBuffer and ReadBuffer procedures for the Write and Read functions, since they use the exact same parameters. I will use the Write( ) and Read( ) functions, when I do NOT want an excepton to be thrown, and the Buffer procedures WriteBuffer( ) and ReadBuffer( ) when I do want exceptions. . I want to show you some methods for testing for file access errors. But you should use the WriteBuffer( ) and ReadBuffer( ) procedures in your code if you do not need special code for file access errors, , and your style of coding would benifit from bad write or read Exceptions. . I will not try and explain exceptions and exception handling here.

USE   ReadBuffer( ) and WriteBuffer( )
For most of the code that Delphi programmers write, the -
ReadBuffer( )
WriteBuffer( )
procedures are better for them to use. These procedures offer a method of protection for file access errors, like reading past the end of the file. Although you can produce code using the functions Read( ) and Write( ) , and you can test your code and it will work for you, but if your code is used on an incorrect file later outside of testing, then it may try and read past the end of a file and not do anything for that error. The exceptions that the ReadBuffer( ) and WriteBuffer( ) procedures create are like so many other Delphi exception methods, that you should use the ReadBuffer( ) and WriteBuffer( ) procedures, unless you have a special reason to test and catch the file access errors yourself. If you do use the Read( ) or Write( ) functions, be sure to have your own tests for file access errors.

Do the Math
All files are a collection of bytes, and all files use mathematic methods (add, subtract, multiply) to know how to write and read what is in the file. If you are storing several different data segments in one file you must account for the position in the file bytes where the data segment for each piece of different data is located, and the size of the byte block for that data. In my code examples so far, the WriteBuffer( ) and ReadBuffer( ) procedures automatically move the file position the amount of the file write or read. So in your code you do not have to set the file position, or do any math operations for position or size of data segments. In the code examples below there are some methods shown to create some muti-data files, but if you are making your own muti-data files, and your file data blocks becomes more complex, you must "Do the Math" for your data arangement. You have to know, and plan, and count, and calculate, the variables or data you need to store in your file, and be able to place any information in your file that is required to read it out of the file. Although for many files you will not need to to do math calculations for data arrangement

How to test
What I always do, is to start with just a few of the most important Data variables, and create the file and write the data in it. Then I have a file reader procedure, which I will read those data blocks out, and I always test what has been read to see if the is data correct. If there are access errors or the data is not correct, you can place a ShowMessage( ) to find where the error is, Place a -
    ShowMessage( 'Int1 '+IntToStr(Int1) )
at different places just after a read out of a data block, to see where the error happens, and try different code to correct the error. At first you may know what to do if there is an error in file write or read. Often you may forget to place a data length segment, or you will have an incorrect value of the "Data Size" written or read on file. . . . . If these data reads are all correct, then I add a few more variables and write them all to file, and then read and test the added ones. I keep adding new variables and testing until I have all of the information I need in the file.

One thing you must do is to have the correct amount of bytes to write or read for the "Count" parameter. For many variable types (with unchanging data length) you can use the SizeOf( ) function. But if you are getting errors in your file reads, then most likely you have some data length or data position error in your code.

Fixed Size Variable Example
Next is an example for how to Write and Read several fixed size variables in a file, which is for methods you may really use. This uses a variable (FixStr1) with the String[15] Type, this is a Fixed Length String and can be used like an Integer for reading and writing. There is a TMyRec Record defined, this is made up of Fixed Size variables and can be used like the other fixed size variables for file read and write.

Writing the file. . Please Notice that in all of the Write functions, the Count parameter is always set to the SizeOf(Var) for the variable that is being written. You can only use the SizeOf  for Fixed Size variables, it will NOT work correctly for Non-Fixed size variables like a Long String or TBitmap.
procedure WriteToFile;
type
 TMyRec = Record
   aInt: Integer;
   aRect: TRect;
   asStr28: String[28];
   end;

  var
  FileStream1: TFileStream;
  Char1: Char;
  Int1: Integer;
  Pnt1: TPoint;
  Rect1: TRect;
  aryChar1: Array[0..9] of Char;
  FixStr1: String[15];
  MyRec1: TMyRec;

begin
Char1 := 'C';
Int1 := 100;
Pnt1.x := 500;
Pnt1.y := -20;
Rect1 := Rect(100,200,300,400);
aryChar1 := 'Char Array';
FixStr1 := 'Fixed String';
MyRec1.aInt := 44;
MyRec1.aRect := Rect(1,2,3,4);
MyRec1.asStr28 := 'Some Text';

FileStream1 := TFileStream.Create('G:\Test two.data',
                 fmCreate or fmOpenWrite or fmShareDenyWrite);
try
  FileStream1.WriteBuffer(Char1, SizeOf(Char1));
  FileStream1.WriteBuffer(Int1, SizeOf(Int1));
  FileStream1.WriteBuffer(Pnt1, SizeOf(Pnt1));
  FileStream1.WriteBuffer(Rect1, SizeOf(Rect1));
  FileStream1.WriteBuffer(aryChar1, SizeOf(aryChar1));
  FileStream1.WriteBuffer(FixStr1, SizeOf(FixStr1));
  FileStream1.WriteBuffer(MyRec1, SizeOf(MyRec1));

  finally
  FileStream1.Free;;
  end;
end;


Reading from this file uses the same method for every variable read, as you can see in the following code. Please Notice that the variables are read in the same exact order that they were written to the file. . Also on the Last Variable Read (MyRec1) there is a Test to see if the Bytes Read are less than the SizeOf(MyRec1), if the Error message is shown, then you know that you have not correctly written, or read the file.
procedure ReadFromFile;
type
 TMyRec = Record
   aInt: Integer;
   aRect: TRect;
   asStr28: String[28];
   end;

   var
  FileStream1: TFileStream;
  Char1: Char;
  Int1: Integer;
  Pnt1: TPoint;
  Rect1: TRect;
  aryChar1: Array[0..9] of Char;
  FixStr1: String[15];
  MyRec1: TMyRec;

begin
FileStream1 := TFileStream.Create('G:\Test two.data',
                 fmOpenRead or fmShareDenyWrite);
try
  FileStream1.Read(Char1, SizeOf(Char1));
  FileStream1.Read(Int1, SizeOf(Int1));
  FileStream1.Read(Pnt1, SizeOf(Pnt1));
  FileStream1.Read(Rect1, SizeOf(Rect1));
  FileStream1.Read(aryChar1, SizeOf(aryChar1));
  FileStream1.Read(FixStr1, SizeOf(FixStr1));
  if FileStream1.Read(MyRec1, SizeOf(MyRec1)) < SizeOf(MyRec1) then
    ShowMessage('ERROR - - Could not Read data from file');

  finally
  FileStream1.Free;;
  end;
ShowMessage(IntToStr(Int1)+'   '+
            IntToStr(Pnt1.x)+'  '+
            IntToStr(Rect1.Left)+'  '+
            IntToStr(MyRec1.aInt));
ShowMessage(aryChar1+'  '+FixStr1+'  '+MyRec1.asStr28);
end;
You could subtitute the ReadBuffer procedure for all of the Read functions in the code above, and if there were any file read errors you would get an Exception, and the Try and Finally block would close the file and Free the FileStream1.

Saving Arrays of Records to File
You can have Arrays of records, using records containg Fixed Size Variables and write these arrays to File. You can NOT use the same methods for the Static and Dymanic Arrays availible in Delphi Pascal. With the dynamic array, the SizeOf(DynamicArray) will NOT give you the corret size. For the next examples I will use a TDemo Record defined as -

type
  TOfDemo = (odOpen, odRead, odWrite, odFind, odClose);

  TDemo = record
    aByte: Byte;
    aInt: Integer;
    FixStr1: String[16];
    aOfDemo: TOfDemo;
    aReal: Real;
    aRect: TRect;
    WriteTime: TFileTime;
    aSet: Set of TOfDemo;
    end;
You must make sure that there are no variables in your record or sub-Records that have a variable memory Data stroage allocation, like a String, Pchar (or other Pointer type), or TObject (like a TBitmap, or TStringList). I have included several different variable types in this record to show some of the Fixed-Size variables you can use.

Code for procedure to write array of TDemo records -

procedure SaveAryFile;
var
aryDemo: Array[0..5] of TDemo;
FStm: TFileStream;
i: Integer;
begin
ShowMessage(IntToStr(SizeOf(aryDemo)));
aryDemo[0].FixStr1 := 'First String';
aryDemo[0].aOfDemo := odRead;
aryDemo[0].aSet := [odWrite, odClose];

aryDemo[5].FixStr1 := 'Last String';
aryDemo[5].aOfDemo := odClose;
aryDemo[5].aSet := [odRead, odFind];
FStm := TFileStream.Create('G:\the Demo.aryDemo',
                 fmCreate or fmOpenWrite or fmShareDenyWrite);
try
  i := High(aryDemo);
  FStm.WriteBuffer(i, SizeOf(i));
  for i := 0 to High(aryDemo) do
    FStm.WriteBuffer(aryDemo[i], SizeOf(TDemo));
  finally
  FStm.Free;
  end;
end;

I have a six member array   Array[0..5] of TDemo; , which I will write to a file. I place some data in the first and last TDemo records just for this demo to read out later. Since I am writting this file, I will know that this array has 6 members (6 TDemo records) in it, but I will place the High Index of this array as the first data segment of the file. That way I can read this value (data) out of the file and test to see if it is the correct amount of array members needed to read into the array of TDemo. And then I have a FOR Loop to run through the array and write all of it's TDemo records to file using the TFileStream WriteBuffer method with the SizeOf(TDemo) as the Count parameter.

Code to read this array file -

procedure LoadRecAry;
var
aryDemo: Array[0..5] of TDemo;
FStm: TFileStream;
i: Integer;
begin
FStm := TFileStream.Create('G:\the Demo.aryDemo',
                 fmOpenRead or fmShareDenyWrite);
try
  FStm.ReadBuffer(i, SizeOf(i));
  if i <> High(aryDemo) then
    begin
    ShowMessage('ERROR - File does NOT have correct array amount');
    Exit;
    end;

  FStm.ReadBuffer(aryDemo[0], SizeOf(aryDemo));
  finally
  FStm.Free;
  end;
if aryDemo[0].aSet = [odWrite, odClose] then
  ShowMessage(aryDemo[5].FixStr1);
* end;

Here I create the TFileStream and read one integer from the file, I test this integer to see if it is the same as the High Index of the array I want to load into. If it is, then I then Read the TFileStream for the array data. But PLEASE NOTICE, I use a different method to read this array, I do NOT have a FOR Loop that will read each TDemo record, , instead I read the entire array from the file at one time. With the line -
    FStm.ReadBuffer(aryDemo[0], SizeOf(aryDemo));
I do not need to FOR Loop an array from file, if it only contains Fixed size variables. I could have used the FOR Loop code -
    for i := 0 to High(aryDemo) do
          FStm.ReadBuffer(aryDemo[i], SizeOf(TDemo));
And I would have gotten the correct data out. . .

Also, I could have used different code to write the array to the file -
    FStm.WriteBuffer(aryDemo[0], SizeOf(aryDemo));
And would also have written the file susccesfully. I hope you can see that even though the variable aryDemo is defined as an array, it is just one continuous block of memory, the size of 6 TDemo records. So you can write or read this array from file in one operation. But if you have any changing size variables (String, PChar) in your record type, you can NOT write or read the array in one operation. . . . .

With Dymanic Arrays you can NOT use the SizeOf( ) function, instead you will need to calulate the "Size" of the memory block with math, you can multiply the Length of the array by the Size of the Record, like this -
    FStm.WriteBuffer(aryDemo[0], Length(aryDemo) * SizeOf(TDemo);

You should know enough now about TFileStream to experiment and try writting and reading your own files with several different fixed size variables in them. You should try making your own file write and read procedures and change the types of Fixed-Size variables and their order. Create your own Record type with Fixed-Size variables and write and read it in a file. Remember to use the same exact sequence of reading the variables as you used to write the file.

  File Extentions -
I will be using several different File Extentions in the code examples below, these file extentions DO NOT affect the way a file is read or used in code, they are a method to have some way to detect what data type the file may have in it. You should use a file extention that gives a "Hint" as to what the file data contains, , , like .BMP is used for a Bitmap file or .EXE is used for an Executable file. If your file will only be used by your programs, then I would use a file extention with more than 3 charaters, maybe 5 charaters, like - .aryIn - or - .input - so it may be something that is not used by other programs for their types of files. You should be familar with some of the commom file extentions, such as -
.TXT .DOC .BMP .WAV .EXE .SYS .ICO .JPG .HTM .MP3 .INI .INF .LOG
You should try and avoid using common file extentions like these on your custom file. I have seen many file writting instructions use the .DAT file extention, you may want to use this extention if you are not sure about file extentions and their use. . . HOWEVER, you should NOT use the .DAT extention if you use it as the registered file extention for your programs special file (double Click on your file to open your program).
Also many the common file extentions are just 3 characters (Left over from the old DOS days), but ALL 32 bit windows systems can have file extention of more than 3 characters, so you should really consider using 4 or 5 character extentions for your special files.




Using Non-Fixed Size Variables (like String) in Files

We have seen that you can use a Fixed Size variable with a TFileStream by setting the "Count" parameter to "SizeOf(Var)", but if you use this method with a Non-Fixed Size Variable, like a standard Delphi Long String, it will NOT work. If you create a FileStream1 and have the variable MyString as a String, then put this line in your code -

FileStream1.Write(MyString, SizeOf(MyString));
It will NOT write that String to File. First, the SizeOf(MyString) will always return the value of Four (4 bytes), no matter what the length of the String acually is. . . Next, using the code
FileStream1.Write(MyString
the MyString in the Buffer parameter will NOT get the MyString's text charaters, because it is not a "Fixed Size" variable, and the Delphi Compiler uses a different method of getting the "Value" data (text charaters for a string) for all Non-Fixed Size variables. The MyString variable will have a SizeOf(MyString) as 4 bytes, because it's just a reference to a Pointer (the SizeOf a Pointer is 4 bytes). And if you use the code
FileStream1.Write(MyString, SizeOf(MyString));
it will write the numeric (Integer) value of the Pointer that MyString represents, but it will not write any of the text that is in MyString. You will not need to know the methods of the Delphi Compiler to use Non-Fixed Size variables in a TFileStream, you will need to know that each Non-Fixed Size variable is just a reference to a Pointer, and you will need a different method to read and write Non-Fixed Size variables to a TFileStream, according to the Type of that variable.

Using Strings in a TFileStream
Since the SizeOf(MyString) will not return the number of Charaters in the MyString string, you will need to use the Length(MyString) function to get the number of charaters (Size) of the string, so we can use the Length( ) function in the Count parameter. Since the variable "MyString" is a reference to a Pointer, we can not use that to get the text in the MyString, but the variable MyString[1] is Not a reference to a Pointer, it is a Fixed-Size variable for a Char, the First character in the MyString string. But we need All of the text charaters in MyString, not just one of them. If you remember, the Buffer parameter in the TFileStream.Write( ) is UnTyped, so the compiler will not know that MyString[1] is for a Char variable, and will start to read bytes from that location in memory and continue reading bytes for the amount in the Count parameter. So it will read all of the Text characters out of MyString and write them to the FileStream. Since I will not use methods for file access errors, I will now switch to the ReadBuffer( ) and WriteBuffer( ) procedures. Look at the next code example that writes a String to file -
procedure WriteToFile2;
var
MyString: String;
FileStream1: TFileStream;
begin
MyString := 'this is a string with several words '+
            'in it to be in a file';
FileStream1 := TFileStream.Create('G:\MyString.string',
                 fmCreate or fmOpenWrite or fmShareDenyWrite);
try
  FileStream1.WriteBuffer(MyString[1], Length(MyString));
  finally
  FileStream1.Free;
  end;
end;
If you load "G:\MyString.str" file into NotePad you will see the text -

this is a string with several words in it to be in a file

in NotePad just like a normal text file. This file "G:\MyString.str" is a normal text file, just like what you would get if you saved the NotePad text to file.

Reading the file into a String - To read this string file into a string will require one additional step, since we will read the file into a string, we need the string that we read it into to be the correct length. You are used to the Compiler automatically setting the length (memory block size) for Delphi Strings. We need to Set the length of the string because the Compiler will not automatically do it for us in this FileStream Read operation. We can set the Length of the String to the Size of the FileStream with the SetLength procedure. The Size Property of a TStream will give you the number of bytes in that Stream, just what we need to set the Length of our string variable. Look at the next code example for reading a string from a file -
procedure ReadFromFile2;
  var
  FileStream1: TFileStream;
  MyString: String;
begin
FileStream1 := TFileStream.Create('G:\MyString.string',
                 fmOpenRead or fmShareDenyWrite);
try
  if FileStream1.Size = 0 then Exit;
  SetLength(MyString, FileStream1.Size);
  FileStream1.ReadBuffer(MyString[1], FileStream1.Size);
  finally
  FileStream1.Free;;
  end;
ShowMessage(MyString);
end;
Notice that I use
SetLength(MyString, FileStream1.Size);
to have MyString with enough memory space to receive all of the characters read into it from the file. If you do NOT set the Length for MyString in this example, then nothing will be written into the MyString string, because the MyString string will be empty (a pointer to nil), and because the Buffer Parametr is an UnTyped variable, the Read function will not throw an exception or a warning if the variable is empty (as nil) using the MyString[1] variable (also nil). So you need to remember that you must have a way to get the "Size" or "Length" of the String you are about to Read from file, and then Set the Length of your String variable to the Length of the string you are going to read from the file. In this example, the entire file is the string, so we can use the "Size" of the FileStream to set the Length of the string variable.

Another way to use the Characters in a String (as a Pointer)
In the Examples above I used the MyString[1] variable to write and read to the text characters in the MyString string. If you remember, I told you that the MyString string variable is a reference to a Pointer, so instead of the MyString[1] variable in the FileStream Write and Read, we can TypeCast the MyString to a Pointer Type, and use that. However, since the TFileStream Buffer parameter is an UnTyped variable, the compiler will NOT know that it is a Pointer type, so we will need to Dereference the Pointer type with a  ^  pointer operator. This tells the compiler to get the "Data" that the pointer is an address for, not the pointer value (a memory location). The following code shows an example of TypeCasting a String to a PChar and writing it to a TFileStream -
procedure WriteToFile2;
var
MyString: String;
FileStream1: TFileStream;
begin
MyString := 'this is a string with several words '+
            'in it to be in a file';

if @Mystring[1] = PChar(MyString) then
  Showmessage('Is the Same Memory Address');

FileStream1 := TFileStream.Create('G:\MyString.string',
                 fmCreate or fmOpenWrite or fmShareDenyWrite);
try
  FileStream1.WriteBuffer(PChar(MyString)^, Length(MyString));
  finally
  FileStream1.Free;
  end;
end;
The variable in the WriteBuffer procedure has been changed from
MyString[1]
  to
PChar(MyString)^
and will do exatly the same thing, you can use either method and the compiler will use them in the same way, they both work. I have put a test for the memory addresses -
if @Mystring[1] = PChar(MyString) then
  Showmessage('Is the Same Memory Address');
which will show the message because @Mystring[1] and PChar(MyString) are equal. You can use the   PChar(MyString)^,   Pointer(MyString)^,   or   MyString[1]   for the Buffer variable in the TFileStream Write and Read functions to access the text in a string. I like to use the MyString[1] style of coding and will use that in these examples.

File Data Segments
Writting and Reading More than One String to a FileStream

In the examples above, we wrote one string to file and read that single string from the file, but we will need a different method to put more than one string in a file and be able to get those strings out of the file. You could use the method that I showed you and write several strings to the FileStream, and then read all of that file into a single string by setting the length of the string to the FileStream Size. But you would not have the separate strings that you put into the file, you would have just One string with all of the strings in it. We could put a "Delimiter" into the FileStream (as in Comma delimited text) and separate the strings from the single file string by searching for each Delimiter in the large string and separating out the original strings, but this is a String operation and Not a FileStream operation. In order to get the separate file strings out of the multi-string file, we will need to record the length of each string into the file, so we can set the length of each string when we read the string out of the file. So each string in the file is a "File Data Segment", or a part of the file with the Data (bytes) for a single variable. We have used File Data Segments before, when we Wrote and Read several Fixed Size variables to a file. I did not need to talk about Data Segments there, because all of the variables were Fixed Size, and we knew the type of variables and used the SizeOf( ) function to know the number of bytes to write and read for that variable. But now we need to use several string variables that are not Fixed Length (Size of bytes), so we will need to write the Length of the string (Data Segment) to file before writting the string's text to file. In the next code example I use a Cardinal variable (Len1) to write the Length of each string to the FileStream, before we write the string. -
procedure WriteToFile2;
var
FileStream1: TFileStream;
StrOne, StrTwo, StrThree: String;
Len1: Cardinal;
begin
StrOne := 'First string in this file';
StrTwo := 'the Second file string';
StrThree := 'String number three of this file';

FileStream1 := TFileStream.Create('E:\Strings.msf',
                 fmCreate or fmOpenWrite or fmShareDenyWrite);
try
  Len1 := Length(StrOne);
  FileStream1.WriteBuffer(Len1, SizeOf(Len1));
  FileStream1.WriteBuffer(StrOne[1], Len1);

  Len1 := Length(StrTwo);
  FileStream1.WriteBuffer(Len1, SizeOf(Len1));
  FileStream1.WriteBuffer(StrTwo[1], Len1);

  Len1 := Length(StrThree);
  FileStream1.WriteBuffer(Len1, SizeOf(Len1));
  FileStream1.WriteBuffer(StrThree[1], Len1);
  finally
  FileStream1.Free;
  end;
end;
We get the Length of the String into Len1 and then write Len1 to the FileStream. The Buffer parameter of TFileStream.Write must be a Variable, so we can NOT use the function Length(StrOne) as the Buffer parameter. Next we write the string to the FileStream, just like we have done before. You must make sure that you write a Length number (Cardinal) before you write the string for Every string that you write.

In the next code example, I will read the three strings from the FileStream, you will need to read the Len1 variable to get the Data Segment (string) Length and then use the SetLength( ) function for each string before we read the file Data into the string. -
procedure ReadFromFile2;
var
  FileStream1: TFileStream;
  StrOne, StrTwo, StrThree: String;
  Len1: Cardinal;

begin
FileStream1 := TFileStream.Create('E:\Strings.msf',
                 fmOpenRead or fmShareDenyWrite);
try
  FileStream1.ReadBuffer(Len1, SizeOf(Len1));
  if Len1 > 2048 then Exit;
  SetLength(StrOne, Len1);
  FileStream1.ReadBuffer(StrOne[1], Len1);

  FileStream1.ReadBuffer(Len1, SizeOf(Len1));
  if Len1 > 2048 then Exit;
  SetLength(StrTwo, Len1);
  FileStream1.ReadBuffer(StrTwo[1], Len1);

  FileStream1.ReadBuffer(Len1, SizeOf(Len1));
  if Len1 > 2048 then Exit;
  SetLength(StrThree, Len1);
  FileStream1.ReadBuffer(StrThree[1], Len1);

  finally
  FileStream1.Free;;
  end;
ShowMessage(StrOne);
ShowMessage(StrTwo);
ShowMessage(StrThree);
end;
We can read all three strings out of this file, because we recorded the number of bytes (length of the string) for each Data Segment, so we could read that out of the file and know how many bytes to read into that variable. You should notice that I put a test for the Len1 variable after each Len1 file read,
if Len1 > 2048 then Exit;
You should consider doing this when you first are trying these file read methods, and you can have coding or sequence errors in your file write or read procedures, since the Buffer is an UnTyped variable, you may not get warnings about variable or syntax code errors, so if your file position is incorrect for a Len1 FileStream Read, you might get a Large value (incorrect read) in the Len1 variable, and try and set the length of the string to an incorrect amount.

This string file example uses three strings, and since we know how the file was written (using 3 strings, with the length of each string recorded as a Cardinal), we can read the file. If you need a method to write and read a file that does Not have a fixed number of strings, you can use Delphi's Dynamic Array, to hold your strings (or a TStringList) then write and read this array to the FileStream.

Writing and Reading Dynamic Arrays of Non-Fixed Size Variables
Delphi's Dynamic Array is a very useful because you can set it's Size with SetLength( ). In this next example, a Dynamic Array of String (arrayString) is used and it's Length is set to 4, but you can set it's length to another amount and the code will still work. This uses the same methods to write and read the strings as the last example, recording the length of the string as a cardinal value, but since the amount of Strings in the Array can change, we need to record the Number of elements in the Array. You get the number of strings in the Array with the Length function and then write this number as the first thing in the FileStream. Then use a FOR loop to write all of the strings and their length to the file.
procedure WriteToFile2;
var
FileStream1: TFileStream;
arrayString: Array of String;
Len1, c: Cardinal;
begin
SetLength(arrayString, 4);
arrayString[0] := 'First string in this Array';
arrayString[1] := 'the Second Array string';
arrayString[2] := 'String number three of this Array';
arrayString[3] := 'this is the fourth String';

FileStream1 := TFileStream.Create('E:\Array Str1.aryS4',
                 fmCreate or fmOpenWrite or fmShareDenyWrite);

try
  c := Length(arrayString);
  FileStream1.WriteBuffer(c, SizeOf(c));
  FOR c := 0 to High(arrayString) do
    begin
    Len1 := Length(arrayString[c]);
    FileStream1.WriteBuffer(Len1, SizeOf(Len1));
    if Len1 > 0 then
    FileStream1.WriteBuffer(arrayString[c][1], Len1);
    end;
  finally
  FileStream1.Free;
  end;
end;
This will write all of the strings in the arrayString to file, it is Important that you place the "Length" of the arrayString Array as the first thing in your file
c := Length(arrayString);
FileStream1.Write(c, SizeOf(c));
So when you Read this file you can set the "Length" of your array to hold all of the strings in the file. Here is the code to read this file -
procedure ReadFromFile2;
  var
  FileStream1: TFileStream;
  arrayString: Array of String;
  Len1, c: Cardinal;

begin
FileStream1 := TFileStream.Create('E:\Array Str1.aryS4',
                 fmOpenRead or fmShareDenyWrite);
try
  FileStream1.ReadBuffer(Len1, SizeOf(Len1));
  if (Len1 = 0) or (Len1 > 1000) then Exit;
  SetLength(arrayString, Len1);
  FOR c := 0 to Len1-1 do
    begin
    FileStream1.ReadBuffer(Len1, SizeOf(Len1));
    if Len1 > 2048 then Exit;
    SetLength(arrayString[c], Len1);
    FileStream1.ReadBuffer(arrayString[c][1], Len1);
    end;
  finally
  FileStream1.Free;;
  end;
ShowMessage(arrayString[0]);
ShowMessage(arrayString[High(arrayString)]);
end;
As you can see, the first thing that is Read from FileStream1 is the Len1 variable (Cardinal) and it is tested to see if it is zero or larger than 1000. If the value of Len1 passes this safety test, then the Length of the arrayString Array is set to the value of Len1. Now that we have enough room in our array for all of the strings, we can use a FOR loop to read the string length, and set the length of each string and then read the string's charaters from the file. You should notice that any NON-Fixed values (like the Length of the Array, or the Length of the Strings) need to be written to the File along with the Data that is in your variables. Lets look at some methods to use for saving another Non-Fixed size variable, a Bitmap, to a Multi-Data File.




Using TGraphic Classes in a Multi-Data FileStream

The TGraphic Classes like TBitmap and TIcon, need a different method to write and read them to a Multi-Data File than we used with the Strings. Just like the Strings would save the "Length" of the string to file, we will also need to save the "Size" of the TGraphic to the FileStream. There are several classes of TGraphic, including - TBitmap, TMetafile, TIcon, TJPEGImage, , , , All of these Graphic Classes have a "SaveToStream" method, which is what I will use to get the "Size" of the Graphic's Data Segment and write and read it to FileStream. I will be using a TBitmap for these examples, but you can subtitute any TGraphic that has the "SaveToStream" and "LoadFromStream" methods. You can also subtitute any VCL type that has the "SaveToStream" and "LoadFromStream" methods, such as TStrings, TStringList, TBlobField, and TCustomTreeView.

The TBitmap does NOT have a "Length" or "Size" property to get the amount of bytes that would be in that Bitmaps File size. Since we will need to record the Size in bytes for the File's Bitmap Data-Segment, we will need a method to find this "Size" value. It will be nessary to Create a TMemoryStream and then use the TBitmap's "SaveToStream" to write this Bitmap file into the MemoryStream. Once the Bitmap file is in the MemoryStream, we can use the TMemortStream "Size" property to get the number of bytes we will be writing to the FileStream for the Bitmap Data-Segment. And since the Bitmap file is now in the MemoryStream, we can just write this MemoryStream into the TFileStream. This first example will use a Dynamic Array of TBitmap, with 4 Bitmaps in the Array, just like the example above for the Array of Strings, we will record the Length of the Array as the first thing in the FileStream. Next we will use a FOR loop to go through all of the Bitmaps in the array. Unlike the example with strings, I will create a TMemoryStream and use the TBitmap's SaveToStream method to place the Bitmap file in the MemoryStream. Then in the FOR loop I can get the "Size" of the Bitmap's Data Segment from the Size property of the MemoryStream and write it to the FileStream.
procedure WriteToFile2;
var
FileStream1: TFileStream;
MemStream1: TMemoryStream;
aryBitmap: Array of TBitmap;
Len1, c: Cardinal;
begin
SetLength(aryBitmap, 4);
For c := 0 to High(aryBitmap) do
  begin
  {this FOR loop just creates the Bitmaps in the Array}
  aryBitmap[c] := TBitmap.Create;
  if c = 0 then
    aryBitmap[c].Canvas.Brush.Color := clAqua
    else
    aryBitmap[c].Canvas.Brush.Color := clYellow;
  aryBitmap[c].Width := 110+ (C*3);
  aryBitmap[c].Height := 86+ (C*3);
  aryBitmap[c].Canvas.TextOut(5,5, 'Bitmap '+IntToStr(c));
  end;
try

  FileStream1 := TFileStream.Create('E:\Array Bitmap.abt',
                 fmCreate or fmOpenWrite or fmShareDenyWrite);
  MemStream1 := TMemoryStream.Create;
  try
    Len1 := Length(aryBitmap);
    FileStream1.WriteBuffer(Len1, SizeOf(Len1));
    for c := 0 to High(aryBitmap) do
      begin
      aryBitmap[c].SaveToStream(MemStream1);
{save aryBitmap[c] to the Memory stream so we can find
out the Size of the Bitmap File}
      Len1 := MemStream1.Size;
{it is VERY important to get the size of bitmap bytes in Len1,
or you will not be able to get the Bitmap out of the file}
      FileStream1.WriteBuffer(Len1, SizeOf(Len1));
 {remember that the MemStream1 position is now at the END of
 the Stream and we need to SaveToStream From the Beginning of
 the MemStream1, so we need to set the Position to zero}
      MemStream1.Position := 0;
      MemStream1.SaveToStream(FileStream1);
      MemStream1.Clear;
{IMPORTANT, to must empty the MemStream1 with Clear, or the next
 SaveToStream will add the bitmap to the ones already there}
      end;
    finally
    FileStream1.Free;
    MemStream1.Free;
    end;
  finally
  for c := 0 to High(aryBitmap) do
    aryBitmap[c].Free;
{you need to Free all TGraphics that you create}
  end;
end;
You need to look at the way each bitmap is saved to the Memory Stream and then how the MemStream1 is used to get the Size of the Data-Segment. The Len1 is set to the "Size" of the MemStream1 and written to the FileStream1. Now we will use the CopyFrom TStream method to put the Bitmap Data in the FileStream, however, remember that all TStream read and write methods advance the Stream Position to where that operation ended. So after the   aryBitmap[c].SaveToStream(MemStream1);   , , the MemStream1 Position is at the End of the Stream. And if we use the   MemStream1.SaveToStream(FileStream1); , , Nothing will be written to the FileStream1, because the stream Position is at the end of the stream. First we need to set the MemStream1 Position to the begining of that Stream with Position := 0; , , Also we will be using the Same MemStream1 for all of the Bitmaps, so we need to Empty that Stream before we put the next Bitmap in it. I use the TStream "Clear" method to do that.

the next example Reads the Bitmaps out of the file into an Array of TBitmap -
procedure ReadFromFile2;
var
  FileStream1: TFileStream;
  MemStream1: TMemoryStream;
  aryBitmap: Array of TBitmap;
  Len1, c: Cardinal;
begin
FileStream1 := TFileStream.Create('E:\Array Bitmap.abt',
                 fmOpenRead or fmShareDenyWrite);
MemStream1 := TMemoryStream.Create;
try
  FileStream1.ReadBuffer(Len1, SizeOf(Len1));
  if (Len1 = 0) or (Len1 > 1024) then Exit;
  SetLength(aryBitmap, Len1);
  for c := 0 to Len1-1 do
    begin
    aryBitmap[c] := TBitmap.Create;
    FileStream1.ReadBuffer(Len1, SizeOf(Len1));
    MemStream1.CopyFrom(FileStream1, Len1);
{copy the MemStream1 from the FileStream1, you need to do
this because any TGraphics LoadFromStream will need a
stream that ONLY has that graphic in it, remember that the
MemStream1 position is now at the END of the Stream, and it
needs to be at the begining}
    MemStream1.Position := 0;
    aryBitmap[c].LoadFromStream(MemStream1);
{If your file read Position was incorrect for the 
MemStream1.CopyFrom then you will get an Exception here in 
this TBitmap LoadFromStream for an incompatable graphic type}
    MemStream1.Clear;
    end;

  finally
  FileStream1.Free;
  MemStream1.Free;
  end;
Form1.Canvas.Draw(7, 7, aryBitmap[0]);
Form1.Canvas.Draw(207, 7, aryBitmap[High(aryBitmap)]);
For c := 0 to High(aryBitmap) do
  aryBitmap[c].Free;
end;
You should experiment and create your own Muti-Data TGraphics Files, try and change the TBitmap Array to a TJPEGImage Array and create your Jpegs and save and read them from a file.

You can also "move through" a muti-segment file like the bitmap file above and not read any of the bitmap image data. In all of the file reading above, I have read ALL of the data segments into a variable until it gets to the end of the file. But if you need to get only ONE bitmap from the file above, you can read the bitmap data segment size and then reset the file read position to the next segment size integer. In For Loop code below, the FileStream1 is created for reading and uses the same code in the ReadFromFile2 procedure above, Except in the "For Loop". To read just One Bitmap use this - For Loop - code, like this -

for c := 0 to Len1-1 do
  begin
  FileStream1.ReadBuffer(Len1, SizeOf(Len1));
  if i = 2 then // get the third bitmap
    begin
    // code here reads out only one bitmap
    Bmp1 := TBitmap.Create;
    MemStream1.CopyFrom(FileStream1, Len1);
    MemStream1.Position := 0;
    Bmp1.LoadFromStream(MemStream1);
    Break; // end loop once bitmap is read
    end;
{IMPORTANT, you must advance the file position, from
 it's current place, to the amount of Len1, the
 data segment size}
  FileStream1.Seek(Len1, soFromCurrent);
  end;
This gives you a way to access a file data segment without reading all of the data in a file. But it may be better to have a file header with more information in it, like the data segment position and size, so I'll talk more about file headers next.



File Headers
In the last code examples, the first thing written and read for the file was the number of elements in the array that was used for that file. This Cardinal value for the "Size" of the Array is a "File Header", because it contains data to apply to the entire file, unlike the "Len1" string length or bitmap size data, that was only used for a single Data-Segment. File Headers are an important and useful method to accomplish several things when creating your own "Special" Multi-Data files. Most of the common file types, like .BMP, .WAV, .EXE, .DOC and many others have File Headers to hold information about the file, neccesary to read and use the file correctly. Text files as viewed in NotePad, do NOT have a File Header, because they do not need a file header, since all of the data segments are one byte. The information in a File Header will vary greatly, and depend on the Data in the file, and what information the file reader needs, to correctly read the file.

File ID - - A Very Important File Addition
Many of these File Headers will begin with a "File Type" identifier and a Version number (if the file type can have more than one version). For instance the Bitmap file .BMP will always have the ASCII characters 'B' and 'M' as the first two bytes in the file, as a File Type identifier (no version info in a Bitmap file, only one version). So when a bitmap file reader starts to read the file it will check and see if the first 2 bytes are 66 (for B) and 77 (for M), and only continue to read the file if these 2 bytes are correct. I ALWAYS place a file identifier as the first thing in any custom Multi-Data file that I create. Your File ID should be at least 2 bytes, I ususlly use an Integer (4 bytes) as a File identifier and version number. I will write this ID number as the first thing in the file, and then when I begin reading this file, I check this ID number, and if it does Not match, I will Not load the file. You must consider that there may be files with the same file extention as the file type you want to read. And these other files will NOT have the data in them you will try and read, so you need a File ID for safety. The code might look like this -

var
FileStream1 : TFileStream;
ID_ver: Integer;

begin
ID_ver := 73485201;
FileStream1 := TFileStream.Create(FileName, fmCreate or 
                  fmOpenWrite or fmShareDenyWrite);
try
  FileStream1.WriteBuffer(ID_ver, SizeOf(ID_ver));
The value of the ID_ver can be any number, I just type in more than 4 digits, and I use the last 2 digits (01 in this case) as the Version Number. I will change the last 2 digits in the ID_ver, if I change anything in the file write method (change of sequence, change of the variables written, change of a variable type, change of a Record structure), I would change it to 73485202 in this case (version 2), with more file write changes I would go to version three, 73485203. One of the main reasons to have a File ID is to prevent your program from trying to read a file that does NOT have the correct data it. So when you open a file for Reading, the first thing you check is the ID number. And Exit the procedure if it is Not the correct ID and version -
var
FileStream1 : TFileStream;
ID_ver: Integer;

begin
ID_ver := 0;
FileStream1 := TFileStream.Create(FileName,fmOpenRead or 
                                  fmShareDenyWrite);
try
FileStream1.ReadBuffer(ID_ver, SizeOf(ID_ver));
if ID_ver <> 73485201 then
  begin
  ShowMessage('ERROR - This is NOT a valid file for this'+
               ' version, and will NOT be loaded');
  Exit;
  end;
You may want to use some ASCII text characters for your File ID, if you want your file to be more easily identified in a HexEditor, for instance, the .GIF files have the first 5 bytes of those files as the ASCII text characters "GIF87" or "GIF89", giving their File ID and version number. So you could do it like this -
var
FileStream1 : TFileStream;
cID_ver: Array[0..4] of Char;

begin
cID_ver := 'FILE1';
FileStream1 := TFileStream.Create(FileName, fmCreate or 
                  fmOpenWrite or fmShareDenyWrite);
try
  FileStream1.WriteBuffer(cID_ver, SizeOf(cID_ver));




Other File Header Data
So far the only File Header Data (information) we have included in a file, is the number of elements in the Array (which is the number of Data segments in the file). You can include any data in the file Header that you will need to read and use the file. You can use the File Write methods that I have already shown, to put information about the type, size, number of elements, a description, and ALL the information that is needed for that type of file to be read. As you begin to build and read your special file, you will find that you must have certian information, or you will not be able to read the file, so you include this information in your header.

Access a Single Data Segment
I will now create a multi-Bitmap File that has the Data Segment Positions as part of the File Header, allowing easier access to individual Data Segments. In the previous examples all of the file reads were done by reading the entire file, and loading all of the Data Segments at the same time. If we place the Data Segment's file Positions in the File Header we can read just One Data Segment out of the file (a Bitmap in this case). I will make a Record that has some information about the Bitmap (width and height) And has it's file position and data size in it, and place one Record for each Bitmap in the file header. The record will have 4 elements, all of fixed-size variables (fixed-size makes it easier) -

TBmp_Rec = Record
   FilePos, bmp_Size: Cardinal;
   width, height: Integer;
   Description: String[15];
   end;
The FilePos and bmp_Size are the two things that are needed to find and read the Bitmap Data Segment, I have included the width and height as Extra data about each bitmap that can be used to sort a list of the bitmaps in a file. Each Description of the Bitmap will be read into a TListBox and the user can choose which Bitmap to load from this List. The File Header for this File will include a file Identification number, the number of Bitmaps in the file, and one TBmp_Rec for each Bitmap in the file. First I will write the ID number to the file, then the number of bitmaps (five bitmaps in this example), and as part of the file header I will write 5 TBmp_Rec, after setting the width and height of the record to the width and height of the bitmap. I set the FilePos and bmp_Size to zero, because the file Position and size are not known, later when a Bitmap is placed in a MemoryStream and it's size can be determined, I will use the TFileStream Position to set the FileStream1 to the location of the TBmp_Rec FilePos and bmp_Size and write the values of the file Position and Size to the file. I will use a Mathamatical formular to calculate the file Position of the TBmp_Rec index for that Bitmap sequence number, shown below -
FileStream1.Position := SizeOf(ID_ver)+SizeOf(Len1)+
           (c * SizeOf(Bmp_Rec1));
You will need to add together all of the byte sizes of what was written to the file in front of the file header segment that you need to write to. The first things written to this file are the ID_ver and Len1 so I get the SizeOf( ) for those, which will be 4 bytes each, or 8 bytes for both, then using (c * SizeOf(Bmp_Rec1)) will get the number of bytes to add for the index (c) of that bitmap's TBmp_Rec. I will add the SizeOf( ) for the first two elements of the TBmp_Rec, width and height, which will be 4 bytes each or 8 bytes for both. I could have written that as -
FileStream1.Position := 8 + (c * SizeOf(Bmp_Rec1));
But I wanted to show you how to Get the byte count for a TBmp_Rec index file Position. When the file Position has been set to the location for the index number of the TBmp_Rec FilePos, then the FilePos is written to file. Next the bmp_Size is written to the file as the value in Len1 which is the Size of the BmpStream, a MemoryStream loaded with the Bitmap file. Now that the FilePos and bmp_Size are recorded to the file, I will move the file Position back to the end of the file which was recorded in the FilePos variable. Now I write the BmpStream to the file.
You can see this in the next example -
procedure WriteToFile3;
type
 TBmp_Rec = Record
   FilePos, bmp_Size: Cardinal;
   width, height: Integer;
   Description: String[15];
{this record will be written as part of the File Header
only the FilePos and bmpSize are required for file write
and read, the width and height are just extra Bmp info
and the Description is for the ListBox}
   end;

var
FileStream1 : TFileStream;
BmpStream: TMemoryStream;
ID_ver: Integer;
Bmp_Rec1: TBMP_rec;
aryBitmap: Array of TBitmap;
Len1, c, FilePos: Cardinal;
FileName: String;
begin
ID_ver := 5211801;
FileName := 'E:\Bmp Rec1.brf';
SetLength(aryBitmap, 5);
FOR c := 0 to High(aryBitmap) do
  begin
{this FOR loop just creates the Bitmaps.
Normally you would have the user load his own bitmaps from file}
  aryBitmap[c] := TBitmap.Create;
  aryBitmap[c].PixelFormat := pf8Bit;
  if c = 0 then
  aryBitmap[c].Canvas.Brush.Color := clAqua
  else
  aryBitmap[c].Canvas.Brush.Color := clYellow;
  aryBitmap[c].Width := 110+ (C*3);
  aryBitmap[c].Height := 86+ (C*3);
  aryBitmap[c].Canvas.TextOut(5,5, 'Bitmap '+IntToStr(c+1));
  end;


try
  FileStream1 := TFileStream.Create(FileName,
                 fmCreate or fmOpenWrite or fmShareDenyWrite);
  BmpStream := TMemoryStream.Create;
  try
    FileStream1.WriteBuffer(ID_ver, SizeOf(ID_ver));
{IMPORTANT, you should write the file ID as the
first thing in the File}

    Len1 := Length(aryBitmap);
    FileStream1.WriteBuffer(Len1, SizeOf(Len1));
{record the number of Bitmaps in this File which is the
Length(aryBitmap)}

    for c := 0 to High(aryBitmap) do
      begin
{as part of a File Header, I will write ONE TBmp_Rec for
each Bitmap in this file}
      Bmp_Rec1.FilePos := 0;
      Bmp_Rec1.bmp_Size := 0;
{Notice that I set the values of FilePos and bmp_Size
to zero, at this point of the file write, the FilePos
and bmp_Size are not availible, they will be written
later, when we are writing each bitmap and know it's
position and size}
      Bmp_Rec1.width := aryBitmap[c].Width;
      Bmp_Rec1.height := aryBitmap[c].Height;
{the width and height fields are not nessary for file
writting and reading, they are here just to show you
that you can include some extra info in the TBmp_Rec}
      Bmp_Rec1.Description := IntToStr(c+1)+' bitmap';
{the Bmp_Rec1.Description will be placed in a ListBox
when this file is Read}
      FileStream1.WriteBuffer(Bmp_Rec1, SizeOf(Bmp_Rec1));
      end;

{after the above FOR loop, we are finished writting the
File Header, and we will now write the Data Segments}

    for c := 0 to High(aryBitmap) do
      begin
      FilePos := FileStream1.Position;
{IMPORTANT, you must get the current File Position, to write
it to File and to come Back to this Position to write
the Data Segment (Bitmap)}
      FileStream1.Position := SizeOf(ID_ver)+SizeOf(Len1)+
           (c * SizeOf(Bmp_Rec1));
{The file Position is Set above with a Mathamatical formular,
we need to go to the Bmp_Rec1 for the corresponding Bitmap
and set the 2 fields of the Record for the FilePos and bmp_Size,
first we add the size of the 2 Cardinal Values
ID_ver and Len1, which are the first 2 things in this file.
Next the Bmp_Rec1 position is calculated by multiplying the
SizeOf(Bmp_Rec1) by the record index c }

      FileStream1.WriteBuffer(FilePos, SizeOf(FilePos));
{the Write above will place this Cardinal, FilePos, in the
Bmp_Rec1.FilePos for that index Record}

      aryBitmap[c].SaveToStream(BmpStream);
{save aryBitmap[c] to the Memory stream so we can find
out the Size of the Bitmap File}
      Len1 := BmpStream.Size;
{it is VERY important to get the size of bitmap bytes in Len1,
and save to file or you will not be able to get the image out of the file}
      FileStream1.Write(Len1, SizeOf(Len1));
{the Write above will place this Cardinal, Len1 in the
Bmp_Rec1.bmp_Size for that index Record}

      FileStream1.Position := FilePos;
{IMPORTANT, you MUST reset the file Position back to FilePos,
so the next Write will be at the end of the file}

{remember that the BmpStream position is now at the END of the Stream
  and we need to SaveToStream From the Beginning of the BmpStream}
      BmpStream.Position := 0;
      BmpStream.SaveToStream(FileStream1);
      BmpStream.Clear;
      end;
    finally
    FileStream1.Free;
    BmpStream.Free;
    end;
  finally
  for c := 0 to High(aryBitmap) do
    aryBitmap[c].Free;
  end;
end;
This file will have a File Header that can be read to get file segment information for each bitmap (data), without reading any of the bitmap data segments, and the TBmp_Rec Description be can be placed into a TListBox for each Bitmap so the user can choose from the list box, which bitmap to get out of this file. Although this takes a little more code work to build the header, it is a far more efficient way of reading a file if you do not need to get all of the data in the file. You really should try and use this kind of file method if you want to have any type of Random Access of data segments in a single file.

The next code will open this file for Reading, and read all of the TBmp_Rec in the file Header, placing it's Description into a TListBox Item. No Bitmaps are loaded, and no Bitmap Data is read from the file, the only things that is read from the file are the TBmp_Rec records in the header.
procedure ReadToListBox;
type
 TBmp_Rec = Record
   FilePos, bmp_Size: Cardinal;
   width, height: Integer;
   Description: String[15];
   end;

var
FileStream1 : TFileStream;
ID_ver: Integer;
Bmp_Rec1: TBmp_Rec;
Len1, c: Cardinal;
FileName: String;
begin
FileName := 'E:\Bmp Rec1.brf';
if Not FileExists(FileName) then
  begin
  ShowMessage('File does not Exist, can not load');
  Exit;
  end;

FileStream1 := TFileStream.Create(FileName,
                      fmOpenRead or fmShareDenyWrite);
try
  FileStream1.ReadBuffer(ID_ver, SizeOf(Integer));
  if ID_ver <> 5211801 then
    begin
{ALWAYS check your ID, if you try to load a file with 
incorrect data BAD THINGS can happen}
    ShowMessage('This is NOT a valid file for this version,'+
                 'can not load');
    Exit;
    end;

  FileStream1.ReadBuffer(Len1, SizeOf(Len1));
  if (Len1 = 0) or (Len1 > 300) then Exit;
{you can increase or leave out the test
above, after you get this file access working}
  ListBox1.Clear;
  FOR c := 0 to Len1-1 do
    begin
    FileStream1.ReadBuffer(Bmp_Rec1, SizeOf(Bmp_Rec1));
{read each TBmp_Rec into the Bmp_Rec1}
    ListBox1.Items.Add(Bmp_Rec1.Description+ 
                 '  width '+IntToStr(Bmp_Rec1.width));
{now add the Discription and width to the ListBox Items}
    end;

  finally
  FileStream1.Free;
  end;
ListBox1.ItemIndex := 0;
end;
This procedure will read the File Header and get a Description from each of the TBmp_Rec records in the header. It will add this description and the width of the bitmap to ListBox1, so the user can choose the bitmap to load from ListBox1.




In the next Code the ListBox1 ItemIndex is used to Load Only One Bitmap from this muti-Bitmap file. You will see that the ListBox1 ItemIndex is used to calculate to file Position of the TBmp_Rec in the file header for that Bitmap index number, with this code -

FileStream1.Position := 8 + (ListBox1.ItemIndex * SizeOf(Bmp_Rec1));
The number 8, is the size in bytes of the first 2 elements of this file header, the ID_ver and Len1, then (ListBox1.ItemIndex * SizeOf(Bmp_Rec1)) will get the byte offset of the TBmp_Rec for that index number, and the file Position is set to this value. After the Bmp_Rec1 is read from this file position, the File Position is set to the value in Bmp_Rec1.FilePos with
FileStream1.Position := Bmp_Rec1.FilePos;
And then the Bitmap Data Segment is copied to the BmpStream and loaded into Bmp1. So we can load only one Bitmap from this file, no matter what position it is in the file, we do not have to load the entire file to use a single Data Segment.
Look at how this is done in the next example -
procedure ReadOneBMP;
type
 TBmp_Rec = Record
   FilePos, bmp_Size: Cardinal;
   width, height: Integer;
   Description: String[15];
   end;

var
FileStream1 : TFileStream;
BmpStream: TMemoryStream;
ID_ver: Integer;
Bmp_Rec1: TBMP_rec;
Bmp1: TBitmap;
Len1, c: Cardinal;
FileName: String;
begin
if ListBox1.ItemIndex < 0 then Exit;
{test the ItemIndex}
FileName := 'E:\Bmp Rec1.brf';
if Not FileExists(FileName) then
  begin
  ShowMessage('File does not Exist, can not load');
  Exit;
  end;

FileStream1 := TFileStream.Create(FileName, 
                    fmOpenRead or fmShareDenyWrite);
BmpStream := TMemoryStream.Create;
try
  FileStream1.Read(ID_ver, SizeOf(Integer));
  if ID_ver <> 5211801 then
    begin
{ALWAYS check your file ID}
    ShowMessage('This is NOT a valid file for this '+
                 'version, can not load');
    Exit;
    end;

  FileStream1.ReadBuffer(Len1, SizeOf(Len1));
  if (Len1 = 0) or (Len1 > 300) or 
      (ListBox1.ItemIndex > Len1-1) then Exit;
{make sure ItemIndex is in the file}
  FileStream1.Position := 8 + (ListBox1.ItemIndex * SizeOf(Bmp_Rec1));
{calculate the TBmp_Rec position for the index by 
adding 8 (ID_ver and Len1) and multiplying 
the index by the SizeOf(Bmp_Rec1)}
  FileStream1.ReadBuffer(Bmp_Rec1, SizeOf(Bmp_Rec1));
{Read the Bmp_Rec1 from the index Position}
  if (Bmp_Rec1.FilePos <> 0) and 
      (Bmp_Rec1.FilePos < FileStream1.Size) then
    begin
    FileStream1.Position := Bmp_Rec1.FilePos;
{set the file Position to the byte that is the 
begining of the Bitmap file Data Segment}
    Bmp1 := TBitmap.Create;
    try
      BmpStream.CopyFrom(FileStream1, Bmp_Rec1.bmp_Size);
{copy the BmpStream from the FileStream, 
remember that the Bmp_Rec1.bmp_Size has the number of bytes 
that you need to copy, the BmpStream position is now at 
the END of the Stream so we need to set it to zero}
      BmpStream.Position := 0;
      Bmp1.LoadFromStream(BmpStream);
      Form1.Canvas.Draw(6,6, Bmp1);
      finally
      Bmp1.Free;
      end;
    end;

  finally
  FileStream1.Free;
  BmpStream.Free;
  end;
end;
When this procedure is called (with a button click or ListBox change selection), the ItemIndex of ListBox1 is used to read just ONE Bitmap from our Multi-Bitmap file. You should experiment and change this Multi-Bitmap file, you might change the array of TBitmap, to an Array of TJPEGImage. Also you might add info variables to your TBmp_Rec record type. You should also Add some information to your file header, maybe a "Text" description or Date. You could also change this Muti_Data file, so it can save and load more than One Type of TGraphic in a single file. However, with variable size data segments (as used above), most of the time, you will NOT be able to replace any data segment, or change the content of your data, without rewritting the entire file. You should also try this file method with records of fixed-size variables as the data segments. Then with fixed-size data, you can replace or change a record (data) in a single segment, without writting the entire file. Just set your file position to that data and rewrite it.

Please notice that by using a file header with the file position and data segment size in it, you can then get a single data segment's position and size, and load only a single Bitmap. This does not use a "Search" the whole file for a data segment, method for finding a single bitmap and loading it. I have seen some others use muti-data file retrival methods that place a "File Marker" in front of a data segment, and then search through the whole file for this File Marker, to locate the data segment. This search method can work, but is an extreamly Non-Efficient way to do data segment positioning. Beleive me, in your Email program, they do Not search through all of the emails in it's data base to find one email to read. A file header with data segment position is the basic building method for many multi data files, and is the idea used for most (maybe all) Data-Base file storage.

Using Data Segment IDs to Separate Data

If you need to create a file with several different types of data in it and you want have a different number of segments in each file, you can use a "Data Segment ID" method. Sometimes you will need to have a more flexible way to write data segments to the file, so you can have a different order and amount of data segments in each file you create. You can use a Data Segment ID number, to identify what type of Data (TRect, TBitmap, String, TMyRec) the following data segment contains, then when you are reading that file you would read the segment ID number and execute code nessary for that type of Data. This also allows you to place your data segments into the file in ANY order, and in ANY amount. Each file will be different in the number and types of data segments it contains.

Using Data Segments Identifiers
Here is some code that will write a file with Strings, Bitmaps and Icons in it. There can be any number of Strings, Bitmaps, and Icons in this file, and they can be in any order in this file. I use a File ID and Version number as a File header. There is a variable segID which I will use as the data segment Identifier I have it as a Word type, but you could use an Integer or Cardinal type. You could also use and array of Char for a segment ID, like 'Strg' or 'ICON', if you want to read it in a Hex Editor. I will use Hex expressions to designate values for this segID number, if you use HEX, then you can see what each Byte value is in your number. But you can use decimal expressions, it makes no difference to the code. The value I use for a "String" ID is $FCC0, the first Byte of this Word value is "FC" and the second byte is "C0". The value I use for a "Bitmap" ID is $FCC1, and for an Icon ID is $FCC2. Although I do not use or test the first byte "FC" in the code below, if you have complex data types with sub-types you could set the first byte "FC" to the Type and the second byte "C0" to the sub-type. Although you could use decimal expressions, it is very difficult (for me) to see the "Byte" relationships in decimal. And since file writing and reading is in a "Byte", it may be useful to know what the byte values are in your Identifiers.

There are several many different ways to test for the end of a file in a LOOP file read, which I will use in the read code for this file. The way I use to test for the end of the file is to place a segment ID with the "End of File" number and when this number is read, I exit the read LOOP. The End of File number I use here is $EEEE.

Your filestream writting will depend on the systems ability to read and write to the disk, a common disk write error is filling up a disk so there is no longer any free space to write data to. As I have said before, I might use the WriteBuffer( ), instead of the Write( ) method, so any file write error will exception out of the code progression. In the code below, I have included a BadWrite function and switched to the Write( ) function, to catch errors in file writting. Look at the code for BadWrite below. You can see that this function takes the filestream "Write" parameters and will use the filestream "Write" function and test it's results to see if all of the bytes were written. If the bytes written are incorrect, then it shows an error message and returns False, so the code progression will stop and exit. Also notice that there is a IsBad boolean variable set to "True" if there is a Bad write. Look in the last finally block of code, you will see a test for the IsBad variable, and if it is True, then this file with a write error will be Deleted. You should always make some effort to Delete a File that is Not properly written to disk.

I will put one bitmap in this file, using the methods I have used before with a TMemoryStream. The TMemoryStream SaveToStream procedure will throw an exception if there is a file write error, so I use a Try, except block of code so I can set the IsBad to True, and I just use raise; to raise that exception.
Here is the code for writting a file with Data Segment ID's -

procedure WriteSegIDFile;
var
MultiFile: TFileStream;
MemStream1: TMemoryStream;
FileName, Str1: String;
Bmp1: TBitmap;
segID: Word;
Len1: Cardinal;
verID: Integer;
IsBad: Boolean;

  function BadWrite(const Data1; Count1, ErrorNum: Integer): Boolean;
  begin
  {when writing to a file, you should have a way to test and exit your
  code if there is a file write error, like not enough disk space.
  this function tests the File write and if it does not write the full
  Count1 bytes, it will result in a False}
  Result := True;
  if MultiFile.Write(Data1, Count1) = Count1 then
    Result := False else
      begin
      Str1 := SysErrorMessage(GetLastError);
      IsBad := True; // set IsBad to True, to delete the file in the finally
      FreeAndNil(Bmp1); // Make sure you free all of the objects you created
      MessageBox(Form1.Handle, PChar('ERROR '+IntToStr(ErrorNum)+
              ' - Unable to complete the File Write, Disk may be Full'#10+Str1),
              'ERROR, INCOMPLETE FILE WRITE', MB_OK or MB_ICONERROR);
      end;
  end;

begin
IsBad := False;
 {the IsBad will be tested in the finally code, and if IsBad is True,
the Bad file will be deleted}
Str1 := 'This is File Data Segmemt string number one';
Bmp1 := TBitmap.Create;
Bmp1.Width := 300;
Bmp1.Height := 270;
Bmp1.Canvas.TextOut(6,25, 'Bitmap ONE data Segment');

FileName := 'N:\many.DataSeg';
MultiFile := TFileStream.Create(FileName, fmCreate or fmOpenWrite or fmShareDenyWrite);
MemStream1 := TMemoryStream.Create;
{I do Not use a LOOP to write this file, I just do ONE data segment at a time}
try
  verID := 7138201;  // File Type and version ID
  if BadWrite(verID, SizeOf(Integer), 1) then  Exit;
  {BadWrite will return True if all of the Bytes in Count1 can NOT be
  written to the file, so you Exit this procedure, and the finally code
  will be executed, where the file will be deleted}

  segID := $FCC0; // $FCC0 is the segment ID for a String
  if BadWrite(segID, SizeOf(segID), 2) then  Exit;
  Len1 := Length(Str1);
  if BadWrite(Len1, SizeOf(Len1), 3) then  Exit;
  if Len1 > 0 then
    if BadWrite(Str1[1], Len1, 4) then  Exit;

{the next string write is commented Out of this code, but you can remove the
comments and place this into the code, but you WILL NOT need to change the
code in your Read procedure, because as long as you place the correct segID
before your data segment, you can write as many data segments as you want}

  {Str1 := 'Commented Out string write';
  segID := $FCC0;
  if BadWrite(segID, SizeOf(segID), 2) then  Exit;
  Len1 := Length(Str1);
  if BadWrite(Len1, SizeOf(Len1), 3) then  Exit;
  if Len1 > 0 then
    if BadWrite(Str1[1], Len1, 4) then  Exit;}

  segID := $FCC1; // $FCC1 is the segment ID for a Bitmap
  if BadWrite(segID, SizeOf(segID), 5) then  Exit;
  Bmp1.SaveToStream(MemStream1);
  Len1 := MemStream1.Size;
  if BadWrite(Len1, SizeOf(Len1), 6) then  Exit;
  MemStream1.Position := 0;
  try
  {SaveToStream error will throw an exception}
    MemStream1.SaveToStream(MultiFile);
    except
    FreeAndNil(Bmp1);
    IsBad := True;
    raise;
    end;
  MemStream1.Clear;
  FreeAndNil(Bmp1);

  Str1 := 'String number TWO';
  segID := $FCC0;
  if BadWrite(segID, SizeOf(segID), 7) then  Exit;
  Len1 := Length(Str1);
  if BadWrite(Len1, SizeOf(Len1), 8) then  Exit;
  if Len1 > 0 then
    if BadWrite(Str1[1], Len1, 9) then  Exit;

  segID := $FCC2; // $FCC2 is the segment ID for an Icon
  if BadWrite(segID, SizeOf(segID), 10) then  Exit;
  Application.Icon.SaveToStream(MemStream1);
  Len1 := MemStream1.Size;
  if BadWrite(Len1, SizeOf(Len1), 11) then  Exit;
  MemStream1.Position := 0;
  try
    MemStream1.SaveToStream(MultiFile);
    except
    IsBad := True;
    raise;
    end;
  MemStream1.Clear;

  segID := $FCC2;
  if BadWrite(segID, SizeOf(segID), 12) then  Exit;
  MainForm1.Icon.SaveToStream(MemStream1);
  Len1 := MemStream1.Size;
  if BadWrite(Len1, SizeOf(Len1), 13) then  Exit;
  MemStream1.Position := 0;
  try
  MemStream1.SaveToStream(MultiFile);
  except
    IsBad := True;
    raise;
    end;
  MemStream1.Clear;

  Str1 := 'Last data Segment';
  segID := $FCC0;
  if BadWrite(segID, SizeOf(segID), 14) then  Exit;
  Len1 := Length(Str1);
  if BadWrite(Len1, SizeOf(Len1), 15) then  Exit;
  if Len1 > 0 then
    if BadWrite(Str1[1], Len1, 16) then  Exit;

  {IMPORTANT
  you must put a Marker to indicate the End of the File, because
  you will Read this file in a LOOP and this segID of $EEEE will
  end the LOOP}
  segID := $EEEE;
  if BadWrite(segID, SizeOf(segID), 17) then  Exit;
// write $EEEE a second time for safty
  if BadWrite(segID, SizeOf(segID), 17) then  Exit;

  finally
  FreeAndNil(MultiFile);
  FreeAndNil(MemStream1);
  {if there is a File Write error, IsBad will be set to True,
   and this File will be Deleted}
  if IsBad then
  DeleteFile(FileName);
  end;
end;
This code for file writting, will check for file write errors and Delete the file if there are any. You should try and experiment by adding additional Data Segments to this file, and then try and add "Other" types of data segments, maybe some kind of record type. This shows you a method to "Identify" the kind of data in a data segment with a number, but you will need to make your own methods for the file writting that your data file needs to store your special data for the application you are programming.


Reading a Data Segment ID File
The methods used to read this file are similar to what was used to write the file. In this file read code there is a BadRead function, because the system may Not be able to read all of the file or the file may be corrupted or cut off. So BadRead will test the file read for the amount of bytes read from the file, and set a IsBad Boolean variable to True. The IsBad variable is tested in the finally block of code so all of the Bitmaps and Icons can be Freed. Unlike the write procedure, this read procedure uses a Loop to keep reading the file until the End of File Segment ID $EEEE comes up. Since there can be any number of data segments, I use 3 dynamic arrays, one for strings, one for bitmaps, and one for icons. In the while loop, the SegID is tested and code is executed to read a string, a bitmap or an icon. It is important to always Free bitmaps so in the finally code block, there is a test of IsBad and all of the bitmaps and Icons are Freed.

procedure ReadSegIDFile;
var
MultiFile: TFileStream;
MemStream1: TMemoryStream;
aryStr: Array of String;
aryBmp: Array of TBitmap;
aryIcon: Array of TIcon;
// Arrays above will add a member for each String, Bmp or Icon in the file
segID: Word;
Len1: Integer;
verID: Integer;
IsBad: Boolean;

  function BadRead(var Data1; Count1, ErrorNum1: Integer): Boolean;
  begin
  {This BadRead function will will show an Error Message if the system
  can NOT read all of the bytes from a file, the IsBad variable set to
  True, so all of the Bitmaps and Icons will be freed in the finally}
  Result := True;
  if MultiFile.Read(Data1, Count1) = Count1 then
    Result := False else
      begin
      IsBad := True;  // IsBad as True will Free all Bitmaps and Icons
      MessageBox(Form1.Handle, PChar('ERROR '+IntToStr(ErrorNum1)+
              ' - Incorrect File Read, Read will END'#10+SysErrorMessage(GetLastError)),
              'ERROR, INCOMPLETE FILE READ', MB_OK or MB_ICONERROR);
      end;
  end;

begin
IsBad := False;
MultiFile := TFileStream.Create('E:\many.DataSeg', fmOpenRead or fmShareDenyWrite);
MemStream1 := TMemoryStream.Create;
try
  MultiFile.ReadBuffer(verID, SizeOf(verID));
{I use ReadBuffer for the first read, because I want this to
exception out if there is less than 4 bytes in this file}
  if verID <> 7138201 then
    begin
{ALWAYS check your file ID}
    ShowMessage('This is NOT a valid file or version for this '+
                 'version, can not load this file');
    Exit;
    end;

  if BadRead(segID, SizeOf(segID), 1) then Exit;
  // Use BadRead to check All of the file reads

  {this while loop will test the segID and execute code for the type of data
  the segment ID number represents, I use Hex for the segID because you
  can see each Byte of the Word value. The value for the End of File segID is
  $EEEE, and this will end the while loop}

  while segID <> $EEEE do // $EEEE is the segment ID for end of file
    begin
    // Application.ProcessMessages; //you may want to include a ProcessMessages
    if segID = $FCC0 then // $FCC0 id the ID for a String
      begin
      if BadRead(Len1, SizeOf(Len1), 2) then Break; // Break loop for error
      if Len1 > 0 then
        begin
        SetLength(aryStr, Length(aryStr)+1);
        SetLength(aryStr[High(aryStr)], Len1);
        if BadRead(aryStr[High(aryStr)][1], Len1, 3) then Break;
        end;
      end else
    if segID = $FCC1  then // $FCC1 is the ID for a Bitmap
      begin
      if BadRead(Len1, SizeOf(Len1), 3) then Break;
      if Len1 > 0 then
        begin
        if MemStream1.CopyFrom(MultiFile, Len1) < Len1 then
          begin
        {test the CopyFrom function for a Bad Read}
          IsBad := True;
          MessageBox(Form1.Handle, PChar('ERROR 4'+
              ' - Incorrect File Read, Read will END'#10+
              SysErrorMessage(GetLastError)),
              'ERROR, INCOMPLETE FILE READ', MB_OK or MB_ICONERROR);
          Break;
          end;
        SetLength(aryBmp, Length(aryBmp)+1);
        aryBmp[High(aryBmp)] := TBitmap.Create;
        MemStream1.Position := 0;
        try
          aryBmp[High(aryBmp)].LoadFromStream(MemStream1);
          except
          {LoadFromStream will throw an Exception for a Bad file read}
          IsBad := True;
          //raise;
          MessageBox(Form1.Handle, PChar('ERROR 5'+
              ' - Incorrect File Read, Read will END'),
              'ERROR, INCOMPLETE FILE READ', MB_OK or MB_ICONERROR);
          Break;
          end;
        MemStream1.Clear;
        end;
      end else
    if segID = $FCC2  then // $FCC2 is the ID for an Icon
      begin
      if BadRead(Len1, SizeOf(Len1), 6) then Break;
      if Len1 > 0 then
        begin
        if MemStream1.CopyFrom(MultiFile, Len1) < Len1 then
          begin
          IsBad := True;
          MessageBox(Form1.Handle, PChar('ERROR 7'+
              ' - Incorrect File Read, Read will END'),
              'ERROR, INCOMPLETE FILE READ', MB_OK or MB_ICONERROR);
          Break;
          end;
        SetLength(aryIcon, Length(aryIcon)+1);
        aryIcon[High(aryIcon)] := TIcon.Create;
        MemStream1.Position := 0;
        try
          aryIcon[High(aryIcon)].LoadFromStream(MemStream1);
          except
          IsBad := True;
          MessageBox(Form1.Handle, PChar('ERROR 8'+
              ' - Incorrect File Read, Read will END'),
              'ERROR, INCOMPLETE FILE READ', MB_OK or MB_ICONERROR);
          Break;
          end;
        MemStream1.Clear;
        end;
      end else // segID is NOT a recognized number
      begin
      IsBad := True;
      MessageBox(Form1.Handle, PChar('ERROR 10'+
              ' - Incorrect File Read, Read will END'),
              'ERROR, INCOMPLETE FILE READ', MB_OK or MB_ICONERROR);
      Break;
      end;

    if BadRead(segID, SizeOf(segID), 11) then Break;
    // get the segID after each data segment read
    end;
  finally
  FreeAndNil(MultiFile);
  FreeAndNil(MemStream1);
  //Test for IsBad and Free all Bitmaps and Icons
  if IsBad then
    begin
    for verID := 0 to High(aryBmp) do
      FreeAndNil(aryBmp[verID]);
    for verID := 0 to High(aryIcon) do
      FreeAndNil(aryIcon[verID]);
    SetLength(aryStr, 0);
    end;
  end;

for verID := 0 to High(aryBmp) do
  Form1.Canvas.Draw(6+(verID*60), 4, aryBmp[verID]);

for verID := 0 to High(aryIcon) do
  Form1.Canvas.Draw(6+(verID*32), 104, aryIcon[verID]);

for verID := 0 to High(aryBmp) do
  FreeAndNil(aryBmp[verID]);
for verID := 0 to High(aryIcon) do
  FreeAndNil(aryIcon[verID]);
if High(aryStr) > -1 then
ShowMessage(aryStr[High(aryStr)]);
end;
I have used an End of File Segment ID to tell the while loop to quit. But you could leave off the End of File Segment ID and then test for -

if MultiFile.Position = MultiFile.Size then break;

you could also put a Data Segment Count in the file header and break the loop when the count is equal to the data segments read. You might want to experiment with some ways to end the file read.

You can also use this segment ID method for the "Header" of your file, some kinds of files (like the JPEG - JFIF image files) have file headers that are NOT a fixed size, , since the information needed for the header may change. So if you need a flexible file information header, you can start your file header with your File ID, and then place a header data segment ID (like $FF11) followed by your header information, and you can place as few or as many header data segments as that special file requires. The JPEG file use this method of header writting because their file headers can have different amount of header data segments in different positions, and each can change in their size (bytes of data in the segment), so they have a header segment ID, followed by a data size byte or Word value.







You should have enough TFileStream knowledge to start and build your own custom Multi-Data files, and use different write and read methods to build your file in a way that will have the options you need for your program. There are Many, Many more ways to place Data into a file and read data out of a file, so you will need to take the time to do this with your own files and experiment with making files that store the data you need for your program. Remember that you will need to know what data is in the file, the position and byte size of that data in the file. If you are able to make a list of all the different pieces of data (variables) and their "delphi types" that your program needs to store and read in a file, you should be able to figure out how to arrange the data segments, then write and read them for a file. There is no way around it, to get to know about file writting and reading, you will need to do it, you will need to take the time to make several different files and read these files. For most this is not an easy thing to understand, since it has so many factors that can change.

There is another Delphi-Zeus about file access, using the window's API functions, like CreateFile( ), you can see this page at lesson 15 - Writing and Reading Files. You may consider using the API file access instead of the TFileStream, since the difficulty with file access is less with the access functions, than with how to arrange, identify and sort the bytes of a file.



       

Lesson -     One  Two  Three  Four  Five  Six  Seven  Eight  Nine  Ten




H O M E