Search code examples
delphifiremonkeydelphi-10.4-sydney

Delphi FMX: Saving and loading container children


Starting from this layout at design time. (It contains several TLayout, TGridPanelLayout, TText elements as example)

enter image description here

At runtime, I am saving the complete objects structure to a file using ObjectBinaryToText

enter image description here

But when loading the file back from the file using ObjectTextToBinary, I get this result

enter image description here

Why the sub-controls are not taking the exqct same layout as saved before? The file structure seems to be OK and containing all sub-controls as described when saving my form with the IDE

Here is a piece of code demonstrating the problem.

PAS File

unit Unit1;

interface

uses
  System.SysUtils, System.Types, System.UITypes, System.Classes,
  system.Variants, FMX.Types, FMX.Controls, FMX.Forms, FMX.Graphics, 
  FMX.Dialogs, FMX.Objects, FMX.Layouts, FMX.Controls.Presentation, 
  FMX.StdCtrls;

type
  TForm1 = class(TForm)
    RecTop: TRectangle;
    ButtonSave: TButton;
    ButtonClear: TButton;
    ButtonLoad: TButton;
    Layout1: TLayout;
    GridPanelLayout1: TGridPanelLayout;
    Text1: TText;
    Text2: TText;
    Text3: TText;
    Text4: TText;
    procedure ButtonSaveClick(Sender: TObject);
    procedure ButtonClearClick(Sender: TObject);
    procedure ButtonLoadClick(Sender: TObject);
    procedure FormCreate(Sender: TObject);
  private
    { Private declarations }
    AppPath: string;
    AppDatFile: String;
  public
    { Public declarations }
  end;

var
  Form1: TForm1;

implementation
{$R *.fmx}

uses
  System.IOUtils;

procedure TForm1.ButtonSaveClick(Sender: TObject);
var
  FileStream : TFileStream;
  MemStream : TMemoryStream;
begin
  FileStream := TFileStream.Create(AppDatFile, fmCreate);
  try
    MemStream := TMemoryStream.Create;
    MemStream.WriteComponent(Layout1);
    MemStream.Position := 0;
    ObjectBinaryToText(MemStream, FileStream);
  finally
    MemStream.Free;
    FileStream.Free;
  end;
end;

procedure TForm1.ButtonClearClick(Sender: TObject);
var
  i: Integer;
begin
  for i := pred(Layout1.ChildrenCount) downto 0 do
    Layout1.Children[i].Free;
end;

procedure TForm1.ButtonLoadClick(Sender: TObject);
var
  FileStream : TFileStream;
  MemStream : TMemoryStream;
begin
  if FileExists(AppDatFile) then
  begin
    FileStream := TFileStream.Create(AppDatFile, fmOpenRead);
    try
      MemStream := TMemoryStream.Create;
      ObjectTextToBinary(FileStream, MemStream);
      MemStream.Position := 0;
      MemStream.ReadComponent(Layout1);
      Layout1.Align:= TAlignLayout.Client;
    finally
      MemStream.Free;
      FileStream.Free;
    end;
  end;
end;

procedure TForm1.FormCreate(Sender: TObject);
begin
  AppPath:= TPath.GetLibraryPath;
  AppDatFile:= TPath.Combine(AppPath, 'SaveLoadLayout.dat');
end;

end

FMX File

  object Form1: TForm1
  Left = 0
  Top = 0
  Caption = 'Form1'
  ClientHeight = 480
  ClientWidth = 640
  FormFactor.Width = 320
  FormFactor.Height = 480
  FormFactor.Devices = [Desktop]
  OnCreate = FormCreate
  DesignerMasterStyle = 0
  object RecTop: TRectangle
    Align = Top
    Size.Width = 640.000000000000000000
    Size.Height = 41.000000000000000000
    Size.PlatformDefault = False
  end
  object ButtonSave: TButton
    Position.X = 8.000000000000000000
    Position.Y = 8.000000000000000000
    TabOrder = 3
    Text = 'Save'
    OnClick = ButtonSaveClick
  end
  object ButtonClear: TButton
    Position.X = 96.000000000000000000
    Position.Y = 8.000000000000000000
    TabOrder = 2
    Text = 'Clear'
    OnClick = ButtonClearClick
  end
  object ButtonLoad: TButton
    Position.X = 184.000000000000000000
    Position.Y = 8.000000000000000000
    TabOrder = 1
    Text = 'Load'
    OnClick = ButtonLoadClick
  end
  object Layout1: TLayout
    Align = Client
    Size.Width = 640.000000000000000000
    Size.Height = 439.000000000000000000
    Size.PlatformDefault = False
    TabOrder = 4
    object GridPanelLayout1: TGridPanelLayout
      Align = Client
      Size.Width = 640.000000000000000000
      Size.Height = 439.000000000000000000
      Size.PlatformDefault = False
      TabOrder = 0
      ColumnCollection = <
        item
          Value = 50.000000000000000000
        end
        item
          Value = 50.000000000000000000
        end>
      ControlCollection = <
        item
          Column = 0
          Control = Text1
          Row = 0
        end
        item
          Column = 1
          Control = Text2
          Row = 0
        end
        item
          Column = 0
          Control = Text3
          Row = 1
        end
        item
          Column = 1
          Control = Text4
          Row = 1
        end>
      RowCollection = <
        item
          Value = 50.000000000000000000
        end
        item
          Value = 50.000000000000000000
        end>
      object Text1: TText
        Align = Client
        Size.Width = 320.000000000000000000
        Size.Height = 219.500000000000000000
        Size.PlatformDefault = False
        Text = 'Text1'
      end
      object Text2: TText
        Align = Client
        Size.Width = 320.000000000000000000
        Size.Height = 219.500000000000000000
        Size.PlatformDefault = False
        Text = 'Text2'
      end
      object Text3: TText
        Align = Client
        Size.Width = 320.000000000000000000
        Size.Height = 219.500000000000000000
        Size.PlatformDefault = False
        Text = 'Text3'
      end
      object Text4: TText
        Align = Client
        Size.Width = 320.000000000000000000
        Size.Height = 219.500000000000000000
        Size.PlatformDefault = False
        Text = 'Text4'
      end
    end
  end
end

Solution

  • As I said in my comment, the problem is that WriteComponent wrongly write items with the format:

    Control = Form1.Text1
    

    This is not correct, it should be

    Control = Text1
    

    The behavior is maybe caused by the fact that serializing a component using other component, their owner is saved along.

    The workaround is to correct what WriteComponent write. A simple implementation using a simple ReplaceString is like this:

    procedure TForm1.ButtonSaveClick(Sender: TObject);
    var
        StringStream : TStringStream;
        MemStream    : TMemoryStream;
        Buf          : String;
    begin
        MemStream    := nil;
        StringStream := TStringStream.Create;
        try
            MemStream := TMemoryStream.Create;
            MemStream.WriteComponent(Layout1);
            MemStream.Position := 0;
            ObjectBinaryToText(MemStream, StringStream); 
            Buf := StringReplace(StringStream.DataString,
                                 '    Control = ' + Self.Name + '.',
                                 '    Control = ', [rfReplaceAll]);
            TFile.WriteAllText(AppDatFile, Buf);
        finally
            MemStream.Free;
            StringStream.Free;
        end;
    end;
    

    Be aware that this workaround implementation works for your example but could be confused because the search and replace do not use a real parser and could replace something else having the same form (A string property for example).