Boolean16
Boolean16

Reputation: 19

How to save a TObjectList in a Tstream

I need to save a TObjectList<TStrings> (or <TStringList>) in a TStream and then retrive it.

To be clear, how to apply SaveToStream and LoadFromStream to a TObjectList?

Upvotes: 0

Views: 1817

Answers (3)

Johan
Johan

Reputation: 76537

What's in your list?
It depends on what type of objects you have in your objectlist.
You loop over the list and save each item in turn.

However the objects inside your list need to have a SaveToStream method.
For reasons unknown SaveToStream is not a method of TPersistent, instead it is implemented independently in different classes.

Test for stream support
If the VCL were built with interfaces in mind, in newer versions has been solved with the IStreamPersist interface.
If all your stuff in the list descents from a base class that has streaming built-in (e.g. TComponent) then there is no problem and you can just use TComponent.SaveToStream.

type
  TStreamableClass = TStrings;  //just to show that this does not depend on TStrings.

procedure SaveToStream(List: TObjectList; Stream: TStream);
var
  i: integer;
begin
  for i:= 0 to List.Count -1 do begin
    if List[i] is TStreamableClass then begin
      TStreamableClass(List[i]).SaveToStream(Stream);
    end;
  end; {for i}
end;

Add stream support
If you have items in your list that do not derive from a common streamable ancestor then you'll have to have multiple if list[i] is TX tests in your loop.

If the object does not have a SaveToStream method, but you have enough knowledge of the class to implement it yourself, then you have twothree options.

A: implement a class helper that adds SaveToStream to that class or B: add a descendent class that implements that option.
If these are your own objects, then see option C: below.

type 
  TObjectXStreamable = class(TObjectX)
  public
    procedure SaveToStream(Stream: TStream); virtual;
    procedure LoadFromStream(Stream: TStream); virtual;
  end;

procedure SaveToStream(List: TObjectList; Stream: TStream);
...
  if List[i] is TObjectX then TObjectXStreamable(List[i]).SaveToStream(Stream);
...

Note that this approach fails if TObjectX has subclasses with additional data. The added streaming will not know about this extra data.

Option C: implement System.Classes.IStreamPersist

type
  IStreamPersist = interface
    ['<GUID>']
    procedure SaveToStream(Stream: TStream);
    procedure LoadFromStream(Stream: TStream);
  end;

  //enhance your streamable objects like so:
  TInterfaceBaseObject = TInterfacedObject //or TSingletonImplementation

  TMyObject = class(TInterfaceBaseObject, IStreamPersist)
    procedure SaveToStream(Stream: TStream); virtual;
    procedure LoadFromStream(Stream: TStream); virtual;

See: Bypassing (disabling) Delphi's reference counting for interfaces

You test the IStreamPersist support using the supports call.

if Supports(List[i], IStreamPersist) then (List[i] as IStreamPersist).SaveToStream(Stream);

If you have a newer version of Delphi consider using a generic TObjectList, that way you can limit your list to: MyList: TObjectList<TComponent>;
Now you can just call MyList[i].SaveToStream, because Delphi knows that the list only contains (descendents of) TComponent.

Upvotes: 1

Remy Lebeau
Remy Lebeau

Reputation: 595349

Try something like this:

procedure SaveListOfStringsToStream(List: TObjectList<TStrings>; Stream: TStream);
var
  Count, I: Integer;
  MStrm: TMemoryStream;
  Size: Int64;
begin
  Count := List.Count;
  Stream.WriteBuffer(Count, SizeOf(Count));
  if Count = 0 then Exit;
  MStrm := TMemoryStream.Create;
  try
    for I := 0 to Count-1 do
    begin
      List[I].SaveToStream(MStrm);
      Size := MStrm.Size;
      Stream.WriteBuffer(Size, SizeOf(Size));
      Stream.CopyFrom(MStrm, 0);
      MStrm.Clear;
    end;
  finally
    MStrm.Free;
  end;
end;

procedure LoadListOfStringsFromStream(List: TObjectList<TStrings>; Stream: TStream);
var
  Count, I: Integer;
  MStrm: TMemoryStream;
  Size: Int64;
  SList: TStringList;
begin
  Stream.ReadBuffer(Count, SizeOf(Count));
  if Count <= 0 then Exit;
  MStrm := TMemoryStream.Create;
  try
    for I := 0 to Count-1 do
    begin
      Stream.ReadBuffer(Size, SizeOf(Size));
      SList := TStringList.Create;
      try
        if Size > 0 then
        begin
          MStrm.CopyFrom(Stream, Size);
          MStrm.Position := 0;
          SList.LoadFromStream(MStrm);
          MStrm.Clear;
        end;
        List.Add(SList);
      except
        SList.Free;
        raise;
      end;
    end;
  finally
    MStrm.Free;
  end;
end;

Alternatively:

procedure SaveListOfStringsToStream(List: TObjectList<TStrings>; Stream: TStream);
var
  LCount, SCount, Len, I, J: Integer;
  SList: TStrings;
  S: UTF8String;
begin
  LCount := List.Count;
  Stream.WriteBuffer(LCount, SizeOf(LCount));
  if LCount = 0 then Exit;
  for I := 0 to LCount-1 do
  begin
    SList := List[I];
    SCount := SList.Count;
    Stream.WriteBuffer(SCount, SizeOf(SCount));
    for J := 0 to SCount-1 do
    begin
      S := UTF8String(SList[J]);
      // or, if using Delphi 2007 or earlier:
      // S := UTF8Encode(SList[J]);
      Len := Length(S);
      Stream.WriteBuffer(Len, SizeOf(Len));
      Stream.WriteBuffer(PAnsiChar(S)^, Len * SizeOf(AnsiChar));
    end;
  end;
end;

procedure LoadListOfStringsFromStream(List: TObjectList<TStrings>; Stream: TStream);
var
  LCount, SCount, Len, I, J: Integer;
  SList: TStrings;
  S: UTF8String;
begin
  Stream.ReadBuffer(LCount, SizeOf(LCount));
  for I := 0 to LCount-1 do
  begin
    Stream.ReadBuffer(SCount, SizeOf(SCount));
    SList := TStringList.Create;
    try
      for J := 0 to SCount-1 do
      begin
        Stream.ReadBuffer(Len, SizeOf(Len));
        SetLength(S, Len);
        Stream.ReadBuffer(PAnsiChar(S)^, Len * SizeOf(AnsiChar));
        SList.Add(String(S));
        // or, if using Delphi 2007 or earlier:
        // SList.Add(UTF8Decode(S));
      end;
      List.Add(SList);
    except
      SList.Free;
      raise;
    end;
  end;
end;

Upvotes: 3

Patrick Forest
Patrick Forest

Reputation: 13

You will need to create your own routine to do this: One for saving, the other for loading. For saving, loop through the list, convert each pointer of the list into is hexadecimal (decimal, octal) then add a separator character like ','; When done write the string contain to the stream. For loading, loop through the list, search for the first separator character, extract the value, convert it back as a pointer then add it to the list.

Procedure ObjListToStream(objList: TObjectList; aStream: TStream);
var
  str: String;
  iCnt: Integer;
Begin
  if not assigned(aStream) then exit; {or raise exception}
  for iCnt := 0 to objList.Count - 1 do 
  begin
    str := str + IntToStr(Integer(objList.Items[iCnt])) + ',';
  end;
  aStream.Write(str[1], Length(str));
End;

Procedure StreamToObjList(objList: TObjectList; aList: String);
var
  str: String;
  iCnt: Integer;
  iStart, iStop: Integer;
Begin
  try
  if not assigned(aStream) then exit; {or raise exception}
  iStart := 0;
  Repeat
    iStop := Pos(',', aList, iStart);
    if iStop > 0 then 
    begin
      objList.Add(StrToInt(Copy(sList, iStart, iStop - iStart)));
      iStart := iStop + 1;
    end;
  Until iStop = 0;
  except
    {something want wrong}
  end;
End;

I haven't test it and wrote it from memory. But it should point you in the right direction.

Upvotes: -2

Related Questions