Another core area of the Delphi class library is its support for streaming, which includes file management, memory, sockets, and other sources of information arranged in a sequence. The idea of streaming is that you move through the data while reading it, much like the Read and Write functions traditionally used by the Pascal language (and discussed in Chapter 12 of Essential Pascal (see Appendix C for availability of this e-book).
The VCL defines the abstract TStream class and several subclasses. The parent class, TStream, has just a few properties, and you'll never create an instance of it, but it has an interesting list of methods you'll generally use when working with derived stream classes.
The TStream class defines two properties, Size and Position. All stream objects have a specific size (which generally grows if you write something after the end of the stream), and you must specify a position within the stream where you want to either read or write information.
Reading and writing bytes depends on the actual stream class you are using, but in both cases you don't need to know much more than the size of the stream and your relative position in the stream to read or write data. In fact, that's one of the advantages of using streams. The basic interface remains the same whether you're manipulating a disk file, a binary large object (BLOB) field, or a long sequence of bytes in memory.
In addition to the Size and Position properties, the TStream class also defines several important methods, most of which are virtual and abstract. (In other words, the TStream class doesn't define what these methods do; therefore, derived classes are responsible for implementing them.) Some of these methods are important only in the context of reading or writing components within a stream (for instance, ReadComponent and WriteComponent), but some are useful in other contexts, too. In Listing 4.2, you can find the declaration of the TStream class, extracted from the Classes unit.
TStream = class(TObject) public // read and write a buffer function Read(var Buffer; Count: Longint): Longint; virtual; abstract; function Write(const Buffer; Count: Longint): Longint; virtual; abstract; procedure ReadBuffer(var Buffer; Count: Longint); procedure WriteBuffer(const Buffer; Count: Longint); // move to a specific position function Seek(Offset: Longint; Origin: Word): Longint; overload; virtual; function Seek(const Offset: Int64; Origin: TSeekOrigin): Int64; overload; virtual; // copy the stream function CopyFrom(Source: TStream; Count: Int64): Int64; // read or write a component function ReadComponent(Instance: TComponent): TComponent; function ReadComponentRes(Instance: TComponent): TComponent; procedure WriteComponent(Instance: TComponent); procedure WriteComponentRes(const ResName: string; Instance: TComponent); procedure WriteDescendent(Instance, Ancestor: TComponent); procedure WriteDescendentRes( const ResName: string; Instance, Ancestor: TComponent); procedure WriteResourceHeader(const ResName: string; out FixupInfo: Integer); procedure FixupResourceHeader(FixupInfo: Integer); procedure ReadResHeader; // properties property Position: Int64 read GetPosition write SetPosition; property Size: Int64 read GetSize write SetSize64; end;
The basic use of a stream involves calling the ReadBuffer and WriteBuffer methods, which are very powerful but not terribly easy to use. The first parameter is an untyped buffer in which you can pass the variable to save from or load to. For example, you can save into a file a number (in binary format) and a string, with this code:
var stream: TStream; n: integer; str: string; begin n := 10; str := 'test string'; stream := TFileStream.Create ('c:\tmp\test', fmCreate); stream.WriteBuffer (n, sizeOf(integer)); stream.WriteBuffer (str, Length (str)); stream.Free;
An alternative approach is to let specific components save or load data to and from streams. Many VCL classes define a LoadFromStream or a SaveToStream method, including TStrings, TStringList, TBlobField, TMemoField, TIcon, and TBitmap.
Creating a TStream instance makes no sense, because this class is abstract and provides no direct support for saving data. Instead, you can use one of the derived classes to load data from or store it to an actual file, a BLOB field, a socket, or a memory block. Use TFileStream when you want to work with a file, passing the filename and some file access options to the Create method. Use TMemoryStream to manipulate a stream in memory and not an actual file.
Several units define TStream-derived classes. The Classes unit includes the following classes:
THandleStream defines a stream that manipulates a disk file represented by a file handle.
TFileStream defines a stream that manipulates a disk file (a file that exists on a local or network disk) represented by a filename. It inherits from THandleStream.
TCustomMemoryStream is the base class for streams stored in memory but is not used directly.
TMemoryStream defines a stream that manipulates a sequence of bytes in memory. It inherits from TCustomMemoryStream.
TStringStream provides a simple way to associate a stream to a string in memory, so that you can access the string with the TStream interface and also copy the string to and from another stream.
TResourceStream defines a stream that manipulates a sequence of bytes in memory, and provides read-only access to resource data linked into the executable file of an application (the DFM files are an example of this resource data). It inherits from TCustomMemoryStream.
Stream classes defined in other units include the following:
TBlobStream defines a stream that provides simple access to database BLOB fields. There are similar BLOB streams for database access technologies other than the BDE, including TSQLBlobStream and TClientBlobStream. (Notice that each type of dataset uses a specific stream class for BLOB fields.) All these classes inherit from TMemoryStream.
TOleStream defines a stream for reading and writing information over the interface for streaming provided by an OLE object.
TWinSocketStream provides streaming support for a socket connection.
Creating and using a file stream can be as simple as creating a variable of a type that descends from TStream and calling components' methods to load content from the file:
var S: TFileStream; begin if OpenDialog1.Execute then begin S := TFileStream.Create (OpenDialog1.FileName, fmOpenRead); try Memo1.Lines.LoadFromStream (S); finally S.Free; end; end; end;
As you can see in this code, the Create method for file streams has two parameters: the name of the file and a flag indicating the requested access mode. In this case, you want to read the file, so you use the fmOpenRead flag (other available flags are documented in the Delphi help).
Of the different modes, the most important are fmShareDenyWrite, which you'll use when you're simply reading data from a shared file, and fmShareExclusive, which you'll use when you're writing data to a shared file. There is a third parameter in TFileStream.Create, called Rights. This parameter is used to pass file access permissions to the Linux filesystem when the access mode is fmCreate (that is, only when you are creating a new file). This parameter is ignored on Windows.
A big advantage of streams over other file access techniques is that they're very interchangeable, so you can work with memory streams and then save them to a file, or you can perform the opposite operations. This might be a way to improve the speed of a file-intensive program. Here is a snippet of a file-copying function to give you another idea of how you can use streams:
procedure CopyFile (SourceName, TargetName: String); var Stream1, Stream2: TFileStream; begin Stream1 := TFileStream.Create (SourceName, fmOpenRead); try Stream2 := TFileStream.Create (TargetName, fmOpenWrite or fmCreate); try Stream2.CopyFrom (Stream1, Stream1.Size); finally Stream2.Free; end finally Stream1.Free; end end;
Another important use of streams is to handle database BLOB fields or other large fields directly. You can export such data to a stream or read it from one by calling the SaveToStream and LoadFromStream methods of the TBlobField class.
Delphi 7 streaming support adds a new exception base class, EFileStreamError. Its constructor takes as parameter a filename for error reporting. This class standardizes and largely simplifies the notification of file-related errors in streams.
By themselves, the VCL stream classes don't provide much support for reading or writing data. In fact, stream classes don't implement much beyond simply reading and writing blocks of data. If you want to load or save specific data types in a stream (and don't want to perform a great deal of typecasting), you can use the TReader and TWriter classes, which derive from the generic TFiler class.
Basically, the TReader and TWriter classes exist to simplify loading and saving stream data according to its type, and not just as a sequence of bytes. To do this, TWriter embeds special signatures into the stream that specify the type for each object's data. Conversely, the TReader class reads these signatures from the stream, creates the appropriate objects, and then initializes those objects using the subsequent data from the stream.
For example, I could have written out a number and a string to a stream by writing:
var stream: TStream; n: integer; str: string; w: TWriter; begin n := 10; str := 'test string'; stream := TFileStream.Create ('c:\tmp\test.txt', fmCreate); w := TWriter.Create (stream, 1024); w.WriteInteger (n); w.WriteString (str); w.Free; stream.Free;
This time the file will include the extra signature characters, so I can read back this file only by using a TReader object. For this reason, using TReader and TWriter is generally confined to component streaming and is seldom applied in general file management.
In Delphi, streams play a considerable role in persistency. For this reason, many methods of TStream relate to saving and loading a component and its subcomponents. For example, you can store a form in a stream by writing
If you examine the structure of a Delphi DFM file, you'll discover that it's really just a resource file that contains a custom format resource. Inside this resource, you'll find the component information for the form or data module and for each of the components it contains. As you would expect, the stream classes provide two methods to read and write this custom resource data for components: WriteComponentRes to store the data, and ReadComponentRes to load it.
For your experiment in memory (not involving DFM files), though, using WriteComponent is generally better suited. After you create a memory stream and save the current form to it, the problem is how to display it. You can do this by transforming the form's binary representation to a textual representation. Even though the Delphi IDE, since version 5, can save DFM files in text format, the representation used internally for the compiled code is invariably a binary format.
The IDE can accomplish the form conversion, generally with the View as Text command of the Form Designer, and in other ways. The Delphi Bin directory also contains a command-line utility, CONVERT.EXE. Within your own code, the standard way to obtain a conversion is to call the specific VCL methods. There are four functions for converting to and from the internal object format obtained by the WriteComponent method:
procedure ObjectBinaryToText(Input, Output: TStream); overload; procedure ObjectBinaryToText(Input, Output: TStream; var OriginalFormat: TStreamOriginalFormat); overload; procedure ObjectTextToBinary(Input, Output: TStream); overload; procedure ObjectTextToBinary(Input, Output: TStream; var OriginalFormat: TStreamOriginalFormat); overload;
Four different functions, with the same parameters and names containing the name Resource instead of Binary (as in ObjectResourceToText), convert the resource format obtained by WriteComponentRes. A final method, TestStreamFormat, indicates whether a DFM is storing a binary or textual representation.
In the FormToText program, I've used the ObjectBinaryToText method to copy the binary definition of a form into another stream, and then I've displayed the resulting stream in a memo, as you can see in Figure 4.5. Here is the code of the two methods involved:
procedure TformText.btnCurrentClick(Sender: TObject); var MemStr: TStream; begin MemStr := TMemoryStream.Create; try MemStr.WriteComponent (Self); ConvertAndShow (MemStr); finally MemStr.Free end; end; procedure TformText.ConvertAndShow (aStream: TStream); var ConvStream: TStream; begin aStream.Position := 0; ConvStream := TMemoryStream.Create; try ObjectBinaryToText (aStream, ConvStream); ConvStream.Position := 0; MemoOut.Lines.LoadFromStream (ConvStream); finally ConvStream.Free end; end;
Notice that by repeatedly clicking the Current Form Object button you'll get more and more text, and the text of the memo is included in the stream. After a few times, the entire operation will become extremely slow, until the program seems to be hung up. In this code, you see some of the flexibility of using streams—you can write a generic procedure that you can use to convert any stream.
It's important to stress that after you've written data to a stream, you must explicitly seek back to the beginning (or set the Position property to 0) before you can use the stream further—unless you want to append data to the stream, of course.
Another button, labeled Panel Object, shows the textual representation of a specific component, the panel, passing the component to the WriteComponent method. The third button, Form in Executable File, performs a different operation. Instead of streaming an existing object in memory, it loads in a TResourceStream object the design-time representation of the form—that is, its DFM file—from the corresponding resource embedded in the executable file:
procedure TFormText.btnResourceClick(Sender: TObject); var ResStr: TResourceStream; begin ResStr := TResourceStream.Create(hInstance, 'TFORMTEXT', RT_RCDATA); try ConvertAndShow (ResStr); finally ResStr.Free end; end;
By clicking the buttons in sequence (or modifying the form of the program) you can compare the form saved in the DFM file to the current run-time object.
A new feature of Delphi 7 is official support for the ZLib compression library (available and described at www.gzip.org/zlib). A unit interfacing ZLib has been available for a long time on Delphi's CD, but now it is included in the core distribution and is part of the VCL source (the ZLib and ZLibConst units). In addition to providing an interface to the library (which is a C library you can directly embed in the Delphi program, with no need to distribute a DLL), Delphi 7 defines a couple of helper stream classes: TCompressStream and TDecompressStream.
As an example of using these classes, I've written a small program called ZCompress that compresses and decompresses files. The program has two edit boxes in which you enter the name of the file to compress and the name of the resulting file, which is created if it doesn't already exist. When you click the Compress button, the source file is used to create the destination file; clicking the Decompress button moves the compressed file back to a memory stream. In both cases, the result of the compression or decompression is displayed in a memo. Figure 4.6 shows the result for the compressed file (which happens to be the source code of the form of the current program).
To make the code of this program more reusable, I've written two functions for compressing or decompressing a stream into another stream. Here is the code:
procedure CompressStream (aSource, aTarget: TStream); var comprStream: TCompressionStream; begin comprStream := TCompressionStream.Create( clFastest, aTarget); try comprStream.CopyFrom(aSource, aSource.Size); comprStream.CompressionRate; finally comprStream.Free; end; end; procedure DecompressStream (aSource, aTarget: TStream) ; var decompStream: TDecompressionStream; nRead: Integer; Buffer: array [0..1023] of Char; begin decompStream := TDecompressionStream.Create(aSource); try // aStreamDest.CopyFrom (decompStream, size) doesn't work // properly as you don't know the size in advance, // so I've used a similar "manual" code repeat nRead := decompStream.Read(Buffer, 1024); aTarget.Write (Buffer, nRead); until nRead = 0; finally decompStream.Free; end; end;
As you can see in the code comment, the decompression operation is slightly more complex because you cannot use the CopyFrom method: You don't know the size of the resulting stream in advance. If you pass 0 to the method, it will try to get the size of the source stream, which is a TDecompressionStream. However, this operation causes an exception, because the compression and decompression streams can be read only from the beginning to the end and don't allow for seeking the end of the file.