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

Home
DelphiZeus
15. Writing and Reading Files
How to Create File Handles
and use File Bytes as Data

Home



How to Access Files on Disk

In this lesson I will give you some examples for reading and writting data to files on a system disk. A file is the basic "group of bytes" used for disk storage, that allows a computer to distinguish one block of disk data (information) from another. You may or may not know about the different windows disk file systems (FAT16, FAT32, NTFS) and File Allocation Tables, with the API file functions you will not have to deal with those or other low level disk operations. The API file access function that is used here is CreateFile( ). This and other API file functions will handle all disk file access work, like reading or changing the different file system FAT tables and accesing the disk sectors of clusters of bytes. So you will not have to worry or deal with low level file sector block management, this is all done for you by the Windows Operating System and the system hardware when you use the CreateFile( ) function.

What you will need to do, is be able to know which bytes of data you need to write or read from a disk file, you will see some examples in this code to save and read several different kinds of data (bytes) to file. File bytes can be written and read using the FileWrite( ) and FileRead( ) functions, however the thing you need to learn, is how to organize and identify the many bytes of data that are in a file.



Disk Files

There are many different files and file types on your drives, these different types of files are generally identified by their file extentions, some file extentions that should be familar to you are -
.TXT   .PAS   .BMP   .DOC   .WAV   .JPG   .MP3
Each of these extentions is used on certain files that contain a certain type of data and organization (order in the file byte sequence), so a program can read the bytes from this file and know how to use these bytes as data to do something. The data in these files can be a simple arangement (order of file bytes) like a .TXT file or they can be a very complex organization of the data bytes, like in a .WAV audio file. I will not try tell you anything about the structures of data files like .BMP or .WAV files here, I will try and give some examples for opening a file, reading or writing bytes in that file and then closing that file. I want you to see that to work with any file, you will need to know what data is in that file, it's data size, sequence, position, and organization. The file system maintains a file as a sequence of bytes on the disk, information about each file is maintained in the FAT (File Allocation Table) and includes the file Name, the file Size, file Attributes, file Times for Creation, Last-Write, Last-Access, , and all of the locations for disk Sectors where the fragments of this file are located. There is no file information stored in the FAT about what the bytes in a file bytes may be used for, except as a file extention (the file extention is just an indicator, NOT a restriction, you can have a WAV file with a BMP file extention). So when you use code to read or write the bytes of a file, your code will determine the data size, sequence, position, and organization of what is in that file, not the system or the delphi compiler.
To access a disk file to read or write, you will need to open it with the CreateFile( ) function.

There are code examples for the subjects of this lesson in the UseFiles program code in the next Lesson 15A, Program Code for UseFiles.



CreateFile Function -

The CreateFile( ) function will be used here only for accessing disk files, it can be used for accessing more things than disk files, but I will only use it for files in this lesson. You can get some information about using CreateFile( ) from your Win32 API Help for index "CreateFile".
At MSDN web library - MSDN-CreateFile Also you may want to read the Help about using files under index "Files".

Here are it's parameters -

function CreateFile(

  lpFileName: PChar; // string pointer to name of the file 
  dwDesiredAccess: Cardinal; // access (read-write) mode 
  dwShareMode: Cardinal; // how to share file 
  lpSecurityAttributes: PSecurityAttributes; // security attributes 
  dwCreationDisposition: Cardinal; // how to create 
  dwFlagsAndAttributes: Cardinal; // set file attributes 
  hTemplateFile: THandle // file handle with attributes to copy
  
  ): THandle; // returns handle to the open file
Here is a list of the prameters, with only a short description of the what the parameter is used for. You should read the Win32 API Help for more options and uses.

lpFileNameThis is a PChar that has the File Path and Name of the file you want to access.
dwDesiredAccessThis will set the "Read" and "Write" access for the file. You can not write to a Read mode file or read from a Write mode file, you can set access to be both Read and Write.
dwShareModeThis will set how or if this file can be used by other calls to open or access it (Share).
lpSecurityAttributesA pointer address for a TSecurityAttributes record that sets the Security Attributes for this file. I will not try and explain security implementation here, so this will always be   nil   in this lesson.
dwCreationDispositionSpecifies which action to take on files when it opens them, and what to do if they exist or do not exist. You can create a new file or only access an existing file. You should take care in setting this for opening a file, since it can erase (overwrite) an existing file.
dwFlagsAndAttributesThis Cardinal value takes Flag bits to set the system's memory caching methods and attributes for the open file.
hTemplateFileA file handle with GENERIC_READ access to a template file, which will supply file attributes and extended attributes for a file only if it is created. This is usually used only when copying a file, and is only availible on NT systems.

The CreateFile( ) function allows access to files by opening an existing file or creating a new file. When you use CreateFile( ), you must declare whether it will read from the file, write to the file, or both. You must also tell it what to do if the file exists or not. So you might tell CreateFile( ) to always create a new file, so that it creates the file if it does not exist or overwrites the file if it does exist. With CreateFile( ) you specify how to share the file, with read or write sharing, both, or neither. You can set the file attributes (hidden, system, readonly) for the file and some file cacheing handling like sequencial-scan and random-access.

As you have seen with other create functions, this returns a "Handle" to the open file or the constant INVALID_HANDLE_VALUE if it could not open the file you gave parameters for. If the result is INVALID_HANDLE_VALUE you can call the GetLastError function to get the reason the system could not open the file. Other functions use the file handle returned to access this file, the FileWrite( ) and FileRead( ) functions use this handle for reading and writting the bytes of this open file. As with many system handles you MUST have the system dispose of this handle and it's object memory with the CloseHandle( ) function as soon as you are finished using this open file.

The Win32 API Help for CreateFile( ) has through and clear explanations for all of this function's parameter settings and options, so I will only explain some of them. You should read over the information in the API Help to get some knowlege about using this function. I will present some code examples below to show you which settings and options I use and why. Here is the code for a very basic CreateFile( ) for reading a file -

hFile := CreateFile('C:\aFolder\aFile.txt', GENERIC_READ, 0,
                     nil, OPEN_EXISTING, 0, 0);
This will open an existing file for reading (because GENERIC_READ is used) with all of the default settings for CreateFile. This function will fail if the file does not exist because it has OPEN_EXISTING as the dwCreationDisposition parameter.

Here is basic code to open a file for writing -

hFile := CreateFile('C:\aFolder\aFile.txt', GENERIC_WRITE, 0,
              nil, CREATE_ALWAYS, FILE_FLAG_SEQUENTIAL_SCAN, 0);
This will create and open an file for writing, because GENERIC_WRITE is used. This will create a file if it does not exist or overwrite a file that does exist because the CREATE_ALWAYS flag bit is used. The FILE_FLAG_SEQUENTIAL_SCAN flag bit is used in this function, which will help the system cache a file and speed writing and reading for larger files, if all file byte access is done in sequence.

TECHNICAL NOTE

Including the FILE_FLAG_SEQUENTIAL_SCAN and FILE_FLAG_RANDOM_ACCESS flags, may not make very much difference in small file access speed for ReadFile( ) and WriteFile( ), in the newer windows systems using newer hard drives, , there is a complex and efficient disk caching to memory methods (from years of testing and development), that is used for file reading and writting. A hard disk memory cache may be larger than 8 megabytes. The hard disk can not read or write byte amounts smaller than the size of a disk "cluster" , usually between 4 Kilobyes and 32 Kilobytes. So the disk does not ever read or write just one byte (or one kilobyte) even if that is what is called for in your code. If in your code you write a single byte to the file, then the disk must first read the whole disk "cluster" containing the byte into memory (usually the disk memory) and replace that single byte in that memory and then write that memory (4 kilobytes or clustor size) back to the file's disk cluster. By using memory to "Cache" file read and write functions, the speed of these operations for a program's code are subtantially increased when accessing large files, however this apparent speed increase for CPU code operations is not the real speed "time of operation" for the disk, which has a fairly constant cluster access rate. The speed increase may be for the "time" it takes for the disk function like WriteFile( ) to finish in your code, which may just write to a memory block, and the acual hardware disk wtite of the WriteFile( ) may be done later. For files larger than your hard disk memory cache, the FILE_FLAG_SEQUENTIAL_SCAN flag can improve speed, if you are not going to move backwards in your file.

Code to Read and Write a Text File
Here is some information about the two file functions used in the first example -

GetFileSize( ) Function
This is the function to use when you need to know the number of bytes on the disk that your open file is using, the file size. You can read about in in you Win32 Help or at the MSDN web library - MSDN-GetFileSize

function GetFileSize(

  hFile: THandle; // file handle for open file
  lpFileSizeHigh: PDWORD // pointer for the size bits over MaxDWORD

  ): Cardinal; // returns the file size below MaxDWORD
As with all file functions you will need to have a valid handle to an open file for the hFile parameter. This function returns a cardinal value that has the file size in it, disk file sizes can be more than a 32 bit variable can hold (over MAXDWORD, 4294967295), so a second parameter was added to recieve the bit values above the 4294967295 limit with the lpFileSizeHigh parameter, a pointer PDWORD type. You can set this to nil if you do not want to deal with files over 4 gigs. In this lesson I will not be concerned with using files over 4 Gigabytes so I will always have the lpFileSizeHigh paramteter as nil. With lpFileSizeHigh paramteter as nil , if a file size is more that the MAXDWORD limit the function result will be for failure.
If the function fails the result will be $FFFFFFFF (MAXDWORD).
If you need to use files over 4 gigs you can mutiply the lpFileSizeHigh by MAXDWORD and add it to the result to get a Int64 value.
  var
  SmallSize, BigSize: Cardinal;
  FileSize: Int64;
begin
SmallSize := GetFileSize(hFile1, @BigSize);
FileSize := SmallSize + (MAXDWORD * BigSize);
end;

ReadFile( ) Function
Usually if you are using CreateFile( ), then you want to read or write bytes for that file. The ReadFile( ) function if used to access and get the values (Read) of the bytes in that file. It uses the file handle obtained by the CreateFile( ) function. You can read about it in the Win32 Help or at the MSDN web library - MSDN-ReadFile

The functions parameters are -

function ReadFile(

  hFile: THandle; // handle of file to read 
  var Buffer; // any variable that will receive data
  nNumberOfBytesToRead: Cardinal; // number of bytes to be read
  var lpNumberOfBytesRead: Cardinal; // total number of bytes read
  lpOverlapped: POverlapped // address of TOverlapped data

  ): BOOL; // True if successful
ReadFile( ) Parameters -

    hFile - You must use a valid file handle from the CreateFile( ) function.

    Buffer - This is an unTyped variable that will receive the bytes read from the open file. (the system writes to this variable) You must have a variable here with enough memory assigned to it to accept all of the bytes you want to read from the file (read amount set in nNumberOfBytesToRead). Unlike many parameters, this one is unTyped, and the Delphi compilier will accept ANY thing that is a variable, there is no type checking done (no warings).
An unTyped Buffer is also used in the WriteFile( ) function.

Know About unTyped Variable? ?
An unTyped variable has no Type checking! This is both good and bad, This was nessarary because there are many different data types that can be read from 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 any kind of coding mistake 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 size (Integer, Byte, Char, TPoint, TRect and others) you can just use that variable as the Buffer and put a SizeOf( ) in the nNumberOfBytesToRead parameter. If you use any variable Type that has a Non-Fixed size (like a String or PChar), then you can NOT use the SizeOf( ) function and you must get or calculate the number of the bytes used by this Variable in this particular open file. (more about how to do this in the examples) For this Buffer variable, the compiler will access it's "Value" data bytes and write the file bytes to it. So if you use any kind of "Pointer" Type variable you will need to dereference it with a ^ . If you use any variable length array like a String variable, you can NOT use that variable in the Buffer (since it is just a Pointer to the array's information), you will need to reference the data or use the first element of the array. Also any kind of TObject variable will NOT work in this function for the Buffer variable. You might need to use this unTyped Buffer variable with different Types to get used to how it works. The examples below will show you enough to get started.

    nNumberOfBytesToRead - This is the amount of bytes the system will try and read from the file, it will Not read past the end of the file, but stops there.

    lpNumberOfBytesRead - This is a returned value that is set to the number of bytes actually read from the open file. This can be zero and the function's result can also be True, (it read nothing, but indicates file read success), if it is at the end of the file or the nNumberOfBytesToRead was set to zero.

    lpOverlapped - This is used for "OverLapped" files, this TOverlapped record address is required if the File was created with FILE_FLAG_OVERLAPPED flag. An overlapped file is used to open a file by several different programs (or threads) at the same time. It allows thread syncronization for writting to the file. I will not say anything about Overlapped files in this lesson so this will always be nil in the examples here.

WriteFile( ) Function
You use the WriteFile( ) function to place your data bytes into the file you have open. The parameters and ways to use this function are much like the ReadFile( ) function above. you should read what the Win32 Help has to say about it, or visit the MSDN web library at - MSDN-WriteFile

function WriteFile(

  hFile: THandle; // handle of file to read
  const Buffer; // any variable to read for data
  nNumberOfBytesToWrite: Cardinal; // number of bytes to write
  var lpNumberOfBytesWritten: Cardinal; // total bytes written
  lpOverlapped: POverlapped // address of TOverlapped data

  ): BOOL;

Methods to Read a Text File
Next is code for a procedure to open and read a text file into a string variable, then set the text of a muti-line edit control to that string. I will use CreateFile( ) with the dwDesiredAccess parameter set to GENERIC_READ, because I only want to read from this file. The dwShareMode is set to FILE_SHARE_READ, which should allow other programs to open this file for read-only access. The dwCreationDisposition parameter is set to OPEN_EXISTING, so that it can only access the file if it already exists. I will not set any Flag value for the dwFlagsAndAttributes (set to zero) and just use the default settings (FILE_ATTRIBUTE_NORMAL). The CreateFile( ) function will return the hFile handle, I always test this handle for INVALID_HANDLE_VALUE to see if a file was opened, and usually display a message that has SysErrorMessage(GetLastError)) in it to see what reason the system gives for the failure. Usually it's a "File does not exist" or a "Charater not acceptable" kind of reason.

After the file is opened, I use the GetFileSize( ) function to determine the amount of bytes to read from the file. For almost every file you open and need to read bytes from, you will need to know the amount of bytes in this file. If a file is empty (has no disk bytes allocated to it, File Size is zero) then you can not read it. Here I test for the file size of zero and 32 Kbs (32768) just to show you simple methods of file analysis. With this text file reader, you will need to allocate an amount of memory for the number of bytes in the file to have a memory container (buffer) to write the file bytes into. I use a String variable for my memory buffer and the SetLength( ) procedure to allocate the memory block.

Next is the ReadFile( ) function, it has the file handle hFile as it's first parameter. Notice that the second parameter Buffer (untyped variable) has the First Charater in the Text1 string variable, Text1[1]. You can NOT use the string variable Text1 here without some kind of dereferencing, or transfer of function access to the string's memory block of text charaters. The Text1 string variable is a Pointer to the strings text data memory location, It is not the variable referencing the strings text data, so if you place the Text1 variable in the Buffer paramter and call the ReadFile function, The system will try and write to the Pointer for the text data, not the text data, and you will get a system error , , like - "This Program has performed an illegal operation", and your program will be shut down. You could use a typecaste to a pointer and then dereference the pointer for this Text1 string variable, , as in -   PChar(Text1)^
I use a reference to the first text character in this string - Text1[1] for the Buffer parameter, although this seems to indicate a single text character, as the ReadFile Buffer the system will just use the memory location of the first byte to start the memory write for the nNumberOfBytesToRead amount.

procedure TextFile2Edit;
const
pError: PChar = 'File Load ERROR';

var
hFile, SizeF, BytesRead: Cardinal;
Text1: String;
begin
// this procedure will open and read a text file into a string
Text1 := 'C:\folder\text file.txt';

hFile := CreateFile(PChar(Text1), GENERIC_READ, FILE_SHARE_READ,
                    nil, OPEN_EXISTING, FILE_FLAG_SEQUENTIAL_SCAN, 0);
{ file is opened for reading only with GENERIC_READ
 For Share, I include a FILE_SHARE_READ to allow other programs to
 read from this file.
 The file must exist or a file error and no open file with OPEN_EXISTING.
 The file Flag is set to FILE_FLAG_SEQUENTIAL_SCAN, which may help in
 access speed for larger files}

if hFile = INVALID_HANDLE_VALUE then
  begin
{ file handle should be checked to see if system has 
  located and opened the file}
  MessageBox(hForm1, PChar('ERROR - System could NOT Open file'#10+
             SysErrorMessage(GetLastError)), pError, MB_ICONERROR);
  Exit;
  end;

SizeF := GetFileSize(hFile, nil); // no lpFileSizeHigh is used
// for most file reads you will need the size of the file on disk
if (SizeF = MAXDWORD) then
  begin
  { the GetFileSize will usually work, although if the file is 
    larger than 4 gigs and there is no lpFileSizeHigh it will fail}
  CloseHandle(hFile);
  // You must try to always use CloseHandle( ) when you are done
  MessageBox(hForm1, 'GetFileSize  FAILED', pError, MB_ICONERROR);
  Exit;
  end;

if SizeF = Zero then
  begin
  // if the file size is zero, there is nothing in the file to read
  CloseHandle(hFile);
  SetWindowText(hEdit1, nil);
  Exit;
  end;

if SizeF > 32768 then
  begin
{ although text files have no file header information, you will
  ususally need to test some property of the file bytes, to see if 
  it is a file you want to use. For an example, I just test for the
  size of 32 Kb, even though it is not nessary}
  CloseHandle(hFile);
  MessageBox(hForm1, PChar('ERROR, File size is MORE than 32,768'#10+
             'CAN NOT LOAD, file is to large at '+Int2Thos(SizeF)),
              pError, MB_ICONERROR);
  Exit;
  end; 

// next are the file read operations
SetLength(Text1, SizeF);
{ a memory block is needed to read the file bytes into, since this 
  is for text charaters I use a String, and to get the 
  memory I use SetLength( ) }

{the next code is typical for a file read, ReadFile( ) will return False
 if it fails. You should also test the BytesRead for the amount that the
 file should read thats in SizeF}
if not ReadFile(hFile, Text1[1], SizeF, BytesRead, nil) or 
       (SizeF <> BytesRead) then
  begin
  MessageBox(hForm1, PChar('ERROR - System could NOT Read file'#10+
             SysErrorMessage(GetLastError)), pError, MB_ICONERROR);
  CloseHandle(hFile);
  Exit;
  end;
CloseHandle(hFile);
// always be sure to CloseHandle( )

SetWindowText(hEdit1, @Text1[1]);
//Place the Text1 string in to the Muti-line Edit hEdit1
end;

Methods to Write a Text File
The next code is for a procedure that will write the text in a muti-line edit to a text file. Here I use the CreateFile( ) function with the GENERIC_WRITE and the CREATE_ALWAYS flag bits so the file can be written to, and it will create a file if it does not exist, or overwrite an existing file.

procedure SaveTextFile;
const
SysError = 'ERROR - System could NOT';
var
hFile, TextLength, BytesWrite: Cardinal;
FileName: String;
pText: PChar;
begin
FileName := 'C:\folder\text file.txt';

hFile := CreateFile(PChar(FileName),GENERIC_WRITE,FILE_SHARE_READ,
             nil, CREATE_ALWAYS,FILE_ATTRIBUTE_NORMAL or 
             FILE_FLAG_SEQUENTIAL_SCAN,0);

if hFile = INVALID_HANDLE_VALUE then
  begin
  MessageBox(hForm1, PChar(SysError+'Create file'+
         #10+SysErrorMessage(GetLastError)), 
         'ERROR - Did not Create File', MB_ICONERROR);
  Exit;
  end;

// I will get the amount of text charaters in the multi-line edit
TextLength := GetWindowTextLength(hEdit1);
if TextLength < One then
  begin
  // if the edit is empty then Close Handle and exit
  CloseHandle(hFile);
  Exit;
  end;

GetMem(pText, TextLength+One);
// this time I use a PChar, and get a memory block for it with GetMem( )
GetWindowText(hPage1, pText, TextLength+One);

// you will need to dereference a Pointer, pText, for a WriteFile( )
if (not WriteFile(hFile,pText^,TextLength,BytesWrite,nil)) or
       (TextLength <> BytesWrite) then
  MessageBox(hForm1, PChar(SysError+'Write file'#10+
          SysErrorMessage(GetLastError)),
          'ERROR - Did not Write File', MB_ICONERROR);
FreeMem(pText); // Release memory and Close Handle
CloseHandle(hFile);
end;

The code above, to read and write a text file, does not need to do any code work to separate or determine the size of the "File Data Segment", , to use the bytes in this simple text file for text data in an edit. All of the "Data" in a text file has a "Data Segment" or "Data Block" size of just One Byte, All text characters use a single byte of a file to store the information needed for an Edit control to display the correct text. And if all you ever need to do is read or write text files, then you are all set. The next section will try and explain some methods for moving data between two files by reading bytes from one file and then writing them to another file. After that I will talk about factors when using files to store non-character data, sometimes called "Binary" files. The terminology I have seen used for naming files "Binary", which seems to mean that the files have more data types in them besides the ASCII text charaters that have byte values between 8 and 126 , the ASCII text only files are often refered to as "Text" files. Because the windows system handing of PChar strings will end the string with a #0 (null-Terminated), files with byte values of zero will have problems if used in PChar or delphi String handling functions. Although some file references talk about "Binary" and "Text" (ASCII) files, there is NOTHING in the Windows File systems or in code to read or write a file, that will have anything different for a "Binary" or "Text" file. ALL the bytes in any file is digital (Binary) data. Here I call it a Text File, if it has a file extention with .TXT , and does not have any bytes with the value of Zero.

Moving Data Between Files

You can use the ReadFile( ) and WriteFile( ) functions to read bytes from one file and put this data into another file or copy the entire file. I have some procedures in my program code - UseFilesU.pas - to copy a file and code to split a file into several smaller pieces and then take these new smaller files and combine them back into a single file. They are based on the following code example.

The code below will create two files with CreateFile( ), one open file is for reading and one open file is for writing. This code will copy all of one file (the Read-File) to another file (the Write-File). It will enter a "While Loop" and read a piece of data into a buffer (memory storage area). It will read a "Chunk" (part of the file), of 512 bytes, from the Read-File and then write this data chunk to the Write-File. The while loop will continue to read and write chunks of bytes until all of the bytes are copied to the new file. I use a memory block in the pointer variable pBufMem to hold all of the bytes that are read and written to the files. I make the memory block with -
    GetMem(pBufMem, ChunkSize);
Please notice that the ChunkSize is a constant value of $200 (512), At first, I thought that using a memory block of the entire size of the file would give the best (fastest) performance, but it did not. For some reason (which I could not find documentation of) the file read and write size of 512 bytes, has a faster performance for the file byte transfer. You should not need to consider or use this file performance stuff, I explained it so you would know why I did it.

Tecnical Note - Hard Disks and Memory Flash Drives have a fairly constant "Read" and "Write" byte transfer rate, so why does changing the ChunkSize in the code below affect performance? I would think that this is due to the difference in the "Read" and "Write" speed to the memory used for file access. For most motherboards using a half kilobyte ChunkSize (512) will write into memory faster than a 64 kilobyte ChunkSize. However this is not a large diference in speed, and is NOT the real disk access speed, the file memory can be written to file AFTER your file write function has ended. For almost all of the file reading and writting you will need to do, , , you will NOT need to consider this "Speed" and perormance factor at all.
Another reason for Not using the file size for your memory storage buffer size, is that your file size may be larger than the available memory, so if you use a smaller "Chunk" size for the memory buffer, you do not have to worry about the file size being to large for the memory to handle.

procedure CopyFileData;
const
ChunkSize: Cardinal = $200;
Err = 'ERROR - ';
CreateErrText = 'System could NOT Create file';
CreateErrTitle = 'Did NOT Create File';

var
hFileRead, hFileWrite, Size1, BytesRead, BytesWrite: Cardinal;
pBufMem: Pointer;


  procedure SysErrorMsg(const Text, Title: String);
  begin
  MessageBox(hForm1, PChar(Err+Text+#10+ 
           SysErrorMessage(GetLastError)),
           PChar(Err+Title), MB_ICONERROR);
  end;


begin
hFileRead := CreateFile('E:\picture.bmp',GENERIC_READ,
               FILE_SHARE_READ,nil,OPEN_EXISTING,
               FILE_FLAG_SEQUENTIAL_SCAN,0);
{ the hFileRead created above will be used to read the
 bytes from to write in copy file }
if hFileRead = INVALID_HANDLE_VALUE then
  begin
  SysErrorMsg(CreateErrText, CreateErrTitle);
  Exit;
  end;

Size1 := GetFileSize(hFileRead, @BytesWrite);
// get file size and test if it is larger than MaxDWord
if (Size1 = MaxDWord) or (BytesWrite <> Zero) then
  begin // test the file size for larger than 4 gigs
  CloseHandle(hFileRead);
  MessageBox(hForm1, 'ERROR - FileSize is to large for copy',
            'ERROR - File too Big', MB_ICONERROR);
  Exit;
  end;

hFileWrite := CreateFile('E:\copy of picture.bmp',GENERIC_WRITE,
                FILE_SHARE_READ,nil,CREATE_ALWAYS, 
                FILE_ATTRIBUTE_NORMAL or 
                FILE_FLAG_SEQUENTIAL_SCAN,0);
// hFileWrite is the Copied file where the file bytes are written
if hFileWrite = INVALID_HANDLE_VALUE then
  begin
  CloseHandle(hFileRead);
  SysErrorMsg(CreateErrText, CreateErrTitle);
  Exit;
  end;

{ pBufMem is the byte Storage area (buffer) of
 memory used to hold the data transfered for read and write}
GetMem(pBufMem, ChunkSize);

while Size1 > 0 do
  begin { Keep reading and writing bytes until all
     bytes are copied. ChunkSize is used for the amount to read, if
     there are less than ChunkSize bytes to read, then ReadFile will
     only read the amount of bytes left, it will NOT read past the
     end of the file. BytesRead will have the correct amount of
     bytes acually read from the file. }
  if not ReadFile(hFileRead,pBufMem^,ChunkSize,BytesRead,nil) then
    begin
    SysErrorMsg('Could NOT Read file', 'Did NOT Read File');
    BadFile := True;
    Break;
    end;

  if BytesRead = Zero then Break;

  if (not WriteFile(hFileWrite,pBufMem^,BytesRead,BytesWrite, nil))
     or (BytesRead <> BytesWrite) then
    begin
    SysErrorMsg('Could NOT Write file', 'Did Not Write File');
    BadFile := True;
    Break;
    end;
  Dec(Size1, BytesWrite);
  end;  // while loop

CloseHandle(hFileRead); // Close all handles
CloseHandle(hFileWrite);
FreeMem(pBufMem); // free your memory buffer
end;

In the next Lesson page, 15A, there is code for the UseFiles program, you can look in the SplitFileU.pas unit, there are several procedures that will "Split" a file into smaller pieces, and then restore these smaller file pieces back to the original larger file, both the split and restore procedure use the same code methods that are shown above.

Making Your Own Data Storage Files
Using More than One Data Segment in Files

File Data Postion and Order, Do the Math -
All files are a collection of bytes, and all files use counting and mathematic methods (add, subtract, multiply) to know "Where" and "How Much" 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 the code examples below, there are some methods shown to create some muti-data files, but if you are making your own multi-data files, 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 extra information in your file that is required to read the Data out of the file. I find that sometimes I need a hand held calculator to add up the bytes used for integers, records, headers, strings and Data, to get the file position of the data I need to write or read.

Fixed Size Variables -
You may need to create you own special files to store your Data to disk, using files that have more than one Data (variable) type in them. So you will need to be able to place several different pieces of information you need for your program in a single file. If you need to store an integer value or TRect in a file, you can not use the text file methods in the examples above. An integer uses 4 bytes for it's data and a TRect uses 16 bytes of data (4 integers). So you will need to set the amount of file bytes read or written to a file for the type of variable you are placing into that file. Since there will be several different variables in one file, I will call the number of bytes used in a file for one variable type a "File Data Segment", as in a piece, a separate block or a part of the file that is used for the data in that variable, record or memory block. Now I will be using several different types of variables (data) with different amounts of bytes needed in a file to recreate that variable by reading the file. If you had an Integer variable named " Int1 " then to Write this integer to a file you could use

WriteFile(hFile, Int1, SizeOf(Int1), BytesWrite, nil);
I just added the SizeOf(Int1) to get the correct amount of bytes to write to the file.
If you have a Rect1, TRect variable, you can read a file to get the Rect1 with the code -
ReadFile(hFile, Rect1, SizeOf(Rect1), BytesRead, nil);
You can use the SizeOf( ) function to get the number of bytes to write for any of the "Fixed Size", "Non-Changeable Memory Use" variables and records, like the Delphi types - Integer, Cardinal, Word, Int64, Byte, TPoint, TRect, TWin32FindData, TSystemTime, TMemoryStatus and many others, even variable and record types that you define in your code. I used the term - "Fixed Size" - which means that the data in these variables do NOT change their memory usage as the data in them is changed. There are some other Delphi Variables, like "String and PChar" that will change their memory block size as the data (text in these) are changed. I will talk about these later.

Here is some code to write and read several Fixed-Size variables to a file. You would use the same code methods to Open and Close the file and use the SizeOf( ) in the WriteFie( ) and ReadFile( ).

  var
  hFile, BytesWrite: Cardinal;
  Int1: Integer;
  Pnt1: TPoint;
  Rect1: TRect;
begin
Int1 := 555;
Pnt1.x := 123;
Pnt1.y := 456;
SetRect(Rect1, 1,2,3,4);
hFile := CreateFile('E:\New File.nfdf', GENERIC_WRITE, 0,
             nil, CREATE_ALWAYS, 0, 0);
WriteFile(hFile, Int1, SizeOf(Int1), BytesWrite, nil);
WriteFile(hFile, Pnt1, SizeOf(Pnt1), BytesWrite, nil);
WriteFile(hFile, Rect1, SizeOf(Rect1), BytesWrite,nil);
CloseHandle(hFile);
end;
The three variables are written to file in sequence, with data segment sizes of 4 (Int1), 8 (Pnt1) and 16 (Rect1), makes the file size 28 bytes. Since you wrote the 'E:\New File.nfdf' file, you know what data is in it, it's data segment sizes and arrangement, so you have the knowledge to read the bytes of the file into variable containers that corrispond to the data containers (variables) that were used to write the file. It is Important that you see that there is NO INFORMATION in this file about what the bytes of this file were used for when this file was written. You can not get any knowledge about what the bytes in this file are used for, by reading this file, from the operating system, or from the Delphi compiler. So in order to read a file you will need to have the information about what is in the file, from someplace other than the bytes in that file.

Code to read this file into the same types of variables used to write the file.

  var
  hFile, BytesRead: Cardinal;
  IntA: Integer;
  PtA: TPoint;
  ReA: TRect;
begin
hFile := CreateFile('E:\New File.nfdf', GENERIC_READ, 0,
             nil, OPEN_EXISTING, 0, 0);
ReadFile(hFile, IntA, SizeOf(IntA), BytesRead, nil);
ReadFile(hFile, PtA, SizeOf(PtA), BytesRead, nil);
ReadFile(hFile, ReA, SizeOf(ReA), BytesRead, nil);
CloseHandle(hFile);
end;

The SetFilePointer( ) Function
When you need to change the position where a file is wtitten to, or read from, you have the SetFilePointer( ) function. Using this function will set where (which byte in the file) the next read or write will begin. It is defined as -

function SetFilePointer(
  hFile: Cardinal; // opened file handle
  lDistanceToMove: Integer; // number of bytes to move file
  lpDistanceToMoveHigh: Pointer; // high-order Word for bytes to move
  dwMoveMethod: Cardinal // file begining, current or end
  ): Cardinal;
The hFile parameter is the file handle. The lDistanceToMove parameter is an integer value which tells the system how many bytes to move the file position. If this is a positive value it is moved towards the end of the file, if this is a negative number, it is moved towards the beginning of the file. The lpDistanceToMoveHigh parameter is for file moves of more than 2 gigabytes (Max Integer value), but I do not use this and will be NIL in this lesson. The dwMoveMethod parameter is a flag that tells the system where in the file the starting point for the move is. It has 3 values - FILE_BEGIN, FILE_CURRENT, and FILE_END. If you are writting a file you can use this function to move the starting point past the end of the file, which will automatically increase the file size. If you are reading the file, it will Not move it beyond the end of the file.

Code to use the SetFilePointer( ) function
The next code opens the same file "E:\New File.nfdf" that was created above. It uses the SetFilePointer( ) function to move the file position 12 bytes from the beginning of the file. Then it reads a single integer, IntA, from the file -

  var
  hFile, BytesRead: Cardinal;
  IntA: Integer;

begin
hFile := CreateFile('E:\New File.nfdf', GENERIC_READ, 0,
             nil, OPEN_EXISTING, 0, 0);
SetFilePointer(hFile,12,nil,FILE_BEGIN);
ReadFile(hFile, IntA, SizeOf(IntA), BytesRead, nil);
CloseHandle(hFile);
MessageBox(hForm1, PChar('Integer is '+Int2Str(IntA)),
             'Integer', MB_ICONERROR);
end;
The Message Box will display that the integer value of IntA is One, the value of the Rect1.Left that was written to the file in the code above. The SetFilePointer( ) function moved the file position to the place where the Rect1 data was placed in this file. PLEASE NOTICE - That a TRect was written to the file and an Integer value, IntA, was read out of the file. There is NO compilier type checking for anything that you read out of any file. The only thing that determines what and how something is read from a file is You, the code writter. So in the file read above, if you moved the file position to "8", then the value of IntA would be 456, which was the value of the Pnt1.y written to the file. What would be the value of IntA in the code above, if you moved the file position to 16 ?

You should create several your own "Fixed Size Data" files now and experiment with reading and writting them, and also experiment with the SetFilePointer( ) function, to read and write different values (variables) from the files to get to know how all of this works. In my code for the UseFiles program, lesson 15A, there are some code examples for "Fixed Size Variables", you can look at in the DataFilesU.pas unit

Changable Size (memory allocation) Variables -
The variables that are dymamic, "Changing" in their data's memory usage like the Delphi types of - String, PChar (or other Pointer types), Dynamic Arrays, and any kind of TObject, are all a delphi "Pointer type", this pointer's 4 bytes of data has the memory location of the Data you use for that variable, NOT the Data of the variable. These changing data size "Pointer Variables", are different than the fixed size variables to access the "Data" used for these variables. You CAN NOT use the code methods that you used above for the Fixed size variables to write or read these to file. If you use the code from above for a String, like this -

{ The hFile in codes below is a valid Open file handle.
  the code for CreateFile( ) and CloseHandle( ) is not shown }

  var
  Str1: String;
  BytesWrite: Cardinal;
begin
Str1 := 'Hello';
WriteFile(hFile, Str1, SizeOf(Str1), BytesWrite, nil);
end;
It will NOT work. Please notice that the variable "Str1" is a String and it is a Pointer reference to the Text data used for the string, so the code here will write the Memory Location Number of this pointer (a Cardinal value, a completly useless number in a file) to the file, but this has Nothing to do with the Text characters in the string. For a String or a Dynamic Array variable you can use the first member of the data, for String "Str1" you would have Str1[1].

You can NOT use the SizeOf( ) function to get the size of their memory and file data requirements. If " Str1 " is a String Variable and you call -
SizeOf(Str1);
It will ALWAYS return the value of 4, no matter if Str1 string has 1 character or a megabyte of characters. ( 4 is the number of bytes in a Pointer type, the Data of this String variable Str1 is a number (memory location), not the text charaters). You will need to call a function to calculate the number of bytes your dynamic variable is currenly using, or create your own "Size" function, or use math to calculate it's data's byte size. For a String (with only ASCII text data in it) you could use the Length( ) function to get the memory size of the character data for this String. So to read a String variable from a file that only has a single string in it, you might use the code in the procedure TextFile2Edit; above, because the byte size of the file is byte length of the text character data. However, if you are storing several variables in the same file as Data Segments, then that method will not work, because you have not stored the "Size" of this String's Data Segment in the file. So when you create and write this file, you will need to write the byte Size (length of the Data Segment, the text) of this String to File as a value, before you write the text character data. Like the code below -

  var
  Str1: String;
  sLength, BytesWrite: Cardinal;

begin
Str1 := 'Hello';
sLength = Length(Str1);
// first write the number of bytes used for the text data
WriteFile(hFile, sLength, SizeOf(sLength), BytesWrite, nil));
// next write the string charaters
WriteFile(hFile, Str1[1], sLength, BytesWrite, nil));
end;
With the byte Length of the String text data recorded in the file you will be able to read this Length value and know how many bytes to set the memory for the String and then how many bytes to place into the string when reading this String Data Segment. You will need to set the length of the String first as in this code -
ReadFile(hFile, sLength, SizeOf(sLength), BytesRead, nil));
SetLength(Str1, sLength); // get memory allocation for string
ReadFile(hFile, Str1[1], sLength, BytesRead, nil));
For any variable that uses a changing memory allocation for it's data, you must store the number of bytes it uses for it's data to the file along with the data of the variable, or have some way to determine the number of bytes you need to read from the file for this data.

If you use a PChar type, then you will need to Dereference it with a ^ and use the function StrLen( ) to get the number of the characters in the PChar -

  var
  pStr: PChar;
  sLength, BytesWrite: Cardinal;

begin
pStr := 'Hello';
sLength = StrLen(pStr);
// first write the number of bytes used for the text data
WriteFile(hFile, sLength, SizeOf(sLength), BytesWrite, nil));
// next write the string charaters
WriteFile(hFile, pStr^, sLength, BytesWrite, nil));
end;

File Headers
When you are creating your special file for data storage, you will need to place information in your file about how to read the file, this could be info about the number of data elements (array sizes), the length or size of a data segment, the file location (position) of a data segment, or the File Type (what kind of file) and the version number of the program that made the file. You would place any information in the header that is needed to know how to read this file. As the file builder, you will need to have a list of what data and information you need to place into the file. In order to write and read this data into and outof the file, you may need to have information stored in a "File Header" data segment, that has this information about the file stucture arangement and sizes, , and any other data that you will need to be able to read all of the different data segments from this file.

File Type Identifier, an Important File Header
In the code used to read any kind of changable size data segment, you will read the data size from file and then use this information to get a memory block (set the byte size of a variable's data block). If you incorrectly read a file for this size information, you might request a memory allocation of a LARGE amount (4 Gigabytes) with this bad size data, and your code (maybe your entire program) will fail, with some kind of "Out of Memory" error or exception. When data segment size information is placed in a file, it is common for file write coders to have a "File Header" with the first few bytes of their file as a "File Identifier", with a "File Type" ID and a Version number . You can read the first bytes of the file and test to see if the "File ID" matchs. This is for file read safety, if you are setting memory block sizes from data out of a file, you want to be sure the file you are reading has the correct data format for your read methods. For instance the standard system Bitmap file (BMP extention), will always have the ASCII text 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 file that reads a "data segment" or "array length" size from the file. You should use at least 2 bytes for this File-ID, I use 4 bytes or more for my File-ID, you can use any kind of data for a File-ID, like a number (integer), or ASCII text characters. . I will use an integer (4 bytes) for the "File Type and Version" Identifier in this code -

var
hFile, ID_ver, sLength: Integer;
Str1: String;

begin
hFile := CreateFile('C:\f1.sfile',GENERIC_WRITE,FILE_SHARE_READ,
               nil, CREATE_ALWAYS,FILE_ATTRIBUTE_NORMAL or 
               FILE_FLAG_SEQUENTIAL_SCAN,Zero);
ID_ver := 519025401;
WriteFile(hFile, ID_ver, SizeOf(ID_ver), BytesWrite, nil);
Str1 := 'Change the 01 in ID_ver to your version number';
sLength = Length(Str1);
WriteFile(hFile, sLength, SizeOf(sLength), BytesWrite, nil));
WriteFile(hFile, Str1[1], sLength, BytesWrite, nil));
CloseHandle(hFile1);
end;
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 519025402 in this case (version 2), and with more file write changes I would go to version three, 519025403. 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 format and arrangement it. So when you open a file for Reading, the first thing you check is the ID number. And if it is Not the correct ID and version then stop file reading and Exit the procedure. For this reason you need to use a different File-ID (number or text) for each file type you create. I used 519025401 in the code above, you should create your own number (or text) for this and do Not use my numbers.

Code Examples to Write and Read Changing Size Variables
Below is the code for the SaveDataSize and LoadDataSize procedures, which will write and read 3 changing data size variables to a file. The first variable is a String, the next is a PChar, and last variable is a dynamic array of integers.

In the SaveDataSize procedure code below, a file is opened for writting, and the first thing that goes in this file is a File Header, which is a "File Type Identifier" or File-ID. Since this file will read out integer values and set the size of memory blocks (string length and array size), it is necessary to have a "File-ID" for safety. Here I use 6 text characters for the File-ID, I have the FileID variable as an Array[0..5] of Char. I set the 6 characters of this array to the file extention 'VSDF' and add a version number '01' so the file ID is 'VSDF01' . A number of files use this file ID convention, text charaters of the file Extention and a version number, for instance the .GIF image files. You should NOT use this ID for your file ID (change your file extention and ID), you should create your own ID text (or number) for your special files, and please remember to try and use a DIFERENT File-ID for every different file type (and version) you create.

Since I write to this file many times, and want to check and see if my file writes were successful, I have included a noWrite( ) function. This function will write to the file and test to see if the write was successful. If the write was not successful then I display an error message, close the file handle, and delete the output file. This fuction returns True if there is a write error, so the code can stop and exit the procedure that it is in. If you are createing a file and then writing it's contents, you should have some way to delete or fix the file in case there is a write error.

After the FileID is written to file, I get the length of Str1 into the DataSize variable, and write it to file, you must remember to place the "File Segment Size" of all changing-size variables into the file. And the very next file write must be placing the text characters of the string to file. I use the same method for the PChar variable, pText, I get the text length with lStrLen( ), please notice that I add one to this length value, so the null character #0 will also be written to file. I do this because, unlike delphi strings, when you read a PChar out of the file, it saves you adding a #0 at the end, if you can also read out the last #0. Next I need to write the aryInteger dynamic array to file, I get the array length and write this to file, but there is no function to get the amount of bytes in the memory block used for the aryInteger data, so I must use math to calculate the amount of bytes to write to the file for aryInteger. I need to mutiply the number of elements the array has (or the number of integers I want to write to the file), times the byte size of each array element (an integer here, with a byte size of 4), here I use the math combination of -
Length(aryInteger)*SizeOf(Integer)
or you could use -
Length(aryInteger)*SizeOf(aryInteger[0])
Which will get the correct amount of bytes. Like the string variables, you can NOT use the aryInteger as the Buffer parameter in the WriteFile( ) function, you must use the first array element instead, which is aryInteger[0]. To finish this file write, I close the file handle and show a success message.

procedure SaveDataSize;
var
OutFile: String;
hFile: Cardinal;
  // FileDataID is 6 text Chars
FileID: Array[0..5] of Char;
DataSize: Integer;
BytesWrite: Cardinal;
// 3 variables below go in file
Str1: String;
pText: PChar;
aryInteger: Array of Integer;

  function noWrite(var Source; Size: Cardinal): Bool;
  begin
  // noWrite will return True if the write is NOT successful
  if WriteFile(hFile,Source,Size,BytesWrite,nil) and
         (BytesWrite = Size) then
    Result := False
    else
    begin
    //if there's a File Write error, close the file and delete it
    CloseHandle(hFile);
    DeleteFile(PChar(OutFile));
    MessageBox(hForm1, 'ERROR - Could NOT write the file',
             'Write File ERROR', MB_ICONERROR);
    Result := True;
    end;
  end;


begin
// OutFile is the file path and name of output file
OutFile := 'C:\var data.vsdf';
  // File ID is set to file Extention and version
FileID := 'VSDF01';
  // initialize all variables
Str1 := 'String Text to write and read in a file';
pText := 'More Text to use in this file';
//as the compilier creates pText above it adds a #0 to the end
SetLength(aryInteger, 4);
aryInteger[0] := 4444444;
aryInteger[1] := 8888888;
aryInteger[2] := 1;
aryInteger[3] := -2;

hFile := CreateFile(PChar(OutFile),GENERIC_WRITE,FILE_SHARE_READ,
               nil, CREATE_ALWAYS,FILE_ATTRIBUTE_NORMAL or 
               FILE_FLAG_SEQUENTIAL_SCAN,0);
if hFile = INVALID_HANDLE_VALUE then
  begin
  MessageBox(hForm1, 'ERROR - Could NOT create the file',
             'Create File ERROR', MB_ICONERROR);
  Exit;
  end;

{ first I write a File type ID number to the file.
  For all your data files you should have some kind of
  file Identifier as the first data block in the file, 
  so you can try and detect if this file has the data bytes 
  that you know how to read }
if noWrite(FileID, SizeOf(FileID)) then Exit;
// if the file write of noWrite fails, then Exit
DataSize := Length(Str1);
// get the number of of string bytes in Len and write it to file
if noWrite(DataSize, SizeOf(DataSize)) then Exit;
if noWrite(Str1[1], DataSize) then Exit; 
// write string text to file, use the first character Str1[1]
DataSize := lStrLen(pText)+1; // add one byte for the NULL char #0
if noWrite(DataSize, SizeOf(DataSize)) then Exit; //also writes the last #0
if noWrite(pText^, DataSize) then Exit;
DataSize := Length(aryInteger);
if noWrite(DataSize, SizeOf(DataSize)) then Exit;
{ write number of integers in array.
 Since the elements of this array are fixed size (integer), 
 write the entire array
 IMPORTANT - you MUST do the math to get the byte Size of 
 all the Data in the array, multiply the length of the array
 by the size it's elements - Length(aryInteger) * SizeOf(Integer)}
DataSize := Length(aryInteger)*SizeOf(Integer);
if noWrite(aryInteger[0], DataSize) then Exit;
// use first menber of the array aryInteger[0] as the Source
CloseHandle(hFile);
MessageBox(hForm1, PChar('Data File was Created at'#10+OutFile),
             'SUCCESS, Data File Creation', MB_ICONINFORMATION);

end;
As you can see in the code above, I have a "Data Size" integer in the file for all of the variables. You must have a way to read the amount, out of the file, for everything (data segment) in the file that has a changing byte Size.

Next is the code used to read this file in the LoadDataSize procedure. This procedure has the noRead( ) function, which is used like the noWtite( ) function above. It tests for read file success and returns True if it fails, which will stop the code and exit the procedure. The first thing I read from the file is a text block of 6 charaters, in the ID variable. I test these characters to see if they are 'VSDF01' , if they are not the correct File-ID, I show an error message and exit. Having this ID safety test can save your program from crashing, if it trys to read the wrong kind of file. . . . . . It will use the same method to read the 3 variables from the file that were used to write the file. It reads an Integer (DataSize) and then uses this to set the size (or Length) of the variable it needs to read data for. The pText variable uses GetMem( ) to assign it's memory block, and then reads out the text data, please notice that the null character #0 is also read from file, this is because GetMem( ) does not set any of that memory block to any value, so if you do not read the #0 from file you must set the last Character to #0 for a PChar variable.-
procedure LoadDataSize;
var
hFile: Cardinal;
ID: Array[0..5] of Char;
DataSize: Integer;
BytesRead: Cardinal;

InFile, Str1: String;
pText: PChar;
aryInteger: Array of Integer;

  function noRead(var Source; Size: Cardinal): Bool;
  begin
  // noRead returns True if the read is NOT successful
  if ReadFile(hFile,Source,Size,BytesRead,nil) 
              and (BytesRead = Size) then
    Result := False
    else
    begin
    CloseHandle(hFile); // close handle if read error
    MessageBox(hForm1, 'ERROR - Could NOT read the file',
             'Read File ERROR', MB_ICONERROR);
    Result := True;
    end;
  end;


begin
// InFile is the file path and name of input file
InFile := 'C:\var data.vsdf';

// open the file for reading
hFile := CreateFile(PChar(InFile),GENERIC_READ,FILE_SHARE_READ,
           nil,OPEN_EXISTING,FILE_ATTRIBUTE_NORMAL or
           FILE_FLAG_SEQUENTIAL_SCAN,0);
if hFile = INVALID_HANDLE_VALUE then
  begin
  MessageBox(hForm1, 'ERROR - Could NOT create the file',
             'Create File ERROR', MB_ICONERROR);
  Exit;
  end;

// if noRead fails (with True) then Exit
if noRead(ID, SizeOf(ID)) then Exit;
// get the File ID chars, and test them
if ID <> 'VSDF01' then
 begin
 CloseHandle(hFile);
 MessageBox(hForm1, 'ERROR - File is NOT a Valid Var Data File',
             'ERROR, NOT a Valid Data File', MB_ICONERROR);
 Exit;
 end;
// get the length of the String text data
if noRead(DataSize, SizeOf(DataSize)) then Exit;
SetLength(Str1, DataSize); // get memory for the string
if noRead(Str1[1], DataSize) then Exit; // read data into string
if noRead(DataSize, SizeOf(DataSize)) then Exit;
// read text length and get memory, the NULL char #0 is included
GetMem(pText, DataSize);
if noRead(pText^, DataSize) then // this also reads the final #0
  begin
  FreeMem(pText); // free the memory before Exit
  Exit;
  end;
if noRead(DataSize, SizeOf(DataSize)) then // read size of array
  begin
  FreeMem(pText);
  Exit;
  end;
// get memory for the array with SetLength( )
SetLength(aryInteger, DataSize);
{ you will need to calculate size of read with
  Length(aryInteger)*SizeOf(Integer) }
if noRead(aryInteger[0], // use the first member of array
          Length(aryInteger)*SizeOf(Integer)) then
  begin
  FreeMem(pText);
  Exit;
  end;

CloseHandle(hFile);
MessageBox(hForm1, 
         PChar(Str1+#10+pText+#10+Int2Str(aryInteger[0])),
             'SUCCESS, Data File Read', MB_ICONINFORMATION);
FreeMem(pText);
end;
In this file read procedure code above, I hope you can see that without the "Data Segment Size" information in this file, you could NOT read the data from the file, because you would have no idea of how many bytes of file data to read into your variable or memory block.

Many Other Ways to Make Files
There are so many different kinds of multi-data files you can create, that I will not give any more file methods here. I do not beleive there are any "One Size Fits ALL" methods or operations for creating a special file storage for your different data requirements that you may have for your program. Some testing and experiments with the file code methods here can teach you how to write and read many different kinds of data files. The next page of these Lessons, 15A - Program Code for UseFiles, has code examples for file writting and reading. . Also I have a web page here at DelphiZeus about creating custom multi-data files with the TFileStream at - Building Custom Files using TFileStream     Although this page uses the TFileStream, the file byte arrangement methods still apply to a "File" no matter what you use to write the file. If you have time you may want to read some of this web page. The sections about File Headers, Other File Header Data, and Using Data Segment IDs to Separate Data may give you some additional Information.


Tab Controls

A windows Tab Control is like the dividers (folders) in an office file cabinet with "Tab" labels, , or like the section dividers in a notebook. You have probally seen many windows tab controls, since they are a useful and widely used control. When using a tab control, your program can have multiple pages or sections used in the same area of a window or dialog box. Each page can be a separate window container for controls, that is shown or hidden with a tab click, or a visual display changed when the user clicks the corresponding tab. You can also have a a style of tab control that displays buttons instead of tabs. You should read the Win32 Help for "Tab Controls" or look at the MSDN web library about Tab Controls at - MSDN-Tab Controls, Overview , and read the several many web pages there. The tab control is a common control so you will need to call InitCommonControls before you create any tab controls. You can create a Tab Control with the CreateWindowEx( ) function using the windows.pas constant WC_TABCONTROL for the Class Name. There are several style options that Tab Controls can use, you will need to look at your Win32 API Help for the style bit flags and what they do. A Tab Control will have the common style as the default, which includes the flags-
TCS_TABS   - Shows Tabs (not buttons) and draws 3D border
TCS_RAGGEDRIGHT   - Will not stretch tabs to fill all the width
TCS_SINGLELINE   - only one line of tabs
Which will create a Tab Control with common features that many people have seen before.

Here is the code used to create a Tab Control -

hTab1 := CreateWindowEx(WS_EX_CONTROLPARENT, WC_TABCONTROL, nil,
           WS_VISIBLE or WS_CHILD or BS_NOTIFY or WS_TABSTOP,
           3,3, 200, 200, hForm1, ID_TabCtrl, hInstance,nil);
I do not have any of the tab control style flags in this, so it will have the default style. I include the WS_EX_CONTROLPARENT so the child windows (pages) can pass the tab key focus change messages. I include the BS_NOTIFY to have the tab notify messages go to it's parent window, and the WS_TABSTOP flag is there so it will respond to the dialog tab key control changes, although you may not want a Tab Control to be in the tab key control navigation. There are other Tab Control creation flag bits, which you should read about in the Win32 Help. Tab Controls are more complex in the code methods used to create one than a button or edit control, since you must add "Tab Items", and also add and size container windows as pages for it. I can not talk about all of the ways (options, styles, flags, tab items, and pages) that are available to tab controls, I will present some code methods to make a tab control that is like the ones I mostly use.


Tab Control Items, the Tabs -

To tell the tab control to display a "Tab" or item, you will need to send it a TCM_INSERTITEM message or use the API function TabCtrl_InsertItem( ). Both of these methods use the TTCItem record (TC_ITEM stucture in Win32 API Help) to give the tab's display information. The TTCItem record looks like this -

  PTCItem = ^TTCItem;
  tagTCITEMA = packed record
    mask: UINT; // indicates which record members to use
    dwState: UINT; // display state of tab
    dwStateMask: UINT; // indicates which State bits to use
    pszText: PChar; // pointer to string for tab text
    cchTextMax: Integer; // size of buffer to recieve pszText
    iImage: Integer; // index to tab control's image
    lParam: Integer; // Integer, your data associated with this tab
  end;
  TTCItem = tagTCITEMA;
The mask member of this record can have these values -
ValueMeaning
TCIF_TEXTUse the pszText member for tab text.
TCIF_IMAGEUse the iImage member for tab image index.
TCIF_PARAMUse the lParam member for integer data.
TCIF_STATEUse the dwState member for tab display state. This member is ignored in the TCM_INSERTITEM message.
TCIF_RTLREADINGDisplays the tab text using right-to-left reading order on language systems with that order (Arabic or Hebrew).
TCIF_ALLUse All of the members above.

In the code examples here I will not be using the any Tab Controls with images and an Image List, so I will not use the TCIF_IMAGE member. If you assigned an Image List to this tab control, the TCIF_IMAGE member would have the Image List index for the tab image. I also will not be using the TCIF_STATE member in these lessons, it can have information about if the tab is highlited or if the tab is shown as "button down" if the TCS_BUTTONS style is used. This state information is mostly used for owner drawn tabs.

After creating the tab control, I will set the each tab by using the TCM_INSERTITEM message with the TTCItem record for that tab in the LParam of the message. You could use the following code with TCM_INSERTITEM in SendMessage( ) to insert a Tab Item into a tab control -

var
TabInfo: TTCItem;
- - - - - - - - - - -

TabInfo.mask := TCIF_TEXT or TCIF_PARAM;
{ the mask tells which members of the TTCItem to read and use
 TCIF_TEXT will use the pszText  member
 TCIF_PARAM will use the lParam  member }
TabInfo.pszText := ' Tab Text ';
// the pszText is a PChar that has the Text on the tab item
TabInfo.lParam := hPage1;
{ the lParam is a user defined integer that you can use to
  store information for each separate tab}

SendMessage(hTab1, TCM_INSERTITEM, Zero, Integer(@TabInfo));
{ you will need to send the TCM_INSERTITEM message
  for each Tab you want on the control }
The code above will insert one tab that has the caption "Tab Text", each tab will be automaticaly sized to fit the text and image of this TabInfo record. I have a space before and after the text to make the Tab look better. If you need to store information in a tab control's Tab reference, then you can set the TabInfo.lParam to some data (integer or pointer) that you can access later. You can now use the code above with the TCM_INSERTITEM message to add as many tabs, as you need, to the tab control. There are 2 tab control messages that are used to get and set tab item properties, the TCM_GETITEM and TCM_SETITEM messages, with a TTCItem record in the LParam. Code to use these messages are in the UseFiles.pas unit, in the GetTabInfo and SetTabInfo procedures, the methods are as the Win32 help describes, except with the TCM_SETITEM message, where you will need to refresh (repaint) the current page control.

Creating Windows as Pages shown on a Tab Control-
Most Tab Controls are containers for several "Pages" which are windows that are also containers for several other controls that display information and get user input. I will use three different kinds of windows as tab control "Pages" in the UseRilesU.pas unit, the first page is a multi-line Edit control, as a text editor might have. The second page is a system Static window, that I use as a container window and subclass it to get child control messages. I have several child edit and button controls on this second page. The third page is a "Panel" control created from the MakeApp.pas Unit, this Panel page has several controls on it. When these pages are created, the hPage1 is visible and can be seen by the user, and the hPage2 and hPage3 are hidden and not seen. Since you will have to create the "Pages" on a tab control, you will need a way to get the size of the pages, that is the area of the tab control where the tabs do not paint. You can use the TCM_ADJUSTRECT message or the TabCtrl_AdjustRect( ) macro function to have the system adjust a TRect to the page display area. Like this code -

var
TabRect: TRect;
- - - - - - - - - -

GetClientRect(hTab1, TabRect);
TabCtrl_AdjustRect(hTab1, False, @TabRect);
TabRect.Right := TabRect.Right - TabRect.Left;
TabRect.Bottom := TabRect.Bottom - TabRect.Top;
hPage1 := CreateWindowEx(WS_EX_CLIENTEDGE, 'EDIT', nil, WS_VISIBLE
    or WS_CHILD or ES_LEFT or ES_AUTOVSCROLL or WS_VSCROLL or
    WS_HSCROLL or ES_MULTILINE, TabRect.Left, TabRect.Top,
    TabRect.Right, TabRect.Bottom, hTab1, 0, hInstance, nil);
The code above gets the page area in the TabRect, and adjusts the Right and Bottom to the height and width needed. Then an edit is created as a page (hPage1) with the TabRect position and size. For most tab pages, the window size for all pages will be the same, but you will only have one page be visible, the rest should be hidden.

Changing the Tab Pages
A tab control does NOT have any methods to "Change Pages" when a new tab is clicked by the user, but if you have the BS_NOTIFY style bit in a tab control creation, it sends a WM_NOTIFY message to it's parent (hForm1) to tell it that the tab selection has changed, the WParam in this WM_NOTIFY message is the ID number of the tab control. But there is NO information in this WM_NOTIFY message about which tab is selected, but in the LParam which is a NMHDR record, will tell you in the NMHDR.code if TCN_SELCHANGE or TCN_SELCHANGING are happening. The TCN_SELCHANGING is sent when a tab is being UN-selected, and the TCN_SELCHANGE is sent after a tab has been selected, but there is no info about which tab it is. You will have to call -
    TabIndex := SendMessage(hTab1, TCM_GETCURSEL,0,0);
the TCM_GETCURSEL message will return the index of the tab the message was for. Since the tab control does not change the pages you will need code to hide and show the page windows as the tab selection changes. As in this code for the hForm1 window proc -

  WM_NOTIFY:
    begin
    // check to see if tab control with  wParam = ID_TabCtrl
    if wParam = ID_TabCtrl then
        if PNMHdr(lParam).code = TCN_SELCHANGING then
        begin
        { the TCN_SELCHANGING signals a tab is now UN-selected
          but you will need to get which tab this message is for
          with the TCM_GETCURSEL message}
        case SendMessage(hTab1, TCM_GETCURSEL,0,0) of
          0: ShowWindow(hPage1, SW_HIDE);
          1: ShowWindow(hPage2, SW_HIDE);
          2: ShowWindow(hPage3, SW_HIDE);
          end; // hide the page that is now Un-selected
        Exit; // stop code, you do not need to call DefWindowProc
        end else
        if PNMHdr(lParam).code = TCN_SELCHANGE then
        begin
        // the TCN_SELCHANGE signals a new tab is selected
        case SendMessage(hTab1, TCM_GETCURSEL,0,0) of
          0: ShowWindow(hPage1, SW_SHOW);
          1: ShowWindow(hPage2, SW_SHOW);
          2: ShowWindow(hPage3, SW_SHOW);
          end; // show the page that is now selected
        Exit;
        end
The WM_NOTIFY code above is for a tab control with 3 pages. The WM_NOTIFY message is processed by the tab controls parent, the LParam is typecast to a PNMHdr and the code of that record is tested for TCN_SELCHANGING and TCN_SELCHANGE. I use a Case-Of code block with the TCM_GETCURSEL message result, to execute code for each different tab selection change. The code above is basic and only hides and shows a window on selection change, you will need to add your code to do things required for each different page change. The moving the control focus to a control on the visible page is a common task in change of page selection.


Shell Special Folders

There are several "Shell" information functions that use the system's Shell Namespace, which is what the user see's as the tree-connected organization of Windows Explorer folders, the root (start) of this folder tree is the Desktop (a virtual shell folder). The disk drives contents (directory stucture) is part of the Shell namespace, but the shell also has "Virtual" folders (folders that do NOT exist on any disk) such as "Control Panel" and "Printers". Every shell folder is an OLE component object model (COM) object, that uses code methods to show its contents and carry out other folder item (file object) actions. In order for Windows Explorer to display folder items and access these items, it implements an IShellFolder interface for each folder. I will not try an explain the IShellFolder interface or the interface COM methods here, which is alot of information. In this lesson I use the SHGetSpecialFolderLocation( ) function to get the "Personal Folder" (My Documents) for the current user. You should look at your Win32 Help for "SHGetSpecialFolderLocation", or go to MSDN web library and see - MSDN-SHGetSpecialFolderLocation( )
function SHGetSpecialFolderLocation(

  hwndOwner: HWND; // window that owns any message box display
  nFolder: Integer; // flag for the special shell folder you want
  var ppidl: PItemIDList // Item List receives the special folder

): HResult; // integer type that indicates success
    Parameters -
nFolder: A number flag that tells the system which special shell folder file path that you want. A few flags for some special shell folders -
  CSIDL_PERSONAL - pesonal folder on disk (My Documents)
  CSIDL_SENDTO - Send To folder on disk
  CSIDL_STARTMENU - Start Menu folder on disk
  CSIDL_DESKTOP - virtual Desktop folder

ppidl: This PItemIDList is allocated and filled with a memory address of the ID list with the system information for the shell folder. If the shell folder is "Real" and on a disk, it has the file path for that folder, which is recorded in the windows registry. If it is a virtual folder (not on a disk), then this PItemIDList is NOT a reference to any disk file path.

Shell namespace Objects (folder items) are assigned item identifiers and item identifier lists. Item identifiers are numbers (like a Handle) that are used to access a folder item, unlike Handles, you do not use item IDs in code, instead you use a PItemIDList type, this "List" pointer allows more information to be passed. An item identifier list (PItemIDList type) has shell information (ID) and the path to the items from the desktop, also it can contain information about more than one item, because it is a List. A pointer to an "Item Identifier List", sometimes called a PIDL (pronounced piddle), is used with SHGetSpecialFolderLocation and many shell functions.
  IMPORTANT - The system will allocate and fill the memory used for this PItemIDList passed in a shell function, but it does NOT track this assigned memory block, so you should ALWAYS have code to FREE and release the PItemIDList system allocated memory. This is done with the IMAlloc interface, the Shell Memory Allocator. You could get this system IMAlloc interface with the CoGetMalloc( ) function. But to free a shell created PItemIDList, you can use the CoTaskMemFree( ) procedure, which will use the system IMAlloc to free it.

Once you have a item ID list in a PItemIDList, you can use this list in other Shell functions to pass folder item information. In this lesson I need a folder path for the CreateFile( ) function, which is not a shell function, so I need to get file path as text from the PItemIDList. To do this I use the SHGetPathFromIDList( ) function -

function SHGetPathFromIDList(
  pidl: PItemIDList; // Item List with the folder path
  pszPath: PChar // PChar that gets the text of the file path
  ): BOOL; // returns true if successful, list item is on disk
The pidl parameter needs to be an ID List for a folder item that has a "Real" disk location (this function fails if it is a "Virtual" folder). The pszPath is for a text memory block to get the text of the path, with at least MAX_PATH bytes in it. This MAX_PATH amount is left over from the old 16 bit windows FAT16 days, I would suggest using a kilobyte (1024) or larger text buffer.


Code to get Special Shell Folders

Below is code to get the Personal Folder (known as My Documents) for the current user and put it in the PersonalFolder string. It requires the Shell function SHGetSpecialFolderLocation( ). The system Shell Folder interface gets all of the "Special" folder disk file paths from reading the registry. So for any special folder there may NOT be any registry information for a folder path , , or the folder path in the registry may be to a folder that does NOT exist. You should ALWAYS check and see if the folder exists. Remember to free the pSpecialDir with CoTaskMemFree( ).

var
pSpecialDir : PItemIdList;
PersonalFolder: String;

begin
SHGetSpecialFolderLocation(hForm1, CSIDL_PERSONAL, pSpecialDir);
{there are several folders recorded in the system for each
 User, that the SHGetSpecialFolderLocation function can get the
 path for, the CSIDL_PERSONAL gets My Documents }
SetLength(PersonalFolder, 1023);
PersonalFolder[1] := #0;
 {you will need to get a text path from the PItemIdList
 with SHGetPathFromIDList. Remember, it is posible that there
 is NO personal folder registered for this user}
SHGetPathFromIDList(pSpecialDir, PChar(PersonalFolder));
// now PersonalFolder has the file path to folder, if registered
CoTaskMemFree(pSpecialDir);
// ALWAYS Free any PItemIdList you get from the system
SetLength(PersonalFolder, PCharLength(PChar(PersonalFolder)));
if not DirectoryExists(PersonalFolder)) then
    PersonalFolder := 'C:\';
end;
You can use the "Shell" functions to access data that the OS uses for it's Explorer Shell display.
You can see code examples for the File access, Tab Control, and Shell Special folder, in the next lesson page for the UseFiles program.



                           

Next Page
There are code examples for all of the subjects of this lesson in the UseFiles program code.
Lesson 15A, Program Code for UseFiles.


       

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




H O M E