1
votes

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
1
Maybe you would edit your question to add .pas and .fmx files so that interested people can test?fpiette
One of the requirements of StackOverflow is that the questions must be self containing, that is, links to external sites, which may become invalid are not allowed. So, please edit your question to include a minimal reproducible example, including the .fmx and .pas files. Please, focus on minimal, that still reproduces the problem.Tom Brunberg
Well, I tried my best to illustrate my question with screenshots. Most of the code is already there to understand my problem. Putting more in the question as suggested would make it even more difficult to readBob Baudewyns
It is ok and good to provide screenshots when the problem is related to layout of the UI, as it is in your case. Two screen shots is sufficient: expected and failure. But, screenshots do not show the structure of your UI, e.g. parent-child relations, and specifically not individual property settings. Your problem is likely related to these details. I already tried my take, based on your vague description, and it works perfectly OK. Clearly I was not able to reproduce your setup. Therefore it is important that you provide a minimal reproducible example including specifically the .fmx file.Tom Brunberg
Thanks for .pas and .fmx files. I'm able to reproduce the issue. The data file that the code create is not totally correct: ControlCollection has "Control = Form1.Text1" items while the .fmx has "Control = Text1". When I manually fix the data file to remove "Form1.", then it works correctly. So this is WriteComponent the culprit. It looks confused by the fact that it doesn't know Form1 and consider it as an external reference. No idea [yet] how to fix that in WriteComponent. A [temporary] solution would be to fix the datafile after having created it or before loading.fpiette

1 Answers

2
votes

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