How to write filters that extend the functionality of the TStream classes
Why do it?
In Java there are various predefined stream classes that provide filters for other stream classes. The filter classes essentially "wrap" the streams they operate on and can often be applied to further filters. This article demonstrates how we can do this in Delphi in a way that is extendable – i.e. we can wrap filters around other filters.
How it's done
First of all, let's look at why we want to do this. Well, say you want to write some data primitives as text to a stream and for the text to be formatted to fit on a page, word wrapping properly. Then, if we can wrap a filter that formats the primitives around another that formats the text and this filter is wrapped round a file stream object, then all we have to do is access the methods of the first class and the rest of the process happens automatically.
The approach I've taken is to define a class, TStreamWrapper, that provides a base class for any filters that we want to define. Any TStreamWrapper performs it's i/o using another TStream object – the wrapped object. The key point is that TStreamWrapper is itself derived from TStream, so that it can also wrap other TSteamWrapper objects – giving the extensibility we need. TSteamWrapper can also cause a wrapped stream to be freed when it is itself freed – allowing the wrapped streams to be created "on the fly" when the TSteamWrapper constructor is called.
There is no additional functionality built in to TSteamWrapper – this is to be provided by derived classes. A small example class is demonstrated here.
First we look at TSteamWrapper. Here's the class declaration:
2 TStreamWrapper = class(TStream)
4 FBaseStream: TStream;
6 FCloseStream: Boolean;
9 procedure SetSize(NewSize: Longint); override;
12 property BaseStream: TStream read FBaseStream;
16 constructor Create(const Stream: TStream;
17 const CloseStream: Boolean = False); virtual;
20 destructor Destroy; override;
22 function Read(var Buffer; Count: Longint): Longint;
24 function Write(const Buffer; Count: Longint): Longint;
26 function Seek(Offset: Longint; Origin: Word): Longint;
and the implementation is:
1constructor TStreamWrapper.Create(const Stream: TStream;
2 const CloseStream: Boolean);
4 inherited Create;
6 FBaseStream := Stream;
7 FCloseStream := CloseStream;
13 if FCloseStream then
15 inherited Destroy;
18function TStreamWrapper.Read(var Buffer;
19 Count: Integer): Longint;
22 Result := FBaseStream.Read(Buffer, Count);
25function TStreamWrapper.Seek(Offset: Integer;
26 Origin: Word): Longint;
29 Result := FBaseStream.Seek(Offset, Origin);
32procedure TStreamWrapper.SetSize(NewSize: Integer);
35 FBaseStream.Size := NewSize;
38function TStreamWrapper.Write(const Buffer;
39 Count: Integer): Longint;
42 Result := FBaseStream.Write(Buffer, Count);
We can now derive a small filter class – TStrStream. As it stands it's not particularly useful, but does demonstrate the techniques. The class reads and writes strings (which are preceded by their lengths) from and to any stream. The declaration is:
2 TStrStream = class(TStreamWrapper)
4 procedure WriteString(AString: string);
5 function ReadString: string;
The class is implemented as follows:
1function TStrStream.ReadString: string;
3 StrLen: Integer;
4 PBuf: PChar;
7 ReadBuffer(StrLen, SizeOf(Integer));
10 GetMem(PBuf, StrLen);
13 ReadBuffer(PBuf^, StrLen);
14 SetString(Result, PBuf, StrLen);
17 FreeMem(PBuf, StrLen);
21procedure TStrStream.WriteString(AString: string);
23 Len: Integer;
26 Len := Length(AString);
27 WriteBuffer(Len, SizeOf(Integer));
29 WriteBuffer(PChar(AString)^, Len);
The following code should demonstrate how to write a string to a file and read it back in again. Here we use a file stream that is created on the fly and automatically closed when we are done. Of course you could use any stream type and handle its lifetime yourself.
1procedure WriteText(const Txt: string);
3 TS: TTextStream;
7 TS := TTextStream.Create(
8 TFileStream.Create('test.dat', fmCreate), True);
13function ReadText: string;
15 TS: TTextStream;
17 TS := TTextStream.Create(
18 TFileStream.Create('test.dat', fmCreate), True);
19 Result := TS.ReadString;
The filter in this example provides additional methods to those in TSteamWrapper. We can also provide filters that override the Read and Write methods to alter the way that files are written.
A demo program to accompany this article can be found in the
delphidabbler/article-demos Git repository on GitHub.
You can view the code in the
article-05 sub-directory. Alternatively download a zip file containing all the available demos by going to the repository's landing page and clicking the Clone or download button and selecting Download ZIP.
The demo project is a Delphi 4 project that writes out some user provided strings to a file using the TStrStream class, and reads them back in again.
This source code is merely a proof of concept and is intended only to illustrate this article. It is not designed for use in its current form in finished applications. The code is provided on an "AS IS" basis, WITHOUT WARRANTY OF ANY KIND, either express or implied.
The demo is open source. See the demo's LICENSE.md file for licensing details.
I hope you found this article useful.
If you have any observations, comments, or have found any errors there are two places you can report them.
- For anything to do with the article content, but not the downloadable demo code, please use this website's Issues page on GitHub. Make sure you mention that the issue relates to "article #5".
- For bugs in the demo code see the
README.md file for details of how to report them.