Click here to Skip to main content
65,938 articles
CodeProject is changing. Read more.
Articles / Languages / Delphi

DFM-serialization in depth

5.00/5 (5 votes)
5 Dec 2015CPOL8 min read 25.5K   1.7K  
In the article I describe the possibilities of standard Delphi DFM-serialization mechanism. I start from the basics and then try to cover more complex situations.

Introduction

DFM-serialization mechanism which Delphi ships with may be pretty useful. Usually the basics of this mechanism are well-known by Delphi programmers. But there are real-world scenarios when you should go beyond the basics and the mechanism actually allows you to do that. In the article I will start from the basics and then try to cover more complex situations.

About DFM-format 

DFM-format actually experienced some serious improvements over the history of Delphi. Historically it was binary and not readable. But we are talking about the current state of the format. The primary target of DFM-format was to store descriptions of the forms in order to make it simple to create form descriptions in design-time and create actual forms from descriptions in run-time. The big advantage of DFM-format is its native Delphi support (a little of coding is needed to implemet serialization support for Delphi object). The disadavntage of DFM-format is its low interoperability (when we need to integrate with some external systems it is more preferrable to use standard formats such as XML or JSON).

Object properties

Let's start from the simple example:

object DfmSampleForm: TDfmSampleForm
  Left = 0
  Top = 0
  Caption = 'DfmSampleForm'
  ClientHeight = 202
  ClientWidth = 221
  Color = clBtnFace
  ...
  OldCreateOrder = False
  PixelsPerInch = 96
  TextHeight = 13
end

It is a description of the form, which was generated by VCL Forms application wizard. I just modified some properties of the form. As you can see DFM-format is pretty straightforward. The description starts with object name and class type and inside this block all object properties with their values are presented each on its own string and with block indent. Here we can see properties with integer, boolean, string and enumeration data types.

Child components

If I will add some components onto the form they will appear inside DFM-file as child objects for the form object. For example when I added TMemo and TPanel with TButton on it onto the form the DFM-file became to look like this:

object DfmSampleForm: TDfmSampleForm
  ...
  object Memo: TMemo
    Left = 8
    Top = 8
    Width = 252
    Height = 217
    Anchors = [akLeft, akTop, akRight, akBottom]
    TabOrder = 0
    ...
  end
  object BottomPanel: TPanel
    Left = 0
    Top = 226
    Width = 268
    Height = 32
    Align = alBottom
    Anchors = [akRight, akBottom]
    ...
    object Button: TButton
      Left = 185
      Top = 5
      Width = 75
      Height = 25
      Anchors = [akRight, akBottom]
      Caption = 'Button'
      TabOrder = 0
    end
  end
end

As you can see the whole object tree was saved into one single DFM-format file. 

Custom classes serialization

In spite of the fact that DFM-serialization is primarily created to store form in the textual descriptions in design-time and to restore forms from those descriptions in run-time, it is actually pretty powerful serialization mechanism, which we can use in our projects for our own custom objects. 

Custom class

I created sample application, which contains the following class declaration:

Pascal
type
  TEnum = (
    enumFirst,
    enumSecond,
    enumThird);

  TSet = set of TEnum;

  TDfmObject = class(TComponent)
  private
    FIntegerProperty: Integer;
    FDoubleProperty: Double;
    FEnumProperty: TEnum;
    FSetProperty: TSet;
    FStringProperty: String;
    FBooleanProperty: Boolean;
    FVariantProperty: Variant;
  published
    property IntegerProperty: Integer read FIntegerProperty write FIntegerProperty;
    property DoubleProperty: Double read FDoubleProperty write FDoubleProperty;
    property EnumProperty: TEnum read FEnumProperty write FEnumProperty;
    property SetProperty: TSet read FSetProperty write FSetProperty;
    property StringProperty: String read FStringProperty write FStringProperty;
    property BooleanProperty: Boolean read FBooleanProperty write FBooleanProperty;
    property VariantProperty: Variant read FVariantProperty write FVariantProperty;
  end;

...

initialization
  System.Classes.RegisterClass(TDfmObject);

...

Sample application form

In the sample application there are two buttons, one of which converts this object into DFM representation and stores it in TMemo component of the form and another button converts this object back from DFM representation in TMemo component into the object instance. 

Image 1

Streaming implemetation

The methods which access DFM streaming mechanism to perform actual object serialization and deserialization look like this:

type
  TDfmSampleForm = class(TForm)
    ...
  private
    FDfmObject: TDfmObject;
    ...
  end;

procedure TDfmSampleForm.StoreButtonClick(Sender: TObject);
var
  MemoryStream: TMemoryStream;
  StringStream: TStringStream;
begin
  StoreProperties;
  MemoryStream := TMemoryStream.Create;
  try
    MemoryStream.WriteComponent(FDfmObject);
    MemoryStream.Position := 0;
    StringStream := TStringStream.Create;
    try
      ObjectBinaryToText(MemoryStream, StringStream);
      PropertiesMemo.Text := StringStream.DataString;
    finally
      StringStream.Free;
    end;
  finally
    MemoryStream.Free;
  end;
end;

procedure TDfmSampleForm.RestoreButtonClick(Sender: TObject);
var
  MemoryStream: TMemoryStream;
  StringStream: TStringStream;
begin
  StringStream := TStringStream.Create(PropertiesMemo.Text);
  try
    StringStream.Position := 0;
    MemoryStream := TMemoryStream.Create;
    try
      ObjectTextToBinary(StringStream, MemoryStream);
      MemoryStream.Position := 0;
      FDfmObject := MemoryStream.ReadComponent(FDfmObject) as TDfmObject;
    finally
      MemoryStream.Free;
    end;
  finally
    StringStream.Free;
  end;
  RestoreProperties;
end;

So we can use DFM-streaming mechanism to store and restore our own classes. There are quite of limitations on the types of properties, which we can use in the published class section (and which are that's why stored in the DFM). It is forbidden or not supported to use properties of type pointer, record, array, interface and object (in the case when the object is not inherited from TPersistent). It is quite interesting that Variant properties streaming is supported, but it requires that Variant property should have value which in its turn can be streamed.

Unicode support

The previously mentioned sample application allows to research the behavior of serialization mechanism, when unicode characters are used. Delphi fully supports unicode now, but in the DFM-file unicode characters appear as a sequence of symbol codes. It is good news, that Delphi supports unicode in DFM-files, but unfortunately when unicode characters are used in the string values DFM-file representaions of these values becomes not readable. Actually when non-latin characters, which we want to use in the DFM-file, fall into default operating system codepage for non-unicode programs we can use these symbols in the DFM-string values and Delphi streaming system will recognize these symbols. But Delphi makes no assumptions over the default operating system codepage for non-unicode programs, so all non-latin characters in the DFM-file by default are encoded with symbol codes. 

Postponed initialization

Sometimes you need to perform some actions after deserialization from DFM is completed. For example, if you develop a visual component you likely want to suppress updating the component during the time of deserialization and likely want to update component when deserialization is completed. Besides, there may be some properties which work only together and that's why you need the component to be completely deserialized to use these properties properly.

To handle such situations gracefully TComponent class supports Loaded method, which you can override in your descendant class. And also TComponent supports ComponentState property, which contains csLoading flag when component is being loaded from stream. It allows you to check if ComponentState property contains csLoading flag to suppress some actions which you don't need until object is fully deserialized.

Custom properties

It is possible to gain full control over property serialization and deserialization. For this we need to define custom methods to store object property in DFM-file and restore object property from DFM-file. When we do it we actually can store any object type, but we need to represent it through available primitive types. Also when we do it we can read some property and not write it or write some property, which was not read. It may become useful when we have legacy object properties, which differ from actual object properties and we need to perform some conversion. Internally we will use TReader and TWriter methods, which are actually very flexible. 

An example below shows the class, which uses custom property serialization mechanism:

Pascal
type
  TDfmObject = class(TComponent)
  private
    ...
    FCustomProperty: TCustomProperty;
    procedure ReadCustomProperty(Reader: TReader);
    procedure WriteCustomProperty(Writer: TWriter);
  protected
    { TPersistent }
    procedure DefineProperties(Filer: TFiler); override;
  public
    property CustomProperty: TCustomProperty read FCustomProperty write FCustomProperty;
    ...
  end;

implementation

{ TDfmObject }

procedure TDfmObject.DefineProperties(Filer: TFiler);
begin
  inherited;
  Filer.DefineProperty('CustomProperty', ReadCustomProperty, WriteCustomProperty,
    (FCustomProperty.IntegerField <> 0) or
    (FCustomProperty.StringField <> ''));
end;

procedure TDfmObject.ReadCustomProperty(Reader: TReader);
begin
  Reader.ReadListBegin;
  FCustomProperty.IntegerField := Reader.ReadInteger;
  FCustomProperty.StringField := Reader.ReadString;
  Reader.ReadListEnd;
end;

procedure TDfmObject.WriteCustomProperty(Writer: TWriter);
begin
  Writer.WriteListBegin;
  Writer.WriteInteger(FCustomProperty.IntegerField);
  Writer.WriteString(FCustomProperty.StringField);
  Writer.WriteListEnd;
end;

The DFM-file with custom property will look like this:

Pascal
object TDfmObject
  ...
  CustomProperty = (
    256
    'string')
end

Object tree serialization

In the previous example there was only one object, which was streamed. Actually it is rare situation: in real scenarios we usually have some object tree, which we want to store in DFM-format and later to restore from DFM-format. Delphi serialization mechanism supports several approaches, which allow to store a whole object tree in a single DFM-file. We will discuss these approaches now.

Collections support

Delphi supports collections of objects, which can be serialized into DFM. The advantage of using collections is Delphi support for editing collections in design-time (you can use standard editor and property inspector to setup a collection). And another advantage is support for serialization with no coding involved. The drawback of collections is the need to inherit from predefined class and that all items in the collection must inherit from one single class type.

To define our own collection we need to create a descendant of TCollectionItem class and, may be, a descendant of TCollection class (actually for serialization support we need to inherit from TOwnedCollection class). Having done it we can make published object property with TCollection class. An example below shows minmal required code to implement a serializable collection with custom items class:

type
  TDfmCollectionItem = class(TCollectionItem)
  private
    FIntegerProperty: Integer;
    FDoubleProperty: Double;
    FStringProperty: String;
  published
    property IntegerProperty: Integer read FIntegerProperty write FIntegerProperty;
    property DoubleProperty: Double read FDoubleProperty write FDoubleProperty;
    property StringProperty: String read FStringProperty write FStringProperty;
  end;

  TDfmObject = class(TComponent)
  private
    FDfmCollection: TOwnedCollection;
    procedure SetDfmCollection(const Value: TOwnedCollection);
  public
    { TComponent }
    constructor Create(AOwner: TComponent); override;
    { TObject }
    destructor Destroy; override;
  published
    property DfmCollection: TOwnedCollection read FDfmCollection write SetDfmCollection;
  end;

implementation

{ TDfmObject }

constructor TDfmObject.Create(AOwner: TComponent);
begin
  inherited;
  FDfmCollection := TOwnedCollection.Create(AOwner, TDfmCollectionItem);
end;

destructor TDfmObject.Destroy;
begin
  FDfmCollection.Free;
  inherited;
end;

procedure TDfmObject.SetDfmCollection(const Value: TOwnedCollection);
begin
  FDfmCollection.Assign(Value);
end;

The result DFM will look like this:

object TDfmObject
  DfmCollection = <
    item
      IntegerProperty = 144
      DoubleProperty = 1.200000000000000000
      StringProperty = 'second'
    end
    item
      IntegerProperty = 256
      DoubleProperty = 3.140000000000000000
      StringProperty = 'first'
    end>
end

You can use the attached sample application to experiment with collection serialization mechanism.

Subcomponents support

Sometimes a group of components exists as a whole. It means that all linked components are created and destroyed simultaneously. And sometimes we want some component to be the part of the other component and we want easily setup both components in design-time. For this scenario Delphi supports subcomponents mechanism. This mechanism allows for component to be stored between with all its subcomponents.

The following code shows simple example of subcomponent usage:

type
  TDfmObject = class;
  TDfmSubObject = class;

  TDfmObject = class(TComponent)
  private
    FStringProperty: String;
    FIntegerProperty: Integer;
    FSubObject: TDfmSubObject;
  public
    { TComponent }
    constructor Create(AOwner: TComponent); override;
  published
    property IntegerProperty: Integer read FIntegerProperty write FIntegerProperty;
    property StringProperty: String read FStringProperty write FStringProperty;
    property SubObject: TDfmSubObject read FSubObject;
  end;

  TDfmSubObject = class(TComponent)
  private
    FStringSubProperty: String;
    FIntegerSubProperty: Integer;
  published
    property IntegerSubProperty: Integer read FIntegerSubProperty write FIntegerSubProperty;
    property StringSubProperty: String read FStringSubProperty write FStringSubProperty;
  end;

implementation

{ TDfmObject }

constructor TDfmObject.Create(AOwner: TComponent);
begin
  inherited;
  FSubObject := TDfmSubObject.Create(Self);
  FSubObject.SetSubComponent(True);
end;

The result DFM-file looks like this:

object TDfmObject
  IntegerProperty = 256
  StringProperty = 'string'
  SubObject.IntegerSubProperty = 4092
  SubObject.StringSubProperty = 'substuing'
end

You can use attached sample application to experiment with subcomponent serialization mechanism.

Child components support

Sometimes you need to store a whole object tree inside the DFM-file and the items in the tree can have different classes. In this scenario you cannot use Delphi collections and you need to create component tree. Delphi TComponent class supports child components list. When you create child component, you just need to specify its parent component as owner. This allows you to create component tree. But by default child components are not stored in the DFM-file and for such storing to happen you need to override some TComponent class methods (namely GetChildOwner and GetChildren). The desired behavior of storing child component descriptions inside the DFM-file is already implemented for TCustomForm class so in our own implementation we just need to do something similar. 

An example below shows code of TComponent descendant class, which allows to store its child components inside DFM-file:

Pascal
type
  TDfmObject = class(TComponent)
  private
    FNameProperty: String;
    FIntegerProperty: Integer;
    FStringProperty: String;
  protected
    { TPersistent }
    procedure AssignTo(Dest: TPersistent); override;
    { TComponent }
    procedure GetChildren(Proc: TGetChildProc; Root: TComponent); override;
    function GetChildOwner: TComponent; override;
  published
    property NameProperty: String read FNameProperty write FNameProperty;
    property IntegerProperty: Integer read FIntegerProperty write FIntegerProperty;
    property StringProperty: String read FStringProperty write FStringProperty;
  end;

implementation

{ TDfmObject }

procedure TDfmObject.AssignTo(Dest: TPersistent);
var
  DestDfmObject: TDfmObject;
begin
  if Dest is TDfmObject then
  begin
    DestDfmObject := Dest as TDfmObject;
    DestDfmObject.NameProperty := NameProperty;
    DestDfmObject.IntegerProperty := IntegerProperty;
    DestDfmObject.StringProperty := StringProperty;
  end;
end;

function TDfmObject.GetChildOwner: TComponent;
begin
  Result := Self;
end;

procedure TDfmObject.GetChildren(Proc: TGetChildProc; Root: TComponent);
var
  I: Integer;
  OwnedComponent: TComponent;
begin
  inherited GetChildren(Proc, Root);
  for I := 0 to ComponentCount - 1 do
    Proc(Components[I]);
end;

initialization
  System.Classes.RegisterClass(TDfmObject);

The result DFM-file looks like this:

object TDfmObject
  IntegerProperty = 0
  object TDfmObject
    NameProperty = 'Level1.1'
    IntegerProperty = 11
    object TInheritedDfmObject
      NameProperty = 'Level2.1'
      IntegerProperty = 21
    end
    object TInheritedDfmObject
      NameProperty = 'Level2.2'
      IntegerProperty = 22
    end
    object TInheritedDfmObject
      NameProperty = 'Level2.3'
      IntegerProperty = 23
    end
  end
  object TDfmObject
    NameProperty = 'Level1.2'
    IntegerProperty = 12
  end
  object TDfmObject
    NameProperty = 'Level1.3'
    IntegerProperty = 13
  end
end

You can use the attached sample application to experiment with object tree serialization mechanism.

Conclusions

In the article I tried to describe Delphi native serialization mechanism in depth (some topics are still not covered though). I hope that this will material help readers to understand that Delphi serialization mechanism is pretty useful and extendable. It is used mostly by component developers, but actually one can use it to store and restore any application object tree.

License

This article, along with any associated source code and files, is licensed under The Code Project Open License (CPOL)