65.9K
CodeProject 正在变化。 阅读更多。
Home

DFM-序列化深度解析

starIconstarIconstarIconstarIconstarIcon

5.00/5 (5投票s)

2015 年 12 月 5 日

CPOL

8分钟阅读

viewsIcon

26124

downloadIcon

1817

在本文中,我将描述标准 Delphi DFM-序列化机制的可能性。我将从基础开始,然后尝试涵盖更复杂的情况。

引言

Delphi 自带的 DFM-序列化机制可能非常有用。通常,Delphi 程序员对该机制的基础知识都很熟悉。但现实场景中,您有时需要超越基础知识,而该机制实际上允许您这样做。在本文中,我将从基础开始,然后尝试涵盖更复杂的情况。

关于 DFM 格式

DFM 格式在 Delphi 的发展历史中其实经历了一些重大的改进。过去,它是二进制格式,不可读。但我们现在讨论的是该格式的当前状态。DFM 格式的主要目标是存储窗体描述,以便在设计时轻松创建窗体描述,并在运行时从描述创建实际窗体。DFM 格式的一大优点是它具有原生的 Delphi 支持(实现 Delphi 对象的序列化支持只需少量编码)。DFM 格式的缺点是其互操作性较低(当我们与某些外部系统集成时,更倾向于使用 XML 或 JSON 等标准格式)。

对象属性

让我们从一个简单的例子开始。

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

这是 VCL 窗体应用程序向导生成的窗体描述。我只修改了窗体的一些属性。如您所见,DFM 格式相当直接。描述以对象名称和类类型开头,在该块内部,所有对象属性及其值都单独显示在一行上,并带有块缩进。在这里,我们可以看到具有整数、布尔、字符串和枚举数据类型的属性。

子组件

如果我在窗体上添加一些组件,它们将作为子对象出现在 DFM 文件中,属于窗体对象。例如,当我将 TMemo 和包含 TButton TPanel 添加到窗体上时,DFM 文件看起来就像这样:

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

如您所见,整个对象树被保存在一个 DFM 格式文件中。

自定义类序列化

尽管 DFM-序列化主要用于在设计时以文本描述的形式存储窗体,并在运行时从这些描述恢复窗体,但它实际上是一个非常强大的序列化机制,我们可以在项目中使用它来处理我们自己的自定义对象。

自定义类

我创建了一个示例应用程序,其中包含以下类声明:

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

...

示例应用程序窗体

在示例应用程序中,有两个按钮,一个按钮将此对象转换为 DFM 表示形式并将其存储在窗体的 TMemo 组件中,另一个按钮将 TMemo 组件中的对象从 DFM 表示形式转换回对象实例。

流实现

访问 DFM 流机制以执行实际对象序列化和反序列化的方法如下所示:

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;

因此,我们可以使用 DFM-流机制来存储和恢复我们自己的类。对于可以在 published 类节中使用(因此可以存储在 DFM 中)的属性类型,存在相当多的限制。不允许或不支持使用指针、记录、数组、接口和对象(当对象未从 TPersistent 继承时)类型的属性。有趣的是,Variant 属性的流式传输是支持的,但这要求 Variant 属性的值本身也必须是可流式传输的。

Unicode 支持

前面提到的示例应用程序允许研究使用 Unicode 字符时的序列化机制的行为。Delphi 现在完全支持 Unicode,但在 DFM 文件中,Unicode 字符显示为符号代码序列。好消息是 Delphi 完全支持 DFM 文件中的 Unicode,但遗憾的是,当字符串值中使用 Unicode 字符时,DFM 文件中这些值的表示将变得不可读。实际上,当我们在 DFM 文件中使用的非拉丁字符落入非 Unicode 程序默认操作系统代码页时,我们可以在 DFM 字符串值中使用这些符号,并且 Delphi 流系统将识别这些符号。但是,Delphi 不会假定非 Unicode 程序使用默认操作系统代码页,因此默认情况下,DFM 文件中的所有非拉丁字符都将以符号代码进行编码。

延迟初始化

有时,您需要在 DFM 反序列化完成后执行一些操作。例如,如果您开发了一个可视组件,您可能希望在反序列化期间抑制组件的更新,并在反序列化完成后更新组件。此外,可能存在一些属性只能协同工作,因此您需要等到对象完全反序列化后才能正确使用这些属性。

为了优雅地处理这种情况,TComponent 类支持 Loaded 方法,您可以在其派生类中重写该方法。此外,TComponent 还支持 ComponentState 属性,当组件正在从流中加载时,该属性包含 csLoading 标志。这允许您检查 ComponentState 属性是否包含 csLoading 标志,以抑制一些在对象完全反序列化之前不需要执行的操作。

自定义属性

可以完全控制属性的序列化和反序列化。为此,我们需要定义自定义方法来将对象属性存储在 DFM 文件中,并从 DFM 文件中恢复对象属性。当我们这样做时,我们实际上可以存储任何对象类型,但需要通过可用的基本类型来表示它。此外,这样做时,我们可以读取某个属性而不写入它,或者写入某个未读取的属性。当处理与实际对象属性不同的旧版对象属性,并且需要进行某些转换时,这可能很有用。在内部,我们将使用 TReader TWriter 方法,它们实际上非常灵活。

下面的示例展示了一个使用自定义属性序列化机制的类:

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;

包含自定义属性的 DFM 文件将如下所示:

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

对象树序列化

在前面的示例中,只有一个对象被流式传输。实际上,这是一种罕见的情况:在实际场景中,我们通常有一个对象树,我们希望将其存储在 DFM 格式中,并在之后从 DFM 格式中恢复。Delphi 序列化机制支持几种方法,允许将整个对象树存储在单个 DFM 文件中。我们现在将讨论这些方法。

集合支持

Delphi 支持可以序列化到 DFM 的对象集合。使用集合的优点是 Delphi 支持在设计时编辑集合(您可以使用标准的编辑器和属性检查器来设置集合)。另一个优点是支持无需编码的序列化。集合的缺点是需要从预定义类继承,并且集合中的所有项都必须继承自同一个类类型。

要定义我们自己的集合,我们需要创建一个 TCollectionItem 类的派生类,可能还有一个 TCollection 类的派生类(实际上,为了支持序列化,我们需要从 TOwnedCollection 类继承)。完成这些后,我们可以创建一个带有 TCollection 类的发布对象属性。下面的示例展示了实现带有自定义项类的可序列化集合所需的最小代码:

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;

结果 DFM 将如下所示:

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

您可以使用附带的示例应用程序来试验集合序列化机制。

子组件支持

有时一组组件作为一个整体存在。这意味着所有链接的组件同时创建和销毁。有时我们希望某个组件成为另一个组件的一部分,并且我们希望在设计时轻松设置这两个组件。为此,Delphi 支持子组件机制。此机制允许将组件及其所有子组件一起存储。

以下代码显示了子组件使用的一个简单示例:

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;

结果 DFM 文件如下所示:

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

您可以使用附带的示例应用程序来试验子组件序列化机制。

子组件支持

有时您需要将整个对象树存储在 DFM 文件中,并且树中的项可以具有不同的类。在这种情况下,您不能使用 Delphi 集合,需要创建组件树。Delphi 的 TComponent 类支持子组件列表。创建子组件时,只需指定其父组件作为所有者。这允许您创建组件树。但默认情况下,子组件不会存储在 DFM 文件中,为了让存储发生,您需要重写某些 TComponent 类方法(即 GetChildOwner GetChildren )。对于 TCustomForm 类,存储子组件描述到 DFM 文件中的期望行为已经实现,因此在我们自己的实现中,我们只需要做类似的事情。

下面的示例展示了允许将其子组件存储在 DFM 文件中的 TComponent 派生类的代码:

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

结果 DFM 文件如下所示:

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

您可以使用附带的示例应用程序来试验对象树序列化机制。

结论

在本文中,我试图深入介绍 Delphi 的原生序列化机制(尽管有些主题尚未涵盖)。我希望这些材料能帮助读者理解 Delphi 的序列化机制非常有用且可扩展。它主要由组件开发者使用,但实际上任何人都可以使用它来存储和恢复任何应用程序对象树。

© . All rights reserved.