Recent

Author Topic: Save record of array of record  (Read 1921 times)

valter.home

  • Jr. Member
  • **
  • Posts: 75
Save record of array of record
« on: April 30, 2021, 05:08:46 pm »
I would need to save a structure like the one below to disk.
Basically I draw several shapes on a background image and I should be able to save the work so I can reload it later.

Code: Pascal  [Select][+][-]
  1. {$modeswitch advancedrecords}
  2.  
  3. Type
  4.   TVertices = array of TPoint;
  5.  
  6.   TFigure1 = record
  7.     Shape: TVertices;
  8.     Name: String;
  9.     Color: TColor;
  10.   end;
  11.  
  12.   TFigure2 = record
  13.     Shape: TVertices;
  14.     Name: String;
  15.     Color: TColor;
  16.   end;
  17.  
  18.   TFigure3 = record
  19.     Shape: TVertices;
  20.     Name: String;
  21.     Color: TColor;
  22.   end;
  23.  
  24.   TContainer = record
  25.     Figure1: array of TFigure1;
  26.     Figure2: array of TFigure2;
  27.     Figure3: array of TFigure3;
  28.   end;

I found some examples but for a single records.
I think the correct way is to use a stream, could someone give me some information about it?

Bart

  • Hero Member
  • *****
  • Posts: 4282
    • Bart en Mariska's Webstek
Re: Save record of array of record
« Reply #1 on: April 30, 2021, 06:01:50 pm »
Basically for managed types you first write the length of the data, then the data itself.
So for TVertices you first write the length of the TVertices instance, then for each TPoint you write X and Y.

Bart

lucamar

  • Hero Member
  • *****
  • Posts: 4009
Re: Save record of array of record
« Reply #2 on: April 30, 2021, 06:22:15 pm »
Something like this (untested!):

Code: Pascal  [Select][+][-]
  1. {$modeswitch advancedrecords}
  2.  
  3. Type
  4.   TVertices = array of TPoint;
  5.  
  6.   { All TFiguresX were the same, so it suffices with just one type }
  7.  
  8.   { TFigure }
  9.  
  10.   TFigure = record
  11.     Shape: TVertices;
  12.     Name: String;
  13.     Color: TColor;
  14.     procedure SaveToStream(AStream: TStream);
  15.     procedure LoadFromStream(AStream: TStream);
  16.   end;
  17.  
  18.   TContainer = record
  19.     Figures1: array of TFigure;
  20.     Figures2: array of TFigure;
  21.     Figures3: array of TFigure;
  22.   end;
  23.  
  24.   {... etc ...}
  25.  
  26. procedure TFigure.SaveToStream(AStream: TStream);
  27. var
  28.   APoint: TPoint;
  29. begin
  30.   AStream.WriteDWord(Length(Shape));
  31.   for APoint in Shape do
  32.     AStream.Write(APoint, SizeOf(TPoint));
  33.   AStream.WriteAnsiString(Name);
  34.   AStream.Write(Color, SizeOf(Color));
  35. end;
  36.  
  37. procedure TFigure.LoadFromStream(AStream: TStream);
  38. begin
  39.   SetLength(Shape, AStream.ReadDWord);
  40.   for i := 0 to Length(Shape)-1 do
  41.     AStream.Read(Shape[i], SizeOf(TPoint));
  42.   Name := AStream.ReadAnsiString;
  43.   AStream.Read(Color, SizeOf(Color));
  44. end;
  45.  
  46. {To save a "container" ...}
  47. procedure SaveContainer(AContainer: TContainer; const AFileName: String);
  48. var
  49.   AStream: TFileStream;
  50.   AFigure: TFigure;
  51. begin
  52.   AStream := TFileStream.Create(AFileName, fmOpenWrite or fmCreate);
  53.  
  54.   AStream.WriteDWord(Length(AContainer.Figures1));
  55.   for AFigure in AContainer.Figures1 do
  56.     AFigure.SaveToStream(AStream);
  57.  
  58.   AStream.WriteDWord(Length(AContainer.Figures2));
  59.   for AFigure in AContainer.Figures2 do
  60.     AFigure.SaveToStream(AStream);
  61.  
  62.   AStream.WriteDWord(Length(AContainer.Figures3));
  63.   for AFigure in AContainer.Figures3 do
  64.     AFigure.SaveToStream(AStream);
  65. end;
  66.  
  67. { and conversely to load it back }

Turbo Pascal 3 CP/M - Amstrad PCW 8256 (512 KB !!!) :P
Lazarus/FPC 2.0.8/3.0.4 & 2.0.12/3.2.0 - 32/64 bits on:
(K|L|X)Ubuntu 12..18, Windows XP, 7, 10 and various DOSes.

Bart

  • Hero Member
  • *****
  • Posts: 4282
    • Bart en Mariska's Webstek
Re: Save record of array of record
« Reply #3 on: April 30, 2021, 11:55:23 pm »
Alternatively use a class helper for TStream:
(Untested code)
Code: Pascal  [Select][+][-]
  1. {$mode objfpc}
  2. {$h+}
  3.  
  4. uses
  5.   SysUtils, Classes;
  6.  
  7. type
  8.   TColor = -$7FFFFFFF-1..$7FFFFFFF;
  9.  
  10. type
  11.   TVertices = array of TPoint;
  12.  
  13.   TFigure = record
  14.     Shape: TVertices;
  15.     Name: String;
  16.     Color: TColor;
  17.   end;
  18.  
  19.   TFigureArray = array of TFigure;
  20.  
  21.   TContainer = record
  22.     Figure1: TFigureArray;
  23.     Figure2: TFigureArray;
  24.     Figure3: TFigureArray;
  25.   end;
  26.  
  27.   { TStreamHelper }
  28.  
  29.   TStreamHelper = class helper for TStream
  30.     procedure WritePoint(P: TPoint);
  31.     procedure WriteVertices(V: TVertices);
  32.     procedure WriteFigure(F: TFigure);
  33.     procedure WriteFigureArray(F: TFigureArray);
  34.     procedure WriteContainer(C: TContainer);
  35.   end;
  36.  
  37. { THelper }
  38.  
  39. procedure TStreamHelper.WritePoint(P: TPoint);
  40. begin
  41.   Self.WriteDWord(P.X);
  42.   Self.WriteDWord(P.Y);
  43. end;
  44.  
  45. procedure TStreamHelper.WriteVertices(V: TVertices);
  46. var
  47.   DataSize: Int64;
  48.   P: TPoint;
  49. begin
  50.   DataSize := Length(V);
  51.   Self.WriteQWord(DataSize);
  52.   for P in V do Self.WritePoint(P);
  53. end;
  54.  
  55. procedure TStreamHelper.WriteFigure(F: TFigure);
  56. begin
  57.   Self.WriteVertices(F.Shape);
  58.   Self.WriteAnsiString(F.Name);
  59.   Self.Write(F.Color, SizeOf(TColor));
  60. end;
  61.  
  62. procedure TStreamHelper.WriteFigureArray(F: TFigureArray);
  63. var
  64.   DataSize: Int64;
  65.   Fig: TFigure;
  66. begin
  67.   DataSize := Length(C);
  68.   Self.WriteQWord(DataSize);
  69.   for Fig in F do Self.WriteFigure(Fig);
  70. end;
  71.  
  72. procedure TStreamHelper.WriteContainer(C: TContainer);
  73. begin
  74.   Self.WriteFigureArray(C.Figure1);
  75.   Self.WriteFigureArray(C.Figure2);
  76.   Self.WriteFigureArray(C.Figure3);
  77. end;
  78.  
  79. var
  80.   FS: TFileStream;
  81.   Container: TContainer;
  82. begin
  83.   //Create the container
  84.   //...
  85.   FS := TFileStream.Create('test.bin', fmCreate);
  86.   FS.WriteContainer(Container);
  87.   FS.Free;
  88.   //Free the container
  89. end.

Bart

valter.home

  • Jr. Member
  • **
  • Posts: 75
Re: Save record of array of record
« Reply #4 on: May 01, 2021, 01:04:06 pm »
Thanks for your answers.
I am currently working on lucamar's suggestion.

I wrote the procedure for reading the stream (for the moment only for one figure).

Code: Pascal  [Select][+][-]
  1. procedure LoadContainer(AContainer: TContainer; const AFileName: String);
  2. var
  3.   AStream: TFileStream;
  4.   AFigure: TFigure;
  5.   i: integer;
  6. begin
  7.   AStream := TFileStream.Create(AFileName, fmOpenRead);
  8.   //AStream.Seek(0,0);
  9.   SetLength(AContainer.Figure1, AStream.ReadDWord);
  10.   for AFigure in AContainer.Figure1 do
  11.   begin
  12.     AFigure.LoadFromStream(AStream);
  13.     memo.lines.add(AFigure.Name);
  14.     for i := 0 to Length(AFigure.Shape)-1 do
  15.       begin
  16.         memo.lines.add(AFigure.Shape[i].X.ToString+'   '+
  17.         AFigure.Shape[i].Y.ToString);
  18.       end;
  19.   end;
  20.   AStream.Free;
  21. end;

It would seem to work but now I'm thinking about how to proceed. Do I have to iterate for each figure copying the data one by one or would there be a way to directly assign AContainer to TContainer with all its contents?
I have tried Move () with no success.
I haven't found enough information around.



lucamar

  • Hero Member
  • *****
  • Posts: 4009
Re: Save record of array of record
« Reply #5 on: May 01, 2021, 01:51:34 pm »
You can't use Move() here, because there are managed types in your records (the strings) which are de facto pointers. The best, if not the only, solution then is to deal with each TFigure separately.

Note that your main bottleneck, in this case, is I/O and would be so even if you could find some "clever" solution. Unless you'll be dealing with transfers of thousands of TFigure from one container to another (in memory) a simple loop should be fast enough.

ETA: Unless that's not what you meant? Please, try to be as specific as you can to tell us what you want to achieve.

And please, please, please don't name variables like TContainer, leave that kind of name for types. Otherwise it's confusing because that's the "normal" convention:
- TSomething is the type, and
- ASomething, SomethingA, etc are variables of that type.
« Last Edit: May 01, 2021, 01:59:53 pm by lucamar »
Turbo Pascal 3 CP/M - Amstrad PCW 8256 (512 KB !!!) :P
Lazarus/FPC 2.0.8/3.0.4 & 2.0.12/3.2.0 - 32/64 bits on:
(K|L|X)Ubuntu 12..18, Windows XP, 7, 10 and various DOSes.

valter.home

  • Jr. Member
  • **
  • Posts: 75
Re: Save record of array of record
« Reply #6 on: May 01, 2021, 03:22:47 pm »
Quote
Please, try to be as specific as you can to tell us what you want to achieve.

What I would like to achieve is this:
For example, I draw about ten polygons (at most they could be about fifty, no more), which can all have equal or different numbers of vertices, but also different properties. Name and color are present in all of them but some may have, for example, type of cursor when the mouse passes over it. So TFigure1, TFigure2 and TFigure3 are different records (sorry if I overlooked this in my post but I did it for simplicity).
Then I save the properties of the figures in a stream.
So I close the program, reopen it and I would like to load the saved work, that is to find all the figures in their original position with all their saved data.
With your suggestions I managed to save correctly in the stream and when I reload it the data are all present.
But at this point it's all in the stream and I should overwrite my TContainer with the contents of the stream.
For example, I draw 5 polygons, save them, delete them, draw others but I don't like them, I would like to load the ones I just saved.

Quote
And please, please, please don't name variables like TContainer
Actually TContainer is not in fact a variable but it is the declared type. Then in the variables I have FContainer: TContainer;
Where in the code did you see this? So maybe I'm doing something wrong.


ASerge

  • Hero Member
  • *****
  • Posts: 1796
Re: Save record of array of record
« Reply #7 on: May 01, 2021, 04:09:50 pm »
Code: Pascal  [Select][+][-]
  1. procedure LoadContainer(AContainer: TContainer; const AFileName: String);
  2. var
  3.   AStream: TFileStream;
  4.   AFigure: TFigure;
  5.   i: integer;
  6. begin
  7.   AStream := TFileStream.Create(AFileName, fmOpenRead);
  8.   //AStream.Seek(0,0);
  9.   SetLength(AContainer.Figure1, AStream.ReadDWord);
  10.   for AFigure in AContainer.Figure1 do
  11.   begin
  12.     AFigure.LoadFromStream(AStream);
  13.     memo.lines.add(AFigure.Name);
  14.     for i := 0 to Length(AFigure.Shape)-1 do
  15.       begin
  16.         memo.lines.add(AFigure.Shape[i].X.ToString+'   '+
  17.         AFigure.Shape[i].Y.ToString);
  18.       end;
  19.   end;
  20.   AStream.Free;
  21. end;
Use "for..in" loop variable only as a read-only variable.
Because in this case, the compiler will copy the value from the array AContainer to the variable AFigure. Then, LoadFromStream will be called for this AFigure variable. However, the data in the AContainer array will not change.

In this case, the usual loop with the index is more appropriate.

jamie

  • Hero Member
  • *****
  • Posts: 4565
Re: Save record of array of record
« Reply #8 on: May 01, 2021, 04:30:47 pm »
for controls that already exists, can't the existing TFiler, TReader, TWrite code be used here ?
The only true wisdom is knowing you know nothing

PascalDragon

  • Hero Member
  • *****
  • Posts: 3056
  • Compiler Developer
Re: Save record of array of record
« Reply #9 on: May 01, 2021, 04:42:44 pm »
The TFiler, TReader, TWriter can only be used together with TComponent and their descendants.

However a generic (de)serializer for records that only contain primitive types, other records and arrays (both static and dynamic) can be created by making use of the RTTI FPC generates (except for the field names however, so one would need to rely on the offset, so a binary representation would be possible, a JSON one less...).

lucamar

  • Hero Member
  • *****
  • Posts: 4009
Re: Save record of array of record
« Reply #10 on: May 01, 2021, 05:31:49 pm »
I have found that, for records, using advanced records and adding LodFrom/SaveToStream (making use of TStream's specialized methods, if needed) is often the more convenient way to load/save them.

And since you can use any TStream descendant you can load/save records from almost anywhere, not just disk files, which is quite a bonus ;)

Add to that SysUtils type helpers with their almost bewildering array of conversion methods and you can load/save in almost any (simple) format your heart desires. ;D
Turbo Pascal 3 CP/M - Amstrad PCW 8256 (512 KB !!!) :P
Lazarus/FPC 2.0.8/3.0.4 & 2.0.12/3.2.0 - 32/64 bits on:
(K|L|X)Ubuntu 12..18, Windows XP, 7, 10 and various DOSes.

 

TinyPortal © 2005-2018