Link to home
Start Free TrialLog in
Avatar of abulka
abulka

asked on

saving treeview + data nodes to file

I need some strategy and code to save my treeview to a file.  Each Ttreenode has a .data pointer to a Tnodeinfo object (which I defined myself).  I want the nodeinfo data saved along with the tree.  The Tnodeinfo object comprises a few strings, booleans and TstringLists.
I know that the treeview may support streaming (being a component) but my nodeinfo objects need to go along for the ride...
Avatar of Epsylon
Epsylon

You have to write your own code. Derive a new Object from TCustomTreeView and write your own methods for loading and saving. Look at TCustomTreeView.LoadFromStream and TCustomTreeView.SaveToStream methods how they are implemented.
Particulary look at TTreeStrings.LoadTreeFromStream and TTreeStrings.SaveTreeToStream.

Note that saved treeviews are files with strings with one or more leading tabs if the item is a child
Avatar of kretzschmar
HI epsylon,

could it be that your answer is a standard-solution? Like: a Problem? -> Create a new Component!

abulka, how will you save your tree, in a flat-file or Database?
i think you should create a record where stored the nodeBeforeID and your nodeData.
a bit code could be available tomorrow.

meikl
Avatar of abulka

ASKER

Thanks for the idea to derive from TCustomTreeview.  Surely there is some generic code for all this? - after all - all that is individual to ME is the type of object I am storing in the tree - others would have a different type of object being stored.  Apart from this (and the particulars of saving/loading to a stream) - the code for the component should be the same for everyone?

Also, I need some guidance on whether some of the custom tree view component's properties would be returning objects of the type that I am storing in it - so that no typecasting is required by the component user.  The typecasting would be hidden in the component.

P.S.  The object I am storing is simply defined in a unit thus:

unit nodeinfo;

interface

uses
  SysUtils, Windows, Messages, Classes, Graphics, Controls, Forms, Dialogs,
  StdCtrls, ComCtrls, Menus, ImgList, ExtCtrls, Buttons, ToolWin, Boxes;

type
    TnodeInfo = class(tobject)
        name, address: string;
        flag1: boolean;
        lots: TstringList;
    public
        constructor Create; overload;
        destructor Destroy; overload;
    end;

implementation

{ TnodeInfo }

constructor TnodeInfo.Create;
begin
     inherited Create;
     name := 'hi there';
     lots := TstringList.Create;
end;

destructor TnodeInfo.Destroy;
begin
     lots.free;
     inherited Destroy;
end;

end.
Avatar of abulka

ASKER

> abulka, how will you save your tree, in a flat-file or Database?
I want to store my objects by streaming them to a file.
Should I define my object as a descendant from Tcomponent or Tpersistent in order to get some streaming support?  Currently (see my comment above) I derive my stored object from plain ole' Tobject.

>     i think you should create a record where stored the nodeBeforeID and your nodeData.
>     a bit code could be available tomorrow.
I am open to the idea of keeping the info in a database.  I am about to explore Igor Komar's tboutline component which does something like this.  Maelstrom did good work in this area too - what are your opinions on these 3rd party solutions?

abulka,

have a look at my TreeNT control, which supports polymorphic tree node creation including Write and Read methods for a node. Download it from www.delphipages.com (tree/list section).

You could then derive a new node class which includes the fields you need (leaving so the Data property alone).

Ciao, Mike
Hi Mike,

great, i've downloaded it.

Hi abulka,

therefore that your Nodeinfo-Object has a dynamical Object (lots) in it, i think, that it will not become a standardsolution. My first try to store your tree with nodeinfo into a flatfile goes over a fileconversion. Following conversion i have made:

Level{NodeName
    FieldName
    FieldAddress
    FieldFlag1(0,1)
    {
        Lots.Line1
              .
              .
              .
        Lots.LineN
    }
}

example

0{Node1
    Name1
    Address1
    1
    {
        Line1ToNode1
        Line2ToNode1
        Line3ToNode1
    }
}
1{ChildToNode1
    Name1Child1
    Address1Child1
    0
    {
        Line1ToNode1Child1
        Line2ToNode1Child1
    }
}
1{Child2ToNode1
    Name1ToChild2
    Address1ToChild2
    1
    {
        Line1ToNode1Child2
        Line2ToNode1Child2
    }
}
2{Child1ToChild2ToNode1
    Name1Child2Child1
    Address1Child2Child1
    0
    {
        Line1ToNode1Child2Child1
        Line2ToNode1Child2Child1
    }
}




The Load and Save Coding (specially for your NodeInfo) (fast and simple coded, no exceptions handled) are

procedure TForm1.SaveTreeClick(Sender: TObject);
Var
  ANode : TTreeNode;
  MyFile : TextFile;
  I : Integer;
begin
  If savedialog1.Execute then
  begin
    AssignFile(MyFile,savedialog1.FileName);
    rewrite(MyFile);
    ANode := Tv1.TopItem;
    while Anode <> Nil do
    begin
      Writeln(MyFile,IntToStr(ANode.Level)+'{'+ANode.Text);
      If ANode.data <> Nil then
      begin
        MyNodeInfo := ANode.data;
        Writeln(MyFile,'    '+MyNodeInfo.Name);
        Writeln(MyFile,'    '+MyNodeInfo.Address);
        If MyNodeInfo.flag1 then
          Writeln(MyFile,'    '+'1')
        else
          Writeln(MyFile,'    '+'0');
        Writeln(MyFile,'    '+'{');
        For i := 0 to MyNodeInfo.Lots.count - 1 do
          Writeln(MyFile,'        '+MyNodeInfo.lots.Strings[i]);
        Writeln(MyFile,'    '+'}');
      end;
      Writeln(MyFile,'}');
      ANode := ANode.GetNext;
    end;
    CloseFile(MyFile);
  end;
end;

procedure TForm1.LoadTreeClick(Sender: TObject);
Var
  Owner : TTreeNodes;
  s, NodeID : String;
  ANode, NextNode : TTreeNode;
  MyFile : TextFile;
  ALevel, I : Integer;
begin
  If OpenDialog1.Execute then
  begin
    Owner := TV1.Items;
    ANode := Nil;
    AssignFile(MyFile,Opendialog1.FileName);
    Reset(MyFile);
    while not(eof(MyFile)) do
    begin
      readln(MyFile,NodeID); NodeID := Trim(NodeID);
      MyNodeInfo := TNodeInfo.Create;
      Readln(MyFile,s); s := Trim(s);
      if s <> '}' then
      begin
        MyNodeInfo.name := s;
        Readln(MyFile,s); s := Trim(s);
        MyNodeInfo.Address := s;
        Readln(MyFile,s); s := Trim(s);
        if s = '1' then
          MyNodeInfo.Flag1 := True
        else MyNodeInfo.flag1 := False;
        Readln(MyFile,s);
        Readln(MyFile,s); s := Trim(s);
        While pos('}',s) = 0 do
        begin
          MyNodeInfo.lots.Add(s);
          readln(MyFile,s); s := Trim(s);
        end;
        readln(MyFile,s);
      end;
      { Where is the Node in the tree, adepted from TTreeStrings.LoadTreeFromStream}
      ALevel := StrToInt(copy(NodeId,1,pos('{',NodeId)-1));
      delete(NodeId,1,pos('{',NodeId));
      if ANode = nil then
        ANode := Owner.AddChildObject(nil, NodeId, MyNodeInfo)
      else
        if ANode.Level = ALevel then
          ANode := Owner.AddChildObject(ANode.Parent, NodeId, MyNodeInfo)
        else
          if ANode.Level = (ALevel - 1) then
            ANode := Owner.AddChildObject(ANode, NodeId, MyNodeInfo)
          else
            if ANode.Level > ALevel then
            begin
              NextNode := ANode.Parent;
              while NextNode.Level > ALevel do
                NextNode := NextNode.Parent;
              ANode := Owner.AddChildObject(NextNode.Parent, NodeId, MyNodeInfo);
            end;
    end;
    CloseFile(MyFile);
  end;
end;

It works for me.
Maybe there will better solutions from other experts.

meikl
Hi abulka.
I work about a year ago with the following code:
It works even if you save other information to the same stream.

procedure SaveTreeDates(Const Treeview1: TTreeView; Const Stream: TFileStream);
var
  i, SizePos, EndPos, Anz, Data: Integer;
  Level: Byte;
  s: String[40];
begin
  SizePos:=Stream.Position;
  Anz:=0;
  Stream.Write(Anz, SizeOf(Anz));
  With TreeView1 do
  begin
    i:=0;
    while i<Items.Count do
    begin
        Level:=Items[i].Level;
        Stream.Write(Level,SizeOf(Level));
        Level:=Items[i].ImageIndex;
        Stream.Write(Level,SizeOf(Level));
        Data:=Integer(Items[i].Data);
        Stream.Write(Data,SizeOf(Data));
        s:=Items[i].Text+'                                        ';
        SetLength(s,40);
        Stream.Write(s[1],40);
        inc(i);
        inc(Anz);
    end;
    EndPos:=Stream.Position;
    Stream.Position:=SizePos;
    Stream.Write(Anz, SizeOf(Anz));
    Stream.Position:=EndPos;
  end;
end;

procedure LoadTreeDates(Const Treeview1: TTreeView; Const Stream: TFileStream);
var
  i, Anz, Data: Integer;
  Level, OldLevel, ImageIndex: Byte;
  s: String[40];
  LastNode: TTreeNode;
  OldSortType: TSortType;
begin
  OldSortType:=Treeview1.SortType;
  Treeview1.SortType:=stNone;
  Stream.Read(Anz, SizeOf(Anz));
  if Anz>0 then
    With TreeView1 do
    begin
      Items.Clear;
      OldLevel:=0;
      LastNode:=nil;
      for i:=1 to Anz do
      begin
       Stream.Read(Level,SizeOf(Level));
       Stream.Read(ImageIndex, SizeOf(ImageIndex));
       Stream.Read(Data, SizeOf(Data));
       SetLength(s,40);
       Stream.Read(s[1],40);
       s:=Trim(s);
       if Level=OldLevel then
         LastNode:=Items.AddObject(LastNode, s, TObject(Data));
       if Level>OldLevel then
         LastNode:=Items.AddChildObject(LastNode, s, TObject(Data));
       if Level<OldLevel then
       begin
         while Level<OldLevel do
         begin
           LastNode:=LastNode.Parent;
           OldLevel:=LastNode.Level;
         end;
         LastNode:=Items.AddObject(LastNode, s, TObject(Data));
       end;
       OldLevel:=Level;
       LastNode.ImageIndex:=ImageIndex;
       LastNode.SelectedIndex:=ImageIndex;
     end;
   end;
  Treeview1.SortType:=OldSortType;
end;

I hope that's what you are looking for.
regards, ptm.
Avatar of abulka

ASKER

Lischke/Mike - From my interpretation of the comment, kretzschmar says your component cannot handle storing my nodeinfo because it contains an internal TstringsList.  Is this true?

Thanks kretzschmar - Thanks for the code - I haven't tried it yet - but I would have to recode the routines everytime I changed the data structure (which will be often) - and older file versions could not be read in, leading to version control problems. Another solution viz. streaming the object properties is simpler and avoids version problems - as far as I know.

Thanks ptmcomp - I like the use of streams, but I'm not sure your code handles my datatype 'nodeinfo' or the internal TstringsList it contains.

Surely there is a solution which leverages the intelligence of the streaming system, and even better - takes advantage of no-code automatic streaming (by virtue of say, deriving my nodeinfo object from Tcomponent or Tpersistent)?
My method I showed you does the following:
-It saves the tree structure with the node textes
-It saves the image indexes of the images of the treenodes
-It saves the node.data (pointer)

If you want to save sth. diffrent or additional you can set there a stream.write command more.
e.g.: stream.write(MyStringList.CommaText);
Here is a more object oriented method:

To do this the TnodeInfo object in the tree should have its own load and save method. So when saving the items of a tree with TnodeInfo objects, first save the name of the first item. Then call the save method of the first TnodeInfo object. Do the same with the second node, and the third and so on...

You can derive new objects, with additional info fields, from TnodeInfo and chain them in the tree. Override the loadit and saveit method in the derives class and add code to load and save the additional info. The loadtree and savetree procedures will then automatically do the proper loading and saving.


  TnodeInfo = class(tobject)
    name, address: string;
    flag1: boolean;
    lots: TstringList;
  public
    constructor Create;
    destructor Destroy;
    procedure loadit(stream);
    procedure saveit(stream);
  end;

  TnodeAditionalInfo = class(TnodeInfo)
    additionalinfo: string;
  public
    procedure loadit(stream); override;
    procedure saveit(stream); override;
  end;

procedure TnodeInfo.loadit(stream);
begin
  // put code here to load info from stream
end;

procedure TnodeInfo.saveit(stream);
begin
  // put code here to save info to stream
end;

procedure TnodeAditionalInfo.loadit(stream);
begin
  inherited;
  // put code here to load additional info from stream
end;

procedure TnodeAditionalInfo.saveit(stream);
begin
  inherited;
  // put code here to save additional info to stream
end;

-----------------------------------------------------

in pseudo code the savetree procedure:

stream:=tfilestream.create(filename)
for i:=0 to treeview1.items.count-1 do
begin
  savestringtostream(stream, items.item[i].text) // add leading tabs to remember the level
  TnodeInfo(TreeView1.Items.Item[0].Data).saveit(stream);
end
stream.free


in pseudo code the loadtree procedure:

stream:=tfilestream.create(filename)
numberofitems := strtoint(loadstringfromstream)
while not endoffile do
begin
  loadstringfromstream(stream, astring) // check leading tabs to recover the level
  addeditem:=AddtoTreeview(astring,level) // use add or addchild etc. according to the level
  TnodeInfo(addeditem.Data).loadit(stream);
end
stream.free

abulka,

you should be aware that every solution using the stream system of Delphi will give you problems when working with different versions of the the saved information. This is inparticular true when the structure of the to be saved/ to lodaded data/structure changes, because Delphi's streaming system relies heavily on a well know structure.

You should consider using chunks or something like that as this will make your loader able to evetually skip unkown data it doesn't understand. This approach is very similar to 3DS files or GIF/TIF tags. It would, though, require a much more sophisticated save/restore behaviour and you cannot use the default streaming.

To your question regarding TreeNT: I admit I never tried to store classes along with the node data, but it should be possible whithout great problems. Each node stores its data in the Writer stream and can then store any other value as well, even components (see Writer.WriteComponent etc.). The hierarchical structure of the streaming system makes it possible. But as already mentioned, you will probably get problems when the structure changes and you want to load a newer file with an older version. On the other hand it could well be that the use of classes in the node info will just be the necessary step to store version independent data, since an older class (in opposition to a simple structure) can skip the unknown data (similar to skipping unknown properties while loading a project in the IDE).

Ciao, Mike
Avatar of abulka

ASKER

Where are we so far?  The solution of looping through the treeview and writing the nodeInfo object data + Ttreenode text to a file (kretzschmar) or to a stream (ptmcomp) is a classic solution to my problem of storing treeView data.  Epsylon's more o-o technique is a nice version of the same technique.  However, as Lischke & I have pointed out, the file format created this way is extremely 'fragile' to version changes in the ttreeview object definition, requiring constant maintenance of the code and version handling nightmares.  I plan to be evolving my nodeInfo object many, many times - and I need a solution that works like VCL component streaming - viz. store & retrieve properties - which will work even if the object definiton is changed (encountering a property name and value in the stream and setting the property of an object is far more robust than expecting a property/data at a certain exact point in the stream).

I suspect the best solution involves streaming components in the same way (& using the same format as) the VCL does & depends on the use of TReader & TWriter & TStream.ReadComponent & Tcomponent.Getchildren etc.

In addition, there is the question of who controls the writing of the data to the stream.  Well, if the nodeInfo data object is somehow 'hooked into the component' so that the nodeInfo published properties are auto-streamed - that would be magic.  In order to do the hooking, I thought that perhaps I could define my custom nodeInfo object as a descendant of TComponent, and set its owner to the treeview?  Or perhaps I need to add each nodeInfo object into someone's component array?  The role of the TtreeNode object to which my nodeInfo object is attached is another factor to be considered - TTreeNode is not a component, but derived from TPersistent.

Also there are "streaming in / reading" considerations:  When reading my tree data back into the treeview, there is the issue of whether I want to load in the entire treeview component, or just the nodes & data stored in it?  Does this distinction make any sense in light of the discovery I made a few nights ago that, in a deep sense, components are never really stored in a stream - just their properties are?  Can I and should I save a component FROM a certain level downwards (e.g. from inside the treeview downwards but not including the treeview itself), thereby avoiding the issue of having to save the treeview itself?  This last question is something similar to the idea that we can either save a form and all the components in it - or we can just save (by looping through the forms controls array) the components on the form, leaving the form out of the saved stream.
I know that in some cases Delphi will create new components when reading things back from a stream... and in other cases properties encountered in the stream are assigned to an *existing* object.  A parameter to certain methods controls this behaviour - but what happens for example when all the objects exist but I don't know what component is to be encountered next in the stream - do I pass nil (and have a new component created which I don't want) or pass a random!? instance (and have possibly the wrong component's properties assigned one of my randomly selected, already existing components?).  Obviously I'm missing something here.

I AM NOT ALONE!  Marco Cantu states in his "Delphi Developers Handbook" p. 99 "To read or write and object's data in a stream, you can take either of two routes.  The first approach is to create your own "read/write data" methods and then call those methods from within your custom streaming framework.  This is the simplest solution, and one you have probably seen in various articles in Delphi magazines.  The other technique involves learning to stream components the same way the VCL does..."  He then goes on for quite a few pages describing a complicated technique involving Tpersistent derived objects and Tcollection, only to conclude that p112 "it is possible to stream noncomponent objects...because we'll have to create a wrapper component to stream a noncomponent object anyway, deriving the object from the Tcomponent class may be the best solution (unless you need to stream a collection of these objects).  .... In the last example, we saw that it was rather simple to save components to a stream.  In fact, unless you want to create your own stream framework, its the only way to save user-defined data types.  For this reason, most Delphi programmers derive new classes from the Tcomponent class rather than from TPersistent.  Obviously, then, we could rewrite the Persist example [Cantu is quoting an example out of his book] rather easily to derive the TDdhPoint class from Tcomponent, replace the TCollection of points with a TList, and then store and load each one from the stream.  Better yet, we could create the point objects with another component as their owner, and then use the TStream,WriteComponent method to save that component (and, via the TWriter.WriteRootComponent method, save all the points)."

Unfortunately, Cantu does not give an example of this technique.  However imagine my suprise when I saw this in the Delphi Component Writing FAQ  (found in comp.lang.pascal.delphi.components.writing)
------------------------------------------------------------------------
Section 7 - Persistant Objects

7.1. How can I save a complex object containing child objects to the
     .DFM file.

I have tried all sorts of schemes using DefineProperties and
WriteComponents and they all failed to work. As far as I can tell the
only way to do this is to use Delphi's default mechanism to store your
child objects.

A sequence that does work for saving to a stream is:

1. Make all of the classes whose objects you want to save descend from
   TComponent.
2. Make all of the values you want to save published.
3. Within your Register procedure add a call to RegisterComponents
   containing all of the classes you wish to store.
4. Each class that owns child classes needs to overload the procedure
   GetChildren. This procedure is needs to call the procedure passed
   as an argument for each child to be stored. (For Delphi V1 you need
   to override the WriteComponents method and call WriteComponent for
   each child.)


Procedure TMyComponent.GetChildren (Proc : TGetChildProc) ;
  Begin
  Proc (Child1) ;
  Proc (Child2) ;
  ...
  Proc (Childn) ;
  End ;

Getting the objects out of the stream is a little trickier.  Your
parent object may need to overload the GetChildOwner and
GetChildParent functions.  Otherwise Delphi will try to make the child
owned by the form. (In Delphi V1 you need to override the Readstate
method.)
----------------------------------------- END EXTRACT FROM F.A.Q.

The actual details of the above recipe and code are missing and perhaps someone could fill them in for my situation, if appropriate.  The other references that I have been reading (and trying to understand) are Danny Thorpes "Delphi Component Design" p 153 chapter on streaming and Ray Lischner's "Secrets of Delphi 2" p 105 on Form Files and reading and writing components.

I'm convinced there must be an ultra simple solution to all this, after all many Delphi books extoll the virtues of the component & streaming system and how in the BAD OLD DAYS programmers had to write custom code to save their data - I want to take advantage of the GOOD NEW DAYS!

Use the TTreeView's SaveToFile and LoadToFile. It saves all the data of a tree to a binay file that can later be loaded with LoadFromFile.
Avatar of abulka

ASKER

TTreeView's SaveToFile and LoadToFile may save the text of the treenodes to and from a file - however these methods do not store the objects/data pointed to by each TTreeNode .data property.  It is important to me to save this additional data.  That's the whole point of this thread.
Avatar of abulka

ASKER

Hey - looks like this thread has stopped dead after my long (but hopefully interesting) rave /  comment earlier.
How about we define the solution to this thread as someone supplying code for the FAQ recipe referred to above viz:

7.1. How can I save a complex object containing child objects to the
          .DFM file.

     I have tried all sorts of schemes using DefineProperties and
     WriteComponents and they all failed to work. As far as I can tell the
     only way to do this is to use Delphi's default mechanism to store your
     child objects.

     A sequence that does work for saving to a stream is:

     1. Make all of the classes whose objects you want to save descend from
        TComponent.
     2. Make all of the values you want to save published.
     3. Within your Register procedure add a call to RegisterComponents
        containing all of the classes you wish to store.
     4. Each class that owns child classes needs to overload the procedure
        GetChildren. This procedure is needs to call the procedure passed
        as an argument for each child to be stored. (For Delphi V1 you need
        to override the WriteComponents method and call WriteComponent for
        each child.)


     Procedure TMyComponent.GetChildren (Proc : TGetChildProc) ;
       Begin
       Proc (Child1) ;
       Proc (Child2) ;
       ...
       Proc (Childn) ;
       End ;

     Getting the objects out of the stream is a little trickier.  Your
     parent object may need to overload the GetChildOwner and
     GetChildParent functions.  Otherwise Delphi will try to make the child
     owned by the form. (In Delphi V1 you need to override the Readstate
     method.)
     ----------------------------------------- END EXTRACT FROM F.A.Q.

The actual details of the above recipe and code are missing and perhaps someone could fill   them in for my situation, using the TnodeInfo class referred to in this thread?




Avatar of abulka

ASKER

Adjusted points to 250
Phew Guys! You write a lot... ;)
I didn't even have the ability to read so much so my answer may be here in the list (sorry).
Things to do:
1. Create a derivant of TTreeNode with brand new virtual methods to save and load its own datas. (as it can point to a data flow by the Data property, it should contain such "TNewNode"s in its sublevels to make us sure that they'll be stored or You'll have to load/save them Yourself for every bit of nodes...)
2. "Override" (what the hell I'm writing? :)) the virtuals for each node just like this:
   {TnodeInfo = class(tobject)
        name, address: string;
        flag1: boolean;
        lots: TstringList;}
   
procedure TNewNode.Save(ToStream: TStream);
var
  Index: Integer;
begin
  // pseudo-code follows...
  Save(Name,Address,Flag1); {Simply use ToStream's Write()/WriteBuffer() for Output}
  for Index:=1 to Lots.Count do
    Save(Lots[Index]; {ditto...}
end;

and the Load proc/func should also be stg like this (not to mention: vice versa)

I hope I could help You...
Avatar of abulka

ASKER

Thanks for replying.  I'm not sure I fully understand everything you've said.

>   1. Create a derivant of TTreeNode with brand new virtual methods to save and
>   load its own datas. (as it can point to a data flow by the Data property, it should

Is it easy to trick treeview to use my nodes instead of the regular treenodes?  There was an article in Visual Developer by "Osborn, Cory" about this.  Looks a little complicated.

>   contain such "TNewNode"s in its sublevels to make us sure that they'll be stored
>   or You'll have to load/save them Yourself for every bit of nodes...)

TTreeview automatically writes out
  nodes added at design time through component streaming.  So this method
  could be used in my application - I guess.

The treeview may write out the nodes, but I guess it can't write them out as objects with published properties - cos TTreenodes DON'T HAVE published properties.  This might explain why when I experiment around with writeComponentRes(treeview1) and examine the output as text, then I see the properties of the treeview OK, but the items appear as a number of lines of binary-ish stuff. e.g.

  object TreeView1: TTreeView
    Left = 40
    Top = 160
    Width = 121
    Height = 113
    Indent = 19
    Items.Data = {
      050000001D0000000000000000000000FFFFFFFFFFFFFFFF0000000000000000
      04416E64792E0000000000000000000000FFFFFFFFFFFFFFFF00000000000000
      0015486F772061726520796F75206F6E206C696E6520332C0000000000000000
      000000FFFFFFFFFFFFFFFF000000000300000013616E6420616761696E206174
      206C696E652034250000000000000000000000FFFFFFFFFFFFFFFF0000000000
      0000000C666972737420696E64656E74290000000000000000000000FFFFFFFF
      FFFFFFFF0000000000000000107374696C6C206174206C6576656C2031290000
      000000000000000000FFFFFFFFFFFFFFFF0000000000000000107374696C6C20
      6174206C6576656C2031200000000000000000000000FFFFFFFFFFFFFFFF0000
      00000000000007497320686572651B0000000000000000000000FFFFFFFFFFFF
      FFFF0000000000000000026869}
    TabOrder = 5
  end

So the items (nodes) are getting written out using some other mechanism than Twriter writing out published properties.  I wonder the actual call sequence is, that causes the above to be written out?

I want to understand the call sequence so that I can understand how to 'hook into it'.

From browsing the VCL, it seems that Twriter.writedata is the heart of the recursive writing out of published properties, and that it calls getChildren quite a lot.  Many components override the getChildren method.  But then again, I cannot see a TreeView or customTreeView  .getChildren method?  Perhaps I should create a new class inheriting from treeview and implement a  .getChildren method?


I also got some similar advice to yours: viz. "but you may want to  override the component streaming methods to write out any new data added to   your own custom nodes."

How do I override the component streaming methods?

Do I do it at the level of treeview or node or nodes?
Do I create my own methods or override existing ones?
How do I hook into the system so my routines get called during a writecomponent call of the treeview?
Do I need to deal with GetChildren or getChildOwner or SetParentComponent?
Who calls what, when?  Exactly when do I write out my 'personal treenode .data object'?
Should I be using writeComponent or writeComponentRes.
How do I read all this stuff back again into a treeview?

I can't believe all this stuff is so convoluted and hard.  After all, ALL I WANT TO DO is have my own object living in each treenode (via the .data pointer), and save and load these babies to a file, using the standard VCL Twriter notation, if at all possible.

I understand that explaining all this stuff to me is probably lots of hard work, so I understand if you don't want to go into any detail - if any on all of this.  I'll take any scraps I can get.  I figure that if I can finally understand and implement this, I will understand much of the big picture of how Delphi & the VCL operates.

Re: Your example
> procedure TNewNode.Save(ToStream: TStream);
Is the .save an overridden method?  How does save get called, by whom?
Is there any way to save using twriter, so that the standard twriter notation gets used?  

Cheers from down under,
-Andy


Hi abulka,

well, here's a description of how TTreeView does its streaming:

While streaming of a TPersistent all published properties with write access are written out to the stream. Additionally the component is asked about own definitions of properties to be streamed out. This happens by calling the component's DefineProperties method. In TTreeView the "Items" property is a class of TTreeNodes. Since "Items" is a TPersistent class it gets streamed out/in as well. TTreeNodes does not contain any published property but defines instead a virtual property and its associated read and write methods:

procedure TTreeNodes.DefineProperties(Filer: TFiler);

  function WriteNodes: Boolean;
  var
    I: Integer;
    Nodes: TTreeNodes;
  begin
    Nodes := TTreeNodes(Filer.Ancestor);
    if Nodes = nil then
      Result := Count > 0
    else if Nodes.Count <> Count then
      Result := True
    else
    begin
      Result := False;
      for I := 0 to Count - 1 do
      begin
        Result := not Item[I].IsEqual(Nodes[I]);
        if Result then Break;
      end
    end;
  end;

begin
  inherited DefineProperties(Filer);
  Filer.DefineBinaryProperty('Data', ReadData, WriteData, WriteNodes);
end;

Note: The 'Data' property does not really exists. It's just an identifier to name the data to be streamed out and in. DefineProperties is a virtual method defined in TPersistent and is called by TFiler after all published properties have been streamed.

The call to Filer.DefineBinaryProperty will in turn call either ReadData or WriteData (depending on direction of streaming).

So what happens is:

// stream out
- create a ressource stream while saving a project in the IDE
  - call TSream.WriteComponent for each form and data module (this indicates that the root must be at least a TComponent, not a TPersistent), TStream.WriteComponent calls in turn TStream.WriteDescendent
    - TStream.WriteDescendent creates a TWriter and call its WriteDescendent method
      - TWriter.WriteDescendent then writes a signature and calls TWriter.WriteComponent
      - WriteComponent calls in turn the Component's SaveState method (Component at this level is still a form or datamodule)
      - SaveState calls the TWriter's WriteData method
        - TWriter.WriteData does now the real work, it calls (beside other methods) its WriteProperties method
          - for each published property do
            - write it to the stream, if the property is a class then call its WriteProperties recursively
            - call the components DefineProperties method
    - free TWriter
- free ressource stream

Stream-in works similar.

As you can see here DefineProperties gets called after all other properties of a class have been written. To come back now to TTreeView (more precisely TTreeNodes): As written above TTreeNodes.WriteData method calls for each root level node TTreeNode.WriteData, which writes a record of information to the stream and calls in turn for each of its children WriteData.

Unfortunately neither TTreeNodes.WriteData nor TTreeNode.WriteData are virtual, which means you cannot override them to save your own data. Now we are at a point where you should know why you cannot use the default streaming with the default classes.

In an earlier comment I wrote that I have created my own classes (TTreeNT etc.) which use the default streaming mechanism to write their data out and I strongly believe that you have to do the same to write what you want.

What I did was to make the WriteData method of my tree nodes virtual. This essentially enables each descendent of TTreeNTNode to define its own WriteData method. With the supplied TStream class one can even write out components attached to the data (or every other own) property.

To sum up all said:

In any case, you have to create your own classes to override certain methods. You can use the GetChildren method if you really want and use the default streaming, but this requires that your classes attached to each node are of type TComponent. This also would require that TTreeNodes promotes its Items property to published. This would involve many additional steps, hence is not recommendable (believe me, I have done this in another project already). It would also produce an overhead not worth the work you want to do...

What I can recommend, though, is that you use my TTreeNT (which is entirely compatible with TTreeView and freeware, apart from many really cool additional features) and create your own node class derived from TTreeNTNode. All you have to do then is to override TTreeNTNode.WriteData and write anything you want (including your attached components). You can therein even implement a simple version control mechanism to keep compatible with further versions of your control.

If you still are not convinced then keep in mind that all other solutions presented here do save and load the tree content to an extra file rather than in the ressource. Dr_Gabe's solution is very similar to mine, but does not work since TTreeNode.WriteData/ReadData methods are not virtual as already pointed out.

I hope my answer will finally clear your confusion and will lead you to a final decision.

Ciao, Mike
Avatar of abulka

ASKER

Thanks Lischke / Mike for your extensive reply.

>     well, here's a description of how TTreeView does its streaming:

Here is some more info I have since understood.

TreeView does not have any children components, instead, it has a TTreeNodes object which
houses many TtreeNode objects.  Neither TTreeNodes or TtreeNode is a component

Treeview streams out its nodes by having the TtreeNodes class override DefineProperties() and has its saveData loop through the items and stream the nodes out!  This output is in binary format and not the nice Twriter style output.


>      begin
>         inherited DefineProperties(Filer);
>         Filer.DefineBinaryProperty('Data', ReadData, WriteData, WriteNodes);
>       end;

>     Note: The 'Data' property does not really exists. It's just an identifier to name the
>     data to be streamed out and in. DefineProperties is a virtual method defined in
>     TPersistent and is called by TFiler after all published properties have been
>     streamed.

To be slightly more precise, Defineproperties is called by the Twriter (passing the TFiler as a parameter) (Danny Thorpe p 166)

>           - SaveState calls the TWriter's WriteData method
>             - TWriter.WriteData does now the real work, it calls (beside other methods) its
>                                                                WriteProperties method
>               - for each published property do
>                 - write it to the stream, if the property is a class then call its WriteProperties
>                                                                  recursively
>                 - call the components DefineProperties method
>         - free TWriter
>     - free ressource stream

I think there is also a call to getChildren() after the call to DefineProperties, above.

>     To sum up all said:
>
>     In any case, you have to create your own classes to override certain methods. You
>     can use the GetChildren method if you really want and use the default streaming,
>     but this requires that your classes attached to each node are of type TComponent.
That's ok by me.

>     This also would require that TTreeNodes promotes its Items property to published.
>     This would involve many additional steps, hence is not recommendable (believe
I don't think this is right.  First of all, TTreeNodes is not a component and so will not have getChildren() called on it.  If I want to override getChildren(), I can do it for the treeview component instead.  This will work.

So basically, I think a solution using getChildren() can work.  I just need to derive from the regular treeview, and just override getchildren(), where I loop and proc() out each additional custom info component associated with my treeview.  The only problem is associating each custom info component with the correct .data Ttreenode when it all gets read back in.  I do this 'custom fixup' in the treeview's .loaded method.

Here is the code which worked for me:

// --------------------------------------- SOLUTION

TTreeViewAndy = class(TTreeView)
  protected
      procedure GetChildren(Proc: TGetChildProc; Root: TComponent); override;
      procedure loaded; override;
end;

// This is the object associated with each TTreeNode .data
TnodeInfoAndy = class(TComponent)
    private
       Fblah1: string;
       procedure Setblah1(const Value: string);
       procedure SetgcId(const Value: string);
       function ReadgcId: string;
    published
       property blah1: string read Fblah1 write Setblah1;
       property gcId: string read ReadgcId write SetgcId;
end;

procedure TnodeInfoAndy.SetgcId(const Value: string); begin   name := Value;  end;
function TnodeInfoAndy.ReadgcId: string;  begin     result := name;  end;
procedure TnodeInfoAndy.Setblah1(const Value: string);  begin   Fblah1 := Value; end;

procedure TTreeViewAndy.GetChildren(Proc: TGetChildProc;
  Root: TComponent);
var
  i: integer;
  obj: TnodeInfoAndy;
begin
    // Pretend the associated .data objects are 'children' of the treeview.
    // Remember to record the item # each nodeinfoAndy object is associated with
    // so we can later re-associate this object with the correct ttreenode .data
     for i := 0 to items.count-1 do
     begin
         obj := items[i].data;
         obj.gcId := 'item'+intToStr(i);
         Proc(obj);
     end;
end;

procedure TTreeViewAndy.loaded;
var
   i: integer;
   foundComponent: TComponent;
begin
    // Re-associate each new, streamed in TnodeInfoAndy object with
    // the correct ttreenode.  Set each ttreenode .data accordingly.
    for I := 0 to items.Count-1 do
    begin
      foundComponent := nil;
      foundComponent := FindComponent('item'+intToStr(i));
      items[i].data :=  foundComponent; // too bad if it's null
    end;
    inherited loaded;
    // ALL DONE, ttreenodes and TnodeInfoAndy are back in synch.
end;

// --------------------------------- END OF BASIC SOLUTION

// Auxiliary routines

procedure TTreeViewAndy.clearnodes;
var
  i: integer;
begin
     for i := items.count-1 downto 0 do
         items[i].Delete;

end;

procedure TTreeViewAndy.clearAndyInfoNodes;
var
  i: integer;
begin
     // loop through all TnodeInfoAndy's owned by tree
     // and deletecomponent it.
     for i := ComponentCount - 1 downto 0 do   // items.count-1 do
     begin
         if components[i].ClassName = 'TnodeInfoAndy' then begin
            RemoveComponent(components[i]);
         end;
     end;
end;


procedure TTreeViewAndy.LoadFromFileAndy(const FileName: string;
  treeObj: TTreeViewAndy);
var
   it : TComponent;
begin
     treeObj.clearAndyInfoNodes;
     RegisterClass(TnodeInfoAndy);
     it := ReadComponentResFile( FileName , treeObj );
end;

procedure TTreeViewAndy.SaveToFileAndy(const FileName: string;
  treeObj: TTreeViewAndy);
begin
     WriteComponentResFile( FileName, treeObj as TComponent);
end;

--------------------------

The getchildren() solution above works.

I guess an alternative solution would have been to override DefineProperties for the treeview, and sream out components within the readdata/writedata of the defineproperty.  But writing out components inside a defineproperty is probably breaking all the rules of Twriter streaming, so this is not a good solution.  Plus I tried it, and it raises an exception.

Another solution - overriding DefineProperties for the Ttreenodes  or
overriding DefineProperties for the Ttreenode class probably won't work for the reasons you point out.  Though I'm not sure both these alternatives have been considered...  In any case, define properties is probably best left to non-component data streaming, me thinks...

>     I hope my answer will finally clear your confusion and will lead you to a final
>     decision.

With your help + quite a few others, I am proud that I created the above solution, and I understand streaming a whole lot better now.  I would be interested in your comments on my solution!

cheers,
-Andy

Avatar of abulka

ASKER

Cannot override TTreeNode.WriteData/ReadData - the methods are not virtual.


Avatar of abulka

ASKER

Hey Lischke / Mike - you haven't replied to my final comment(s)?   The points are yours if you do.
ASKER CERTIFIED SOLUTION
Avatar of Lischke
Lischke

Link to home
membership
This solution is only available to members.
To access this solution, you must be a member of Experts Exchange.
Start Free Trial