Home |
15. Writing and Reading Files How to Create File Handles and use File Bytes as Data |
Home |
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 - There are code examples for the subjects of this lesson in the UseFiles program code in the next Lesson 15A, Program Code for UseFiles.
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 fileHere 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.
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.
Code to Read and Write a Text File GetFileSize( ) Function function GetFileSize( hFile: THandle; // file handle for open file lpFileSizeHigh: PDWORD // pointer for the size bits over MaxDWORD ): Cardinal; // returns the file size below MaxDWORDAs 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 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 successfulReadFile( ) 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). 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 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 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)^ |
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 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 - 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.
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 File Data Postion and Order, Do the Math -
Fixed Size Variables - 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 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 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 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 - 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 File Type Identifier, an Important File Header 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 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 -
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.-
Many Other Ways to Make Files 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.
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 -
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- 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 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; endThe 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 successParameters - 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. 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 diskThe 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.
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.