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...
I know that the treeview may support streaming (being a component) but my nodeinfo objects need to go along for the ride...
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
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
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.
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.
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?
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
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(Sende r: TObject);
Var
ANode : TTreeNode;
MyFile : TextFile;
I : Integer;
begin
If savedialog1.Execute then
begin
AssignFile(MyFile,savedial og1.FileNa me);
rewrite(MyFile);
ANode := Tv1.TopItem;
while Anode <> Nil do
begin
Writeln(MyFile,IntToStr(AN ode.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(Sende r: 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,Opendial og1.FileNa me);
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.LoadTreeFromS tream}
ALevel := StrToInt(copy(NodeId,1,pos ('{',NodeI d)-1));
delete(NodeId,1,pos('{',No deId));
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(NextN ode.Parent , NodeId, MyNodeInfo);
end;
end;
CloseFile(MyFile);
end;
end;
It works for me.
Maybe there will better solutions from other experts.
meikl
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(Sende
Var
ANode : TTreeNode;
MyFile : TextFile;
I : Integer;
begin
If savedialog1.Execute then
begin
AssignFile(MyFile,savedial
rewrite(MyFile);
ANode := Tv1.TopItem;
while Anode <> Nil do
begin
Writeln(MyFile,IntToStr(AN
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[
Writeln(MyFile,' '+'}');
end;
Writeln(MyFile,'}');
ANode := ANode.GetNext;
end;
CloseFile(MyFile);
end;
end;
procedure TForm1.LoadTreeClick(Sende
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,Opendial
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.LoadTreeFromS
ALevel := StrToInt(copy(NodeId,1,pos
delete(NodeId,1,pos('{',No
if ANode = nil then
ANode := Owner.AddChildObject(nil, NodeId, MyNodeInfo)
else
if ANode.Level = ALevel then
ANode := Owner.AddChildObject(ANode
else
if ANode.Level = (ALevel - 1) then
ANode := Owner.AddChildObject(ANode
else
if ANode.Level > ALevel then
begin
NextNode := ANode.Parent;
while NextNode.Level > ALevel do
NextNode := NextNode.Parent;
ANode := Owner.AddChildObject(NextN
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].Dat a);
Stream.Write(Data,SizeOf(D ata));
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.Sor tType;
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(L evel));
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.AddChildOb ject(LastN ode, 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:=Image Index;
LastNode.SelectedIndex:=Im ageIndex;
end;
end;
Treeview1.SortType:=OldSor tType;
end;
I hope that's what you are looking for.
regards, ptm.
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:=Items[i].ImageIndex
Stream.Write(Level,SizeOf(
Data:=Integer(Items[i].Dat
Stream.Write(Data,SizeOf(D
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.Sor
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(L
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(
if Level>OldLevel then
LastNode:=Items.AddChildOb
if Level<OldLevel then
begin
while Level<OldLevel do
begin
LastNode:=LastNode.Parent;
OldLevel:=LastNode.Level;
end;
LastNode:=Items.AddObject(
end;
OldLevel:=Level;
LastNode.ImageIndex:=Image
LastNode.SelectedIndex:=Im
end;
end;
Treeview1.SortType:=OldSor
end;
I hope that's what you are looking for.
regards, ptm.
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)?
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) ;
-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.
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].Da ta).saveit (stream);
end
stream.free
in pseudo code the loadtree procedure:
stream:=tfilestream.create (filename)
numberofitems := strtoint(loadstringfromstr eam)
while not endoffile do
begin
loadstringfromstream(strea m, astring) // check leading tabs to recover the level
addeditem:=AddtoTreeview(a string,lev el) // use add or addchild etc. according to the level
TnodeInfo(addeditem.Data). loadit(str eam);
end
stream.free
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(
begin
inherited;
// put code here to load additional info from stream
end;
procedure TnodeAditionalInfo.saveit(
begin
inherited;
// put code here to save additional info to stream
end;
--------------------------
in pseudo code the savetree procedure:
stream:=tfilestream.create
for i:=0 to treeview1.items.count-1 do
begin
savestringtostream(stream,
TnodeInfo(TreeView1.Items.
end
stream.free
in pseudo code the loadtree procedure:
stream:=tfilestream.create
numberofitems := strtoint(loadstringfromstr
while not endoffile do
begin
loadstringfromstream(strea
addeditem:=AddtoTreeview(a
TnodeInfo(addeditem.Data).
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
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
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.co mponents.w riting)
-------------------------- ---------- ---------- ---------- ---------- ------
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!
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
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.co
--------------------------
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.)
--------------------------
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.
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.
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?
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.)
--------------------------
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?
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...
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...
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(treeview 1) 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 = {
050000001D0000000000000000 000000FFFF FFFFFFFFFF FF00000000 00000000
04416E64792E00000000000000 00000000FF FFFFFFFFFF FFFF000000 00000000
0015486F772061726520796F75 206F6E206C 696E652033 2C00000000 00000000
000000FFFFFFFFFFFFFFFF0000 0000030000 0013616E64 2061676169 6E206174
206C696E652034250000000000 0000000000 00FFFFFFFF FFFFFFFF00 00000000
0000000C666972737420696E64 656E742900 0000000000 0000000000 FFFFFFFF
FFFFFFFF000000000000000010 7374696C6C 206174206C 6576656C20 31290000
000000000000000000FFFFFFFF FFFFFFFF00 0000000000 0000107374 696C6C20
6174206C6576656C2031200000 0000000000 00000000FF FFFFFFFFFF FFFF0000
00000000000007497320686572 651B000000 0000000000 000000FFFF FFFFFFFF
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
> 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(treeview
object TreeView1: TTreeView
Left = 40
Top = 160
Width = 121
Height = 113
Indent = 19
Items.Data = {
050000001D0000000000000000
04416E64792E00000000000000
0015486F772061726520796F75
000000FFFFFFFFFFFFFFFF0000
206C696E652034250000000000
0000000C666972737420696E64
FFFFFFFF000000000000000010
000000000000000000FFFFFFFF
6174206C6576656C2031200000
00000000000007497320686572
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.DefinePropertie s(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/ReadDa ta 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
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.DefinePropertie
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
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
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/ReadDa
I hope my answer will finally clear your confusion and will lead you to a final decision.
Ciao, Mike
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(cons t Value: string); begin name := Value; end;
function TnodeInfoAndy.ReadgcId: string; begin result := name; end;
procedure TnodeInfoAndy.Setblah1(con st 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'+intTo Str(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.clearAndyInf oNodes;
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.LoadFromFile Andy(const FileName: string;
treeObj: TTreeViewAndy);
var
it : TComponent;
begin
treeObj.clearAndyInfoNodes ;
RegisterClass(TnodeInfoAnd y);
it := ReadComponentResFile( FileName , treeObj );
end;
procedure TTreeViewAndy.SaveToFileAn dy(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
> 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
> 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:
// --------------------------
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(cons
function TnodeInfoAndy.ReadgcId: string; begin result := name; end;
procedure TnodeInfoAndy.Setblah1(con
procedure TTreeViewAndy.GetChildren(
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'+intTo
items[i].data := foundComponent; // too bad if it's null
end;
inherited loaded;
// ALL DONE, ttreenodes and TnodeInfoAndy are back in synch.
end;
// --------------------------
// Auxiliary routines
procedure TTreeViewAndy.clearnodes;
var
i: integer;
begin
for i := items.count-1 downto 0 do
items[i].Delete;
end;
procedure TTreeViewAndy.clearAndyInf
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
end;
end;
end;
procedure TTreeViewAndy.LoadFromFile
treeObj: TTreeViewAndy);
var
it : TComponent;
begin
treeObj.clearAndyInfoNodes
RegisterClass(TnodeInfoAnd
it := ReadComponentResFile( FileName , treeObj );
end;
procedure TTreeViewAndy.SaveToFileAn
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
ASKER
Cannot override TTreeNode.WriteData/ReadDa ta - the methods are not virtual.
ASKER
Hey Lischke / Mike - you haven't replied to my final comment(s)? The points are yours if you do.
ASKER CERTIFIED SOLUTION
membership
This solution is only available to members.
To access this solution, you must be a member of Experts Exchange.
Particulary look at TTreeStrings.LoadTreeFromS
Note that saved treeviews are files with strings with one or more leading tabs if the item is a child