I've been trying to boil down to an MCVE some code the author of another q sent me to illustrate a problem with a custom component.
The component is simply a TPanel descendant which includes an embedded TDBGrid. My version of its source, and a test project are below.
The problem is that if the embedded DBGrid has been created with persistent columns, when its test project is re-opened in the IDE, an exception is raised
Error reading
TColumn.Grid.Expanded
. PropertyGrid
does not exist.
Executing the Stream
method of the test project shows how this problem arises:
For comparison purposes, I also have a normal TDBGrid, DBGrid1, on my form. Whereas the Columns of this DBGrid1 are streamed as
Columns = <
item
Expanded = False
FieldName = 'ID'
Visible = True
end
[...]
the embedded grid's columns are streamed like this
Grid.Columns = <
item
Grid.Expanded = False
Grid.FieldName = 'ID'
Grid.Visible = True
end
[...]
It's obviously the Grid
prefix of Grid.Expanded
and the other column properties which is causing the problem.
I imagine that the problem is something to do with the fact that DBGridColumns is a TCollection descendant and that the embedded grid isn't the top-level object in the DFM.
My question is: How should the code of TMyPanel
be modified so that the grid's
columns get correctly streamed?
Component source:
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.
Test project source:
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.
Seems there is not much you can do about it. When you look into procedure WriteCollectionProp
(local to TWriter.WriteProperties
) you see that FPropPath
is cleared before the call to WriteCollection
.
The problem with TDBGrid
, or better TCustomDBGrid
, is that the collection is marked as stored false
and the streaming is delegated to DefineProperties
, which uses TCustomDBGrid.WriteColumns
to do the work.
Inspecting that method reveals that, although it also calls WriteCollection
, the content of FPropPath
is not cleared before. This is somewhat expected as FPropPath
is a private field.
The reason why it nonetheless works in the standard use case is that at the moment of writing FPropPath
is just empty.
As even Delphi 10.1 Berlin behaves the same as Delphi 7, I suggest filing a QP report together with just this example.
The solution would involve the embedded grid not having the form that owns the panel as the streaming root, but the panel itself. This will prevent the grid's properties being qualified by 'Grid', which, in effect, will eliminate column properties being wrongly qualified by the same. That is to say, the below is a workaround for faulty behavior.
To achieve the above, remove the SetSubComponent
call,
constructor TMyPanel.Create(AOwner: TComponent);
begin
inherited Create(AOwner);
FGrid := TDBGrid.Create(Self);
// FGrid.SetSubcomponent(True);
FGrid.Parent := Self;
end;
The csSubComponent
style being removed, now the grid is not streamed at all.
Then override GetChildren
for the panel to stream the grid through the panel. GetChildren
, as documented, is used to determine which child controls are saved (streamed) of a control. Since we have only one control (the grid) we don't need to make a distinction and instead can call the inherited handler modifying the root.
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;
Then remains resolving subcomponent complications. Complication here was a second grid being created sitting in front of the panel which assumes streamed properties. Very much like in this unanswered question. Note that this problem is not related to the solution provided above. The original code displays the same problem.
Having read the question mentioned above, and this one, and this one, and this one, and still not being able to resolve with the help of the code, clues, advices in them, I traced the streaming system and came up with my solution as below.
I'm not claiming it is how it is supposed to be. It is just how I could make this to work. Main modifications are, the sub-grid is now writable (which would require a setter in production code), the conditional creation of the grid, and the overriden GetChildOwner
of the panel. Below is the entire unit having TMyPanel2
(TMyPanel
couldn't make it... ).
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.
来源:https://stackoverflow.com/questions/38293072/how-to-correctly-stream-a-tcollection-property-of-a-subcomponent-e-g-the-colum