Streaming components
│
Deutsch (de) │
English (en) │
français (fr) │
日本語 (ja) │
polski (pl) │
português (pt) │
Introduction
Normally, when you want to store data on disk or to network streams, you must write code for loading and saving each property. This tutorial describes how to write classes, that can be loaded from and saved to streams without writing extra load/save code by using the RTTI.
There is an example in the lazarus sources, demonstrating how to save a TGroupBox with a TCheckBox child to a stream and read the stream back to create a copy of both components.
See <lazaruspath>/examples/componentstreaming/
In combination with RTTI controls you can reduce the amount of code needed for connecting the program Data with the GUI and the Disk/Network to a minimum.
TComponent / TPersistent
The class TPersistent is defined in the unit Classes and is uses the {$M+} compiler switch. This switch tells the compiler to create Run Time Type Information (RTTI). This means it and all its descendants get a new class section published. 'Published' properties are visible as 'public', but additionally their structure is accessible at run time. That means all published properties can be read and written at run time. The IDE for instance uses this to work with components it never heard of.
TComponent extends TPersistent by the ability to have child components. This is important for streaming, where one component is the root component also called lookup root with a list of child components.
TReader / TWriter
These are the worker classes, which reads/writes a TComponent to/from a stream (See CreateLRSReader and CreateLRSWriter). They use a Driver to read/write a special format. At the moment there are a reader (TLRSObjectReader) and a writer (TLRSObjectWriter) for binary object format defined in the LResources unit and a writer (TXMLObjectWriter) for TDOMDocument defined in Laz_XMLStreaming. The LResources unit also contains functions to convert binary format to text and back (LRSObjectBinaryToText, LRSObjectTextToBinary). The LCL prefers UTF8 for strings, while Delphi prefers Widestrings. So there are some conversion functions as well.
Writing your own component - Part 1
A custom component can be as simple as: type
TMyComponent = class(TComponent) private FID: integer; published property ID: integer read FID write FID; end;
Writing a component to a stream
The unit LResources has a function for that:
procedure WriteComponentAsBinaryToStream(AStream: TStream; AComponent: TComponent);
It writes a component in binary format to the stream. For example:
procedure TForm1.Button1Click(Sender: TObject); var AStream: TMemoryStream; begin AStream:=TMemoryStream.Create; try WriteComponentAsBinaryToStream(AStream,AGroupBox); ... save stream somewhere ... finally AStream.Free; end; end;
Reading a component from a stream
The unit LResources has a function for that:
procedure ReadComponentFromBinaryStream(AStream: TStream; var RootComponent: TComponent; OnFindComponentClass: TFindComponentClassEvent; TheOwner: TComponent = nil);
- AStream is the stream containing one component in binary format. Everything behind that component in the stream is not read, including other components.
- RootComponent is either an existing component, which data will be overwritten, or it is nil and a new component will be created.
- OnFindComponentClass is a function, that is used by TReader to get the class from the classnames in the stream. For example:
procedure TCompStreamDemoForm.OnFindClass(Reader: TReader; const AClassName: string; var ComponentClass: TComponentClass); begin if CompareText(AClassName,'TGroupBox')=0 then ComponentClass:=TGroupBox else if CompareText(AClassName,'TCheckBox')=0 then ComponentClass:=TCheckBox; end;
- TheOwner is the component owner, when creating a new component.
Streamable properties
There are some limitations, what types TReader/TWriter can stream:
- Base types can be streamed: string, integer, char, single, double, extended, byte, word, cardinal, shortint, method pointers, etc. .
- TPersistent and descendants can be streamed
- records, objects and classes not descending from TPersistent can not be streamed. To stream them you need to tell TReader/TWriter how. See below #Streaming custom Data - DefineProperties.
Streaming custom Data - DefineProperties
You can stream additional arbitrary data by overriding DefineProperties. This allows to stream all data, that have no base types. For example to stream a variable FMyRect: TRect of your component, add the following three methods to your component:
procedure DefineProperties(Filer: TFiler); override; procedure ReadMyRect(Reader: TReader); procedure WriteMyRect(Writer: TWriter);
With the following code:
procedure TMyComponent.DefineProperties(Filer: TFiler); var MyRectMustBeSaved: Boolean; begin inherited DefineProperties(Filer); MyRectMustBeSaved:=(MyRect.Left<>0) or (MyRect.Top<>0) or (MyRect.Right<>0) or (MyRect.Bottom<>0); Filer.DefineProperty('MyRect',@ReadMyRect,@WriteMyRect,MyRectMustBeSaved); end; procedure TMyComponent.ReadMyRect(Reader: TReader); begin with Reader do begin ReadListBegin; FMyRect.Left:=ReadInteger; FMyRect.Top:=ReadInteger; FMyRect.Right:=ReadInteger; FMyRect.Bottom:=ReadInteger; ReadListEnd; end; end; procedure TMyComponent.WriteMyRect(Writer: TWriter); begin with Writer do begin WriteListBegin; WriteInteger(FMyRect.Left); WriteInteger(FMyRect.Top); WriteInteger(FMyRect.Right); WriteInteger(FMyRect.Bottom); WriteListEnd; end; end;
This will save MyRect as a property 'MyRect'.
If you stream a lot of TRect, then you probably do not want to write everytime this code. The unit LResources contains an example how to write a procedure to define a rect property:
procedure DefineRectProperty(Filer: TFiler; const Name: string; ARect, DefaultRect: PRect);
This way the above code can be written this short:
procedure TMyComponent.DefineProperties(Filer: TFiler); begin inherited DefineProperties(Filer); DefineRectProperty(Filer,'MyRect',@FMyRect,nil); end;
Writing your own component - Part 2
Now the example can be extended and we can use arbitrary properties with only a few lines of code:
type TMyComponent = class(TComponent) private FID: integer; FRect1: TRect; FRect2: TRect; protected procedure DefineProperties(Filer: TFiler); override; public property Rect1: TRect read FRect1 write FRect1; property Rect2: TRect read FRect2 write FRect2; published property ID: integer read FID write FID; end; procedure TMyComponent.DefineProperties(Filer: TFiler); begin inherited DefineProperties(Filer); DefineRectProperty(Filer,'Rect1',@FRect1,nil); DefineRectProperty(Filer,'Rect2',@FRect2,nil); end;
This component can now be saved, loaded or used by the RTTI controls. You don't need to write any further code.
Writing and Reading components from/to LFM
See unit lresources function ReadComponentFromTextStream and WriteComponentAsTextToStream for examples.
Writing and Reading components from/to XML
Streaming components is simple: See the example in lazarus/examples/xmlstreaming/.
Conclusion
RTTI is a powerful mechanism, which can be used to easily stream whole classes and helps to avoid writing a lot of boring load/save code.
See also