Sign in Join
Wiki copytemporary

Streaming components

Deutsch · English · Français · Polski · Português · 日本語

Introduction

Normally, when you want to store data on disk or to a network stream, you have to write code which loads and saves each property. This tutorial describes how to write classes that can be loaded from and saved to streams using RTTI (RunTime Type Information) without the need to write extra load/save code.

The Lazarus sources include an example which demonstrates how to save a TGroupBox with a TCheckBox child to a stream, and how to read the stream back to recreate a copy of both components. See <lazaruspath>/examples/componentstreaming/.

By combining use of appropriate RTTI controls you can minimise the amount of code needed to connect your program's GUI with corresponding Data from Disk/Network storage.

TComponent / TPersistent

The class TPersistent is defined in the unit Classes and it uses the {$M+} compiler switch. This switch tells the compiler to create Run Time Type Information (RTTI). This means classes in this unit and all their descendants get a new published class section. 'Published' properties are as visible as 'public' properties, but additionally their structure is accessible at run time. That means all published properties can be read and written at run time. For example, the IDE uses RTTI to work with components it otherwise does not know of.

TComponent extends TPersistent with the ability to have child components. This is important for streaming, where one component is the root component (also called the lookup root) holding a list of child components.

TReader / TWriter

TReader and TWriter are worker classes, which read or write any TComponent to or from a stream (See CreateLRSReader and CreateLRSWriter). They use a Driver to read/write using a special data format. At the moment there is 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 the custom binary data format to text and back (LRSObjectBinaryToText, LRSObjectTextToBinary). The LCL prefers UTF8 for strings, while Delphi prefers Widestrings. So there are some conversion functions provided as well so you can deal easily with streaming component data not only in Lazarus but using the Delphi binary format too.

Streaming Collections

See it here TCollection#Streaming

This is a full example how to create a list of items using the classes TCollectionItem, TCollection and stream it using TComponent.

Writing your own component - Part 1

A custom component can be as simple as:

pascal
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:

pascal
procedure WriteComponentAsBinaryToStream(AStream: TStream; AComponent: TComponent);

It writes a component in binary format to the stream. For example:

pascal
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:

pascal
procedure ReadComponentFromBinaryStream(AStream: TStream;
    var RootComponent: TComponent; OnFindComponentClass: TFindComponentClassEvent; TheOwner: TComponent = nil);
pascal
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;

Streamable properties

TReader and TWriter have several limitations on what types they can stream:

Streaming custom Data - DefineProperties

You can stream additional arbitrary data by overriding DefineProperties. This allows you to stream data that is not a basic Pascal type, and classes that are not TPersistent descendants. For example to stream a record variable FMyRect: TRect which is a field in your component, add the following three methods to your component:

pascal
procedure DefineProperties(Filer: TFiler); override;
procedure ReadMyRect(Reader: TReader);
procedure WriteMyRect(Writer: TWriter);

With the following code:

pascal
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 fields, then you probably don't want to repeat this code every time. The unit LResources contains an example of how you can write a procedure that defines a rect property:

pascal
procedure DefineRectProperty(Filer: TFiler; const Name: string; ARect, DefaultRect: PRect);

Having written a procedure to define a rect property, the above code can be reduced to:

pascal
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:

pascal
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/.

Names

Conclusion

RTTI is a powerful mechanism for streaming entire classes easily. RTTI also helps you to avoid repeatedly writing a lot of boring load/save code.

See also


From the Free Pascal wiki, page "Streaming components", as it stood on 2020-02-27 after a change by Trev, out of the wiki's dump of 2024-05-26. Copyright stays with the author who wrote it, according to the old wiki's Licensing page. If you have concerns about this material, message the admin.

Revision 1, last changed by WikiArchive .

Talk about this page

Nobody has opened a bench for this page yet. Sign in to open one.