【问题标题】:How to deserialize fields from subobjects using readComponent()如何使用 readComponent() 反序列化子对象中的字段
【发布时间】:2016-07-18 21:32:26
【问题描述】:

我正在使用 writeComponent() 来序列化一个对象和一个子对象。这似乎没有任何问题。在下面的例子中,我序列化了一个 TConfigData 对象和一个 TFoo 子对象:

object TConfigData
  myInteger = 999
  object TFoo
    value = 777
  end
end

但是,当我尝试重新读取它时,readCompontent 仅恢复根对象中的 myInteger 值,它无法恢复它设置为零的子对象 TFoo 中的值。我将整个代码附在下面。两个主要类是 TConfigData 和该 TFoo 内。我已经在互联网上进行了广泛的搜索,但我不明白为什么它无法读取 TFoo.value。

关于如何让阅读发挥作用的任何建议? (使用 XE6)。我相信有一个简单的解释,但它暂时避开了我。

unit ufMain;

interface

uses Winapi.Windows, Winapi.Messages, System.SysUtils, System.Variants,    
System.Classes, Vcl.Graphics, Vcl.Controls, Vcl.Forms, Vcl.Dialogs, V     
Vcl.StdCtrls;

type
  TFoo = class (TComponent)
   private
     fValue : integer;
   published
     property value : integer read fValue write fValue;
  end;

  TConfigData = class (TComponent)
     private
       fInteger : integer;
       fFoo : TFoo;  // <- Subobject
       function ComponentToStringProc(Component: TComponent): string;
       class function StringToComponentProc(Value: string): TComponent;
     protected
       procedure GetChildren(Proc: TGetChildProc; Root: TComponent); override;
      function   GetChildOwner: TComponent; override;
      published
       property myInteger : Integer read fInteger write fInteger;
       property foo : TFoo read fFoo write fFoo;
     public
       procedure save (fileName : string);
       class function  load (fileName : string) : TConfigData;
       function  getConfigStreamString : string;
       constructor Create (AOwner : TComponent); override;
       destructor Destroy; override;
  end;

  TForm1 = class(TForm)
    Button1: TButton;
    Memo1: TMemo;
    Button2: TButton;
    procedure Button1Click(Sender: TObject);
    procedure Button2Click(Sender: TObject);
  private
    { Private declarations }
  public
    { Public declarations }
  end;

var
  Form1: TForm1;

implementation

{$R *.dfm}

Uses IOUTils;

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

function TConfigData.GetChildOwner: TComponent;
begin
  result := Self;
end;

constructor TConfigData.Create (AOwner : TComponent);
begin
  inherited;
  fFoo := TFoo.Create (self);
  // foo.SetSubComponent (True); <- I don't want to use this because it flattens the dfm file.
end;

destructor TConfigData.Destroy;
begin
  fFoo.Free;
  inherited;
end;

function TConfigData.getConfigStreamString : string;
begin
   result := ComponentToStringProc (self);
end;

procedure TConfigData.save (fileName : string);
var configStr : string;
begin
  configStr := ComponentToStringProc (self);
  TFile.WriteAllText (fileName, configStr);
end;

class function TConfigData.load (fileName : string) : TConfigData;
var configStr : string;
begin
  configStr := TFile.ReadAllText (fileName);
  result := StringToComponentProc (configStr) as TConfigData;
end;

function TConfigData.ComponentToStringProc(Component: TComponent): string;
var
  BinStream:TMemoryStream;
  StrStream: TStringStream;
  s: string;
begin
  BinStream := TMemoryStream.Create;
  try
    StrStream := TStringStream.Create(s);
    try
      BinStream.WriteComponent(Component);
      BinStream.Seek(0, soFromBeginning);
      ObjectBinaryToText(BinStream, StrStream);
      StrStream.Seek(0, soFromBeginning);
      Result:= StrStream.DataString;
    finally
      StrStream.Free;
    end;
  finally
    BinStream.Free
  end;
end;

class function TConfigData.StringToComponentProc(Value: string): TComponent;
var
  StrStream:TStringStream;
  BinStream: TMemoryStream;
begin
  StrStream := TStringStream.Create(Value);
  try
    BinStream := TMemoryStream.Create;
    try
      ObjectTextToBinary(StrStream, BinStream);
      BinStream.Seek(0, soFromBeginning);
      Result:= BinStream.ReadComponent(nil);
    finally
      BinStream.Free;
    end;
  finally
    StrStream.Free;
  end;
end;

procedure TForm1.Button1Click(Sender: TObject);
var config : TConfigData;
    configStr : string;
begin
  config := TConfigData.Create (nil);
  config.myInteger := 999;
  config.foo.value := 777;
  config.save('c:\\tmp\\config.dat');
  Memo1.text := config.getConfigStreamString;
end;

procedure TForm1.Button2Click(Sender: TObject);
var config : TConfigData;
begin
  config := TConfigData.load ('c:\\tmp\\config.dat');
  Memo1.Clear;
  Memo1.Lines.Add(inttostr (config.myInteger));
  Memo1.Lines.Add(inttostr(config.foo.value));
end;

initialization
 RegisterClasses([TConfigData, TFoo]);
end.

【问题讨论】:

    标签: delphi


    【解决方案1】:

    处理子对象有两种不同的方式。

    其中一个是SetSubComponent(true),或者首先使用TPersistent 而不是TComponent。在这种情况下,TConfigData 负责在其构造函数中创建子对象并在析构函数中销毁它。流系统“预期”该组件已经存在,它只需要修改其字段。

    但是子组件的处理方式不同。它们由流系统本身创建,属于GetChildOwner(在您的情况下为TConfigData)返回的组件。如果子组件没有名称(或者说,名称为空),则不再执行任何操作。但如果它有名称,流系统会查找同名的已发布字段,以将它们分配给新创建的组件。这就是它在 VCL 中的工作方式:TForm1(例如)将所有控件作为已发布字段,这些字段指向自动从 .dfm 加载的控件。

    在您的情况下,组件 TFoo 已成功加载并列在 Components[] 中,但它与 foo 属性无关,这会导致在 TConfigData 构造函数中创建的空组件。

    如何解决

    子组件与其父组件的连接比子组件弱,预计父组件不知道它可以拥有多少个子组件,这适用于大多数控件。这就是为什么不期望在构造函数中创建子代的原因。如果从一开始就知道需要什么类型的子组件,那么使用子组件似乎更合乎逻辑。

    不过,它也可以用子组件来完成。

    使用已发布的字段 Foo 代替属性 Foo:

    TConfigData = class (TComponent)
         private
           fInteger : integer;
           function ComponentToStringProc(Component: TComponent): string;
           class function StringToComponentProc(Value: string): TComponent;
         protected
           procedure GetChildren(Proc: TGetChildProc; Root: TComponent); override;
          function   GetChildOwner: TComponent; override;
          published
           Foo: TFoo;
           property myInteger : Integer read fInteger write fInteger;
         public
           procedure save (fileName : string);
           class function  load (fileName : string) : TConfigData;
           function  getConfigStreamString : string;
           constructor Create (AOwner : TComponent); override;
           destructor Destroy; override;
      end;
    

    在构造函数中,我们创建新的 TFoo 并为其命名:

    constructor TConfigData.Create (AOwner : TComponent);
    begin
      inherited;
      Foo := TFoo.Create (self);
      Foo.name = 'Foo'; //looks like tautology but it's not!
    end;
    

    但是当从文件加载时,我们会破坏所有现有的组件,因为流系统从头开始创建它们,否则会发生名称冲突。类似的东西:

    class function TConfigData.StringToComponentProc(Value: string): TComponent;
    var
      StrStream:TStringStream;
      BinStream: TMemoryStream;
    begin
      StrStream := TStringStream.Create(Value);
      try
        BinStream := TMemoryStream.Create;
        try
          ObjectTextToBinary(StrStream, BinStream);
          BinStream.Seek(0, soFromBeginning);
          Result:= TConfigFile.Create(nil); //it creates components we don't need
          Result.DestroyComponents; //not any more
          BinStream.ReadComponent(Result); //it reads to component already created
        finally
          BinStream.Free;
        end;
      finally
        StrStream.Free;
      end;
    end;
    

    现在应该可以了。

    也许完全删除构造函数Create(aOwner: TComponent)(继承就足够了)并使用另一个构造函数更优雅,例如仅从您的代码中调用的CreateNew,以防找不到此配置文件。或者,代替构造函数使用过程InitializeDefault 或类似这样的smth,它将所有字段设置为默认值并在需要时创建TFoo

    甚至可以在运行时从不显式创建TFoo,而是从文件或存储了默认值的资源中加载TConfigData

    【讨论】:

    • 感谢您的回复。你给的东西确实有效。在我的特定应用程序中,我不会有子子对象,但如果一个有多个对象层,我想知道它会如何工作。
    • @rhody 它会工作得很好。为了实现它,我将引入抽象类 TStreamingClass 以及在 TConfigData 中实现的 GetChildren 和 GetChildOwner 方法,因此 TConfigInfo 和 TFoo 以及其他组件可能都是 TStreamingClass 的后代。然后您可以根据需要使层次结构变得复杂,就像在 dfm 描述表单中所做的那样,尽管差别不大。在可视化控件表单中是所有组件的所有者,而在 TConfigData 中,所有者与父级相同。
    • @rhody 这就是为什么在表单中,如果您有 Panel1 位于 Button1 上,您编写 form1.button1 或简单的 button1,根本不提及 Panel1。在您的代码中,如果 ConfigData 具有具有子对象 Bar 的子对象 Foo,则您必须编写 ConfigData.Foo.Bar。我认为,这很好,不需要 2 个不同的层次结构作为父/子和所有者/它的组件,这会过于复杂。
    【解决方案2】:

    我不知道如何阅读工作,我只能解释代码有什么问题。

    将 Button2 处理程序替换为

    procedure TForm1.Button2Click(Sender: TObject);
    var config : TConfigData;
      I: Integer;
    
    begin
      config := TConfigData.load ('c:\temp\config.dat');
      Memo1.Clear;
      Memo1.Lines.Add(inttostr (config.myInteger));
      Memo1.Lines.Add(inttostr(config.ComponentCount));
      for I:= 0 to config.ComponentCount - 1 do
        Memo1.Lines.Add(inttostr((config.Components[I] as TFoo).value));
    end;
    

    现在很清楚幕后发生了什么。您从流中加载的 TConfigData 实例包含 2 个 TFoo 实例 - 第一个在 TConfigData 构造函数中创建,第二个由 Delphi 流系统创建,第二个由“777”值加载.

    【讨论】:

      猜你喜欢
      • 1970-01-01
      • 1970-01-01
      • 2019-01-30
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      相关资源
      最近更新 更多