Difference between revisions of "Streaming components"

From Free Pascal wiki
Jump to navigationJump to search
Line 13: Line 13:
 
== TComponent / TPersistent ==
 
== 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.
+
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 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.
+
'''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 / TWriter ==

Revision as of 14:45, 8 April 2013

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

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

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

Names

  • All components of one component (Owner) must have distinct names. So two forms owned by applications must have distinct names. And two labels on a form must have distinct names. But two labels on two different forms can have the same name. And a form can have the same name as one of its childs.
  • TComponent.Name can be empty and you can have more than one component without a name. TWriter will write it, but TReader will not find the component and reading will fail. Therefore the IDE object inspector does not allow that.
  • When referencing other forms: All root components (forms, datamodules, ...) referenced by other (forms,etc) must have unique names. They don't need to be owned by application, but then the programmer himself must make sure the names are unique. Forms and Datamodules are found via the Screen object, where all forms and datamodules register themselves automatically.
  • You can create many forms with the name Form1, for example via TForm1.Create(nil). If a Form2 references a Form1.OpenDialog, then the first Form1 in Screen is used.
  • A Form1 and an embedded frame can have both a child Label1. When the Label1 is referenced then it should be unique on the whole form including all embedded frames. So it is recommended to give all components unique names.
  • Global fixup: TReader reads a component stream. If it finds an embedded frame a second TReader is created, which reads the frame stream. Then it returns and continues. References to other components (e.g. Form1.Button1) are saved to a global fixup list in the unit classes (see GetFixupReferenceNames). The references are fixed up after reading.
  • TReader/TWriter use the special name Owner to refer to the current Owner.

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