如何正确地流式传输子组件的TCollection属性,例如嵌入式TDBGrid的Columns属性。

4
我一直在尝试将另一个问题的作者给我的代码转化为MCVE格式,以说明自定义组件存在的问题。
该组件仅是一个TPanel派生类,其中包含一个嵌入的TDBGrid。下面是源代码和测试项目的版本。
问题在于,如果嵌入的DBGrid使用持久性列创建,则在重新打开IDE中的测试项目时,会引发异常。 执行测试项目的Stream方法可以显示出问题的原因:
为了进行比较,我的表单上还有一个普通的TDBGrid,名为DBGrid1。而这个DBGrid1的列被流化为...
Columns = <
  item
    Expanded = False
    FieldName = 'ID'
    Visible = True
  end
[...]

嵌入式网格的列会像这样流式传输。
Grid.Columns = <
  item
    Grid.Expanded = False
    Grid.FieldName = 'ID'
    Grid.Visible = True
  end
[...]

显然是Grid.Expanded和其他列属性的Grid前缀导致了问题。
我想问题与DBGridColumns是TCollection的后代以及嵌入式网格不是DFM中的顶层对象有关。
我的问题是:应该如何修改TMyPanel的代码,以便正确地流式传输网格的列?
组件源码:
unit MAGridu;

interface

uses
  Windows, SysUtils, Classes, Controls, ExtCtrls, DBGrids;

type
  TMyPanel = class(TPanel)
  private
    FGrid : TDBGrid;
  public
    constructor Create(AOwner : TComponent); override;
  published
    property Grid : TDBGrid read FGrid;
  end;

procedure Register;

implementation

procedure Register;
begin
  RegisterComponents('Standard', [TMyPanel]);
end;

constructor TMyPanel.Create(AOwner: TComponent);
begin
  inherited Create(AOwner);
  FGrid := TDBGrid.Create(Self);
  FGrid.SetSubcomponent(True);
  FGrid.Parent := Self;
end;

end.

测试项目源代码:

type
  TForm1 = class(TForm)
    DBGrid1: TDBGrid;
    CDS1: TClientDataSet;
    DataSource1: TDataSource;
    MyPanel1: TMyPanel;
    Memo1: TMemo;
    Button1: TButton;
    procedure Button1Click(Sender: TObject);
    procedure FormCreate(Sender: TObject);
  private
    procedure Stream;
  public
  end;

var
  Form1: TForm1;

implementation

{$R *.dfm}

procedure TForm1.Button1Click(Sender: TObject);
begin
  Stream;
end;

procedure TForm1.Stream;
//  This method is included as an easy way of getting at the contents of the project's
//  DFM.  It saves the form to a stream, and loads it into a memo on the form.
var
  SS : TStringStream;
  MS : TMemoryStream;
  Writer : TWriter;
begin
  SS := TStringStream.Create('');
  MS := TMemoryStream.Create;
  Writer := TWriter.Create(MS, 4096);

  try
    Writer.Root := Self;
    Writer.WriteSignature;
    Writer.WriteComponent(Self);
    Writer.FlushBuffer;
    MS.Position := 0;
    ObjectBinaryToText(MS, SS);
    Memo1.Lines.Text := SS.DataString;
  finally
    Writer.Free;
    MS.Free;
    SS.Free;
  end;
end;
end.

procedure TForm1.FormCreate(Sender: TObject);
var
  Field : TField;
begin
  Field := TIntegerField.Create(Self);
  Field.FieldName := 'ID';
  Field.FieldKind := fkData;
  Field.DataSet := CDS1;

  Field := TStringField.Create(Self);
  Field.FieldName := 'Name';
  Field.Size := 20;
  Field.FieldKind := fkData;
  Field.DataSet := CDS1;

  CDS1.CreateDataSet;
  CDS1.InsertRecord([1, 'One']);

end;

end.

1
只是好奇:为什么您将嵌入式网格的父级设置为基础MyPanel的父级?在TMyPanel.Create期间,网格已成为MyPanel的父级。它不应该保持这种状态吗? - Uwe Raabe
@MartynA:你的意图是让TDBGrid始终成为Panel的子控件吗?如果是这样,那么Panel的构造函数应该有一个FGrid.Parent := Self;语句,并且应该完全删除SetParent()重载。 - Remy Lebeau
感谢@RemyLebeau。我已经按照您的建议在我的代码和我的q中更新了TMyPanel构造函数。不幸的是,流传问题仍然存在TColumns。 - MartynA
@SertacAkyuz:抱歉耽搁了。我试着按照你的答案做了一部分,但是被一场车祸分心了。我在D7中尝试了你答案的前半部分,直到TestPanel2(还没有尝试Seattle)。我可以在IDE中设置持久字段,但是DFM中没有关于网格的任何信息被保存。因此,网格在运行时保持为空。当然,如果我将网格的数据源分配给我的表单上的一个数据源,它会创建动态列。我明天会试试你的TestPanel2。 - MartynA
@Martyn - 很抱歉发生了意外。请慢慢来,只要知道你会测试它,我就没问题了。 - Sertac Akyuz
显示剩余4条评论
2个回答

2
似乎你不能对此进行太多处理。当你查看TWriter.WriteProperties本地的WriteCollectionProp过程时,你会发现在调用WriteCollection之前,FPropPath被清空了。
关于TDBGrid或更好的TCustomDBGrid的问题在于该集合被标记为stored false,而流式传输被委托给DefineProperties,它使用TCustomDBGrid.WriteColumns来完成工作。
检查该方法会发现,虽然它也调用WriteCollection,但在此之前没有清除FPropPath的内容。这在某种程度上是预期的,因为FPropPath是一个私有字段。
即使Delphi 10.1 Berlin的操作方式与Delphi 7相同,我建议您一起提交QP报告并附带此示例。

非常感谢。我一直在使用D7,只是因为我不确定原始问题的OP正在使用什么。我会检查它在西雅图是否表现类似(柏林尚未安装),然后按照您的建议提交QP报告。 - MartynA

1
解决方案涉及嵌入式网格不具有拥有面板作为流根的形式,而是面板本身。这将防止网格属性被“Grid”限定,从而消除列属性错误限定的影响。也就是说,以下是对错误行为的解决方法。
要实现上述内容,请删除SetSubComponent调用。
constructor TMyPanel.Create(AOwner: TComponent);
begin
  inherited Create(AOwner);
  FGrid := TDBGrid.Create(Self);
//  FGrid.SetSubcomponent(True);
  FGrid.Parent := Self;
end;
csSubComponent 样式已被移除,现在栅格不再被流式传输。
然后为面板覆盖 GetChildren 以通过面板流式传输栅格。如 文档 所述,GetChildren 用于确定控件的哪些子控件将被保存(流式传输)。由于我们只有一个控件(栅格),因此我们不需要进行区分,而是可以调用继承处理程序修改根。
type
  TMyPanel = class(TPanel)
  private
    FGrid : TDBGrid;
  public
    constructor Create(AOwner : TComponent); override;
    procedure GetChildren(Proc: TGetChildProc; Root: TComponent); override;
  published
    property Grid : TDBGrid read FGrid;
  end;

...

procedure TMyPanel.GetChildren(Proc: TGetChildProc; Root: TComponent);
begin
  inherited GetChildren(Proc, Self);
end;

然后需要解决子组件的复杂性。这里的复杂性是指在面板前创建了第二个网格,假设它具有流属性,非常类似于this未解答的问题。请注意,这个问题与上面提供的解决方案无关。原始代码显示相同的问题。
阅读了上述提到的问题,以及thisthisthis,仍然无法通过其中的代码、提示和建议解决问题,我跟踪了流系统,并提出了以下解决方案。

我不是在声称这就是应该的方式,只是这是我能够让它工作的方式。主要修改包括:子网格现在可写(这需要在生产代码中设置器),网格的条件创建以及面板的覆盖GetChildOwner方法。下面是整个单元,其中包含TMyPanel2TMyPanel无法实现...)。

unit TestPanel2;

interface

uses
  Windows, SysUtils, Classes, Controls, ExtCtrls, DBGrids;

type
  TMyPanel2 = class(TPanel)
  private
    FGrid : TDBGrid;
  protected
    function GetChildOwner: TComponent; override;
  public
    constructor Create(AOwner : TComponent); override;
    destructor Destroy; override;
    procedure GetChildren(Proc: TGetChildProc; Root: TComponent); override;
  published
    property Grid : TDBGrid read FGrid write FGrid;
  end;

procedure Register;

implementation

procedure Register;
begin
  RegisterComponents('Test', [TMyPanel2]);
end;

constructor TMyPanel2.Create(AOwner: TComponent);
begin
  inherited Create(AOwner);
  if not (csReading in AOwner.ComponentState) then begin
    FGrid := TDBGrid.Create(Self);
    FGrid.Name := 'InternalDBGrid';
    FGrid.Parent := Self;
  end else
    RegisterClass(TDBGrid);
end;

destructor TMyPanel2.Destroy;
begin
  FGrid.Free;
  inherited;
end;

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

procedure TMyPanel2.GetChildren(Proc: TGetChildProc; Root: TComponent);
begin
  Proc(Grid);
end;

end.

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接