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:
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.
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:
type
TDfmObject = class(TComponent)
private
...
FCustomProperty: TCustomProperty;
procedure ReadCustomProperty(Reader: TReader);
procedure WriteCustomProperty(Writer: TWriter);
protected
procedure DefineProperties(Filer: TFiler); override;
public
property CustomProperty: TCustomProperty read FCustomProperty write FCustomProperty;
...
end;
implementation
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:
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:
type
TDfmObject = class(TComponent)
private
FNameProperty: String;
FIntegerProperty: Integer;
FStringProperty: String;
protected
procedure AssignTo(Dest: TPersistent); override;
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
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.