958 lines
30 KiB
TeX
958 lines
30 KiB
TeX
@@GettingStartedE.html
|
|
<TITLE Virtual Treeview step by step>
|
|
|
|
|
|
<PRE>
|
|
Written by Sven H. (<EXTLINK mailto:h.sven@gmx.at>h.sven@gmx.at</EXTLINK>), Revision and translation by Mike Lischke (<EXTLINK mailto:public@delphi-gems.com>public@delphi-gems.com</EXTLINK>)
|
|
</PRE>
|
|
|
|
|
|
At the time when this description was created I had not much Delphi knowledge and had not yet read through any of my two
|
|
Delphi books. But I was quite impatient and wanted to try out what is possible. Although I have some knowledge about
|
|
\object oriented programming and C++ (I have learned something about it during my studies), this project was my first
|
|
attempt to program in Delphi. It could be that I have not provided the most elegant solutions und I am always open for
|
|
improvement suggestions. But all principles I demonstrated here do work (at least for me J). I have implemented them in
|
|
my first project this way. This guidance is made in the first place for programmers who are not yet familiar with Virtual
|
|
Treeview and will so perhaps have an easier start. If you have questions or suggestions regarding this guidance please
|
|
forward them to <EXTLINK mailto:h.sven@gmx.at>h.sven@gmx.at</EXTLINK>. For other questions you can contact Mike and use
|
|
the dedicated newsgroup, respectively.
|
|
|
|
|
|
|
|
I am neither a Virtual Treeview nor a Delphi expert and have collected all the answers (with the help of Mike) with quite
|
|
some effort. In order to avoid the afterwards relatively simple things to become problematic I have written this short
|
|
guidance. The real problems will appear later.
|
|
|
|
© 2001 The parts in this guidance beyond the text from the online help are copyrighted. Every publication requires my
|
|
admission.
|
|
|
|
Have fun with it, Sven.
|
|
|
|
|
|
|
|
* Preparations *
|
|
Before we start some preparations are necessary:
|
|
|
|
* Place a Virtual Treeview component on a form.
|
|
* Change the properties as you like.
|
|
* A record for node data must be defined.
|
|
|
|
|
|
|
|
In order to store the own node data some musing is important. How shall the record look like?
|
|
|
|
<B>a) All nodes in the tree are equal</B>
|
|
|
|
In this case a simple record defines the necessary data structure, e.g.:
|
|
|
|
|
|
<CODE>
|
|
<B>type</B>
|
|
rTreeData = <B>record</B>
|
|
Text: WideString;
|
|
URL: <B>string</B>[<COLOR Pink>255</COLOR>];
|
|
CRC: LongInt;
|
|
isOpened: Boolean;
|
|
ImageIndex: Integer;
|
|
<B>end</B>;
|
|
</CODE>
|
|
|
|
|
|
<B>b) There are different nodes in the tree (e.g. folders that can have sub nodes)</B>
|
|
|
|
I will follow this case because my tree will hold folders, which can in turn get own nodes. Since I intent to store
|
|
created trees in a file in order to restore them later further deliberations are necessary: Suppose a folder node has
|
|
\only a name and a leaf node has a name and a text info field. Potentially, I also want to store a second kind of leaf
|
|
node, which will for instance have a number instead of the text field. The problem in the context of reading data form a
|
|
stream is that I must know which data is stored in which order in the stream, because I have to read it in exactly the
|
|
same order again. Hence I have to determine from the very first information in the stream what information will follow.
|
|
For instance there is a node name, but then? Is there nothing more or another text information (string) or even an
|
|
integer value? I think the point is clear. The first data, which I read, has to carry this information.
|
|
|
|
|
|
|
|
These deliberations have leaded me to the following solution: I save now in the stream [label]-\>[name]-\>[following
|
|
data]
|
|
|
|
|
|
|
|
0 -\> 'Folder'
|
|
|
|
1 -\> 'Info node' -\> 'Blabla'
|
|
|
|
2 -\> 'Number node' -\> 123
|
|
|
|
|
|
|
|
I know from the stream I always read an integer value first. Depending whether this is 0, 1 or 2 I have to read - now
|
|
known - following values. Now let us consider the record.
|
|
|
|
|
|
<CODE>
|
|
<B>type</B>
|
|
rTreeData = <B>record</B>
|
|
Typ: Integer;
|
|
Name: <B>string</B>[<COLOR Pink>255</COLOR>];
|
|
pNodeData: Pointer;
|
|
<B>end</B>;
|
|
</CODE>
|
|
|
|
|
|
Hey, there is suddenly a pointer in the record. Well, here are some additional comments:
|
|
|
|
1. Typ is an integer value, from which I can determine what kind of node this is, in my example 1, 2 or 3.
|
|
2. Name is the name of the node. This will be needed relatively often because it is also seen as part of the tree
|
|
and I want to access this information easily (man, I am lazy).
|
|
3. The pointer allows (similar to the data property of the tree) a record or even better a class instance to
|
|
connect.
|
|
|
|
|
|
|
|
Now I still have the freedom to define a base class of node. It contains all properties and methods, which all classes
|
|
will share. And from this I can derive proper sub classes (e.g. text nodes, value nodes etc.). An additional advantage of
|
|
this record is its fixed size. Hence you can always return the same size in case the tree asks for it (see also property
|
|
NodeDataSize), but more about that later.
|
|
|
|
Just one remark: If you don't want to use classes you can also simply define 3 records, which define as first element, a
|
|
type and which react differently depending on this type.
|
|
|
|
|
|
|
|
<B><U>Alternative solution:</U></B>
|
|
|
|
Okay, I admit it. It would of course also be possible to write the type into the stream and read it from the stream
|
|
separately without saving it as part of the record. The type of the node class is indirectly known because you can ask a
|
|
class which class name it has (see e.g. class function ClassName) and the class knows it too. So I shall store a node,
|
|
\okay. I pass on the stream to the Node.SaveToFile(Stream) method, which writes, depending on which node class we
|
|
actually have, automatically the value 1, 2 or 3 into the stream.
|
|
|
|
|
|
|
|
During load from stream I read first the value 1, 2 or 3 and decide what class is meant. Then I create an instance of
|
|
this class and call its LoadFromFile method. Well, this solution is my most preferred and before another one enters my
|
|
brain I will implement it (Note: in step 5 I will change something).
|
|
|
|
|
|
|
|
<U>So I do following:</U>
|
|
|
|
|
|
|
|
As you can see from the declaration of the internal node of Virtual Tree
|
|
|
|
|
|
<CODE>
|
|
TVirtualNode = <B>packed</B> <B>record</B>
|
|
Index, <COLOR Blue>// index of node with regard to its parent</COLOR>
|
|
ChildCount: Cardinal; <COLOR Blue>// number of child nodes</COLOR>
|
|
...
|
|
...
|
|
LastChild: PVirtualNode; <COLOR Blue>// link to the node's last child...</COLOR>
|
|
Data: <B>record</B> <B>end</B>; <COLOR Blue>// this is a placeholder, each node gets extra</COLOR>
|
|
<COLOR Blue>// data determined by NodeDataSize</COLOR>
|
|
<B>end</B>;
|
|
</CODE>
|
|
|
|
|
|
there is another record at the end of the record structure. Which exact structure this is will be determined indirectly.
|
|
|
|
|
|
<CODE>
|
|
<B>type</B>
|
|
rTreeData = <B>record</B>
|
|
Name: <B>string</B>[<COLOR Pink>255</COLOR>]; <COLOR Blue>// the identifier of the node</COLOR>
|
|
ImageIndex: Integer; <COLOR Blue>// the image index of the node</COLOR>
|
|
pNodeData: Pointer;
|
|
<B> end</B>;
|
|
</CODE>
|
|
|
|
|
|
Let the above record be the structure. The Virtual Treeview does not really know this structure, but it knows how much
|
|
space must be reserved. We tell it by
|
|
|
|
|
|
<CODE>
|
|
myVirtualTree.NodeDataSize := SizeOf(rTreeData);
|
|
</CODE>
|
|
|
|
|
|
\Note, even if you want to store only one value, e.g. a pointer as node data, simply return the size, which should be
|
|
reserved.
|
|
|
|
|
|
|
|
<B>Implementation</B>
|
|
|
|
<I>An empty tree</I>
|
|
|
|
|
|
|
|
I begin with an empty tree (no top level nodes are created at design time):
|
|
|
|
* Either an existing tree is read from a file or
|
|
* A top-level node is created.
|
|
|
|
* *
|
|
Before a node can be created you have to determine the size of the actual node data. According to the docs there are
|
|
three opportunities:
|
|
|
|
* In the object inspector
|
|
* In the OnGetNodeDataSize - event or
|
|
* During creation of the form
|
|
|
|
|
|
|
|
I decide to use the last variant and will now do the following during form creation:
|
|
|
|
|
|
<CODE>
|
|
<B>procedure</B> TMyForm.FormCreate(Sender: TObject);
|
|
|
|
<B>var</B>
|
|
Node: PVirtualNode;
|
|
|
|
<B>begin</B>
|
|
...
|
|
<COLOR Blue>// create tree</COLOR>
|
|
MyTree.NodeDataSize := SizeOf(TTreeData);
|
|
<B>if</B> MyForm.filename = <COLOR Pink>''</COLOR> <B>then</B> <B>begin</B> <COLOR Blue>// if there is no tree to load</COLOR>
|
|
<COLOR Blue>// create tree with top level node</COLOR>
|
|
Node := BookmarkForm.BookmarkTree.AddChild(nil); <COLOR Blue>// adds a top level node</COLOR>
|
|
<B> end
|
|
else
|
|
begin</B>
|
|
<COLOR Blue>// load tree</COLOR>
|
|
....
|
|
<B> end</B>;
|
|
....
|
|
<B>end</B>;
|
|
</CODE>
|
|
|
|
|
|
<B><I>Data for the node</I></B>
|
|
|
|
|
|
|
|
After the call of AddChild data can be assigned. For this a pointer to the self-defined record will be declared and via
|
|
the function GetNodeData connected with the correct address. By using this pointer we can now access the elements of the
|
|
record and assign them values.
|
|
|
|
|
|
<CODE>
|
|
<B>var</B>
|
|
...
|
|
NodeData: ^rTreeData;
|
|
|
|
<B>begin</B>
|
|
...
|
|
<COLOR Blue>// determine data for node</COLOR>
|
|
NodeData := BookmarkForm.BookmarkTree.GetNodeData(Node);
|
|
NodeData.Name := <COLOR Pink>'new project'</COLOR>;
|
|
NodeData.ImageIndex := <COLOR Pink>0</COLOR>;
|
|
...
|
|
</CODE>
|
|
|
|
|
|
<B><I>Show the node name</I></B>
|
|
|
|
|
|
|
|
The name of the node shall now appear as node identification in the tree. All data about the node as well as the name are
|
|
unknown to the treeview and it has to query for them.
|
|
|
|
|
|
|
|
Every time the identification of the node is needed an event OnGetText will be triggered. In the event handler we return
|
|
the name of the node in the variable Text. Nothing more is needed.
|
|
|
|
|
|
<CODE>
|
|
<B>procedure</B> TBookmarkForm.BookmarkTreeGetText(Sender: TBaseVirtualTree;
|
|
Node: PVirtualNode; Column: Integer; TextType: TVSTTextType; <B>var</B> Text: WideString);
|
|
<B>
|
|
var</B>
|
|
NodeData: ^rTreeData;
|
|
|
|
<B>begin</B>
|
|
NodeData := Sender.GetNodeData(Node);
|
|
<COLOR Blue>// return identifier of the node</COLOR>
|
|
Text := NodeData.Name;
|
|
<B>end</B>;
|
|
</CODE>
|
|
|
|
|
|
<B><I>The icon for the node</I></B>
|
|
|
|
|
|
|
|
Because I like it colorful I want also to provide an icon for the top-level node. Following steps are necessary to
|
|
accomplish that:
|
|
|
|
|
|
|
|
* A TImageList must be placed onto the form and filled with images
|
|
* The property Images of the VirtualTreeview gets assigned this image list
|
|
* Implement an OnGetImageIndex event handler.
|
|
|
|
|
|
|
|
In the event OnGetImageIndex you can the index be determine which determines in turn which image form the list must be
|
|
shown.
|
|
|
|
|
|
|
|
Because the method is also called for the state icons but I do not want yet to state icons (but I already have assigned
|
|
and image list to the property StateImages) the value for this case (Kind à ikState) is -1.
|
|
|
|
|
|
<CODE>
|
|
<B>procedure</B> TBookmarkForm.BookmarkTreeGetImageIndex(Sender: TBaseVirtualTree;
|
|
Node: PVirtualNode; Kind: TVTImageKind; Column: Integer; <B>var</B> Index: Integer);
|
|
|
|
<B>var</B>
|
|
NodeData: ^rTreeData;
|
|
|
|
<B>begin</B>
|
|
NodeData := Sender.GetNodeData(Node);
|
|
<B> case</B> Kind <B>of</B>
|
|
ikState: <COLOR Blue>// for the case the state icon has been requested</COLOR>
|
|
Index := <COLOR Pink>-1</COLOR>;
|
|
ikNormal, ikSelected: <COLOR Blue>// normal or the selected icon is required</COLOR>
|
|
Index := NodeData.ImageIndex;
|
|
<B> end</B>;
|
|
<B>end</B>;
|
|
</CODE>
|
|
|
|
|
|
Depending on whether a node is selected or not, different icons shall be shown (see step 6).
|
|
|
|
<B><I>
|
|
|
|
Only one node class in the record</I></B>
|
|
|
|
|
|
|
|
Since I want to avoid mixing data in the record and later then data in the node class I decided to change this record
|
|
|
|
|
|
<CODE>
|
|
<B>type</B>
|
|
TTreeData = <B>record</B>
|
|
Name: <B>string</B>[<COLOR Pink>255</COLOR>]; <COLOR Blue>// the identifier of the node</COLOR>
|
|
ImageIndex: Integer; <COLOR Blue>// the image index of the node</COLOR>
|
|
pNodeData: Pointer;
|
|
<B>end</B>;
|
|
</CODE>
|
|
|
|
|
|
into a record which contains only one pointer to a node class. I declare therefore first a node class
|
|
|
|
|
|
<CODE>
|
|
TBasicNodeData = <B>class</B>
|
|
...
|
|
<B>end</B>;
|
|
</CODE>
|
|
|
|
|
|
and then a structure of the form:
|
|
|
|
|
|
<CODE>
|
|
rTreeData = <B>record</B>
|
|
BasicND: TBasicNodeData;
|
|
<B>end</B>;
|
|
</CODE>
|
|
|
|
|
|
This record always needs 4 bytes for the pointer to the class.
|
|
|
|
|
|
|
|
Particular attention is to direct to the event OnGetText. This event will already be called during creation of the node
|
|
with Tree.AddChild(nil) in order to determine the space the new node's caption will need (but only if no columns were
|
|
created). At this point however the node class could not yet be initialised (no constructor call yet). Hence for this
|
|
case
|
|
|
|
|
|
<CODE>
|
|
<B>if</B> NodeD.BasicND = <B>nil</B> <B>then</B>
|
|
Text := <COLOR Pink>''</COLOR>
|
|
</CODE>
|
|
|
|
|
|
must be returned or you wrap the entire initialization into a BeginUpdate/EndUpdate block and initialized the nodes
|
|
before EndUpdate is called (e.g. by ValidateNode(Node)).*
|
|
|
|
|
|
|
|
Without this provision an access violation would be the result.
|
|
|
|
|
|
|
|
* *
|
|
* Example class declaration *
|
|
|
|
<CODE><B>unit</B> TreeData;
|
|
|
|
<B>interface</B>
|
|
|
|
<COLOR Blue>//===========================================</COLOR>
|
|
|
|
<B>type</B>
|
|
<COLOR Blue>// declare common node class</COLOR>
|
|
TBasicNodeData = <B>class</B>
|
|
<B>protected</B>
|
|
cName: ShortString;
|
|
cImageIndex: Integer;
|
|
<B>public</B>
|
|
<B>constructor</B> Create; <B>overload</B>;
|
|
<B>constructor</B> Create(vName: ShortString; vIIndex: Integer = 0); <B>overload</B>;
|
|
|
|
<B>property</B> Name: ShortString read cName write cName;
|
|
<B>property</B> ImageIndex: Integer read cImageIndex write cImageIndex;
|
|
<B>end</B>;
|
|
|
|
<COLOR Blue>// declare new structure for node data</COLOR>
|
|
rTreeData = <B>record</B>
|
|
BasicND: TBasicNodeData;
|
|
<B>end</B>;
|
|
|
|
<B>implementation</B>
|
|
|
|
<B>constructor</B> TBasicNodeData.Create;
|
|
<B>begin</B>
|
|
<COLOR Blue>{ not necessary
|
|
cName := '';
|
|
cImageIndex := 0;
|
|
}</COLOR>
|
|
<B>end</B>;
|
|
|
|
<B>constructor</B> TBasicNodeData.Create(vName: ShortString; vIIndex: Integer = 0);
|
|
<B>begin</B>
|
|
cName := vName;
|
|
cImageIndex := vIIndex;
|
|
<B>end</B>;
|
|
|
|
<B>end</B>.
|
|
</CODE>
|
|
|
|
|
|
* Example creation of the tree *
|
|
|
|
<CODE><COLOR Blue>// Tree will be created when the form is created.</COLOR>
|
|
<B>procedure</B> TMyForm.FormCreate(Sender: TObject);
|
|
|
|
<B>var</B>
|
|
Node: PVirtualNode;
|
|
NodeD: ^rTreeData;
|
|
|
|
<B>begin</B>
|
|
....
|
|
<COLOR Blue>// create tree</COLOR>
|
|
MyTree.NodeDataSize := SizeOf(rTreeData);
|
|
<B>if</B> MainControlForm.filename = <COLOR Pink>''</COLOR> <B>then</B>
|
|
<B>begin</B>
|
|
<COLOR Blue>// create tree with top level node</COLOR>
|
|
Node := MyTree.AddChild(<B>nil</B>); <COLOR Blue>// adds a node to the root of the tree</COLOR>
|
|
<COLOR Blue>// assign data for this node</COLOR>
|
|
NodeD := MyTree.GetNodeData(Node);
|
|
NodeD.BasicND := TBasicNodeData.Create(<COLOR Pink>'new project'</COLOR>);
|
|
<B>end</B>
|
|
<B>else</B>
|
|
<B>begin</B>
|
|
<COLOR Blue>// load tree</COLOR>
|
|
<B>end</B>;
|
|
...
|
|
<B>end</B>;
|
|
|
|
<COLOR Blue>// returns the text (the identification) of the node</COLOR>
|
|
<B>procedure</B> TMyForm.MyTreeGetText(Sender: TBaseVirtualTree; Node: PVirtualNode; Column: Integer;
|
|
TextType: TVSTTextType; <B>var</B> Text: WideString);
|
|
|
|
<B>var</B>
|
|
NodeD: ^rTreeData;
|
|
|
|
<B>begin</B>
|
|
NodeD := Sender.GetNodeData(Node);
|
|
|
|
<COLOR Blue>// return the identifier of the node</COLOR>
|
|
<B>if</B> NodeD.BasicND = <B>nil</B> <B>then</B>
|
|
Text := <COLOR Pink>''</COLOR>
|
|
<B>else</B>
|
|
Text := NodeD.BasicND.Name;
|
|
<B>end</B>;
|
|
|
|
<COLOR Blue>// returns the index for image display</COLOR>
|
|
<B>procedure</B> TMyForm.MyTreeGetImageIndex(Sender: TBaseVirtualTree;
|
|
Node: PVirtualNode; Kind: TVTImageKind; Column: Integer; <B>var</B> Index: Integer);
|
|
|
|
<B>var</B>
|
|
NodeD: ^rTreeData;
|
|
|
|
<B>begin</B>
|
|
NodeD := Sender.GetNodeData(Node);
|
|
|
|
<B>case</B> Kind <B>of</B>
|
|
ikState: <COLOR Blue>// for the case the state index has been requested</COLOR>
|
|
Index := <COLOR Pink>-1</COLOR>;
|
|
ikNormal, ikSelected: <COLOR Blue>// normal icon case</COLOR>
|
|
Index := NodeD.BasicND.ImageIndex;
|
|
<B>end</B>;
|
|
<B>end</B>;
|
|
</CODE>
|
|
|
|
|
|
* Icons for selected nodes *
|
|
If a node is selected a different symbol shall be shown. Therefore I implement a new method
|
|
|
|
|
|
<CODE>
|
|
<B>function</B> GetImageIndex(focus: Boolean): Integer; <B>virtual</B>;
|
|
</CODE>
|
|
|
|
|
|
which gets the normal image index or the index for focused nodes depending on whether the node has the focus or not.
|
|
|
|
|
|
|
|
Call:
|
|
<CODE>
|
|
Index := NodeD.BasicND.GetImageIndex(Node = Sender.FocusedNode);
|
|
</CODE>
|
|
|
|
|
|
Implementation of the method:
|
|
|
|
|
|
<CODE>
|
|
<B>function</B> TBasicNodeData.GetImageIndex(focus: Boolean): Integer;
|
|
|
|
<B>begin</B>
|
|
<B>if</B> focus <B>then</B>
|
|
\Result := cImageIndexFocus
|
|
<B>else</B>
|
|
\Result := cImageIndex;
|
|
<B>end</B>;
|
|
</CODE>
|
|
|
|
|
|
where cImageIndex has always the normal index and cImageIndex Focus the index for focused nodes. I assume in this case
|
|
that the selected index is always one more than the normal index. To ensure this, the constructor is changed this way:
|
|
|
|
|
|
<CODE>
|
|
<B>constructor</B> TBasicNodeData.Create(vName: ShortString; vIIndex: Integer = 0);
|
|
|
|
<B>begin</B>
|
|
cName := vName;
|
|
cImageIndex := vIIndex;
|
|
cImageIndexFocus := vIIndex + <COLOR Pink>1</COLOR>;
|
|
<B>end</B>;
|
|
</CODE>
|
|
|
|
|
|
* Adding and deleting nodes *
|
|
In order to implement and test more functions I want finally an opportunity to create the tree. By using a context menu
|
|
is shall be possible to add and remove nodes.
|
|
|
|
|
|
|
|
Hence I define a popup menu with two entries: [Add] and [Remove]. To have the clicked node getting the focus the option
|
|
voRightClickSelect must be set to True.
|
|
|
|
|
|
|
|
So if Add has been chosen a child node will be created for the focused node:
|
|
|
|
|
|
<CODE>
|
|
<B>procedure</B> TMyForm.addClick (Sender: TObject);
|
|
|
|
<B>var</B>
|
|
Node: PVirtualNode;
|
|
NodeD: ^rTreeData;
|
|
|
|
<B>begin</B>
|
|
<COLOR Blue>// Ok, a node must be added.</COLOR>
|
|
Node := MyTree.AddChild(MyTree.FocusedNode); <COLOR Blue>// adds a node as the last child</COLOR>
|
|
<COLOR Blue>// determine data of node</COLOR>
|
|
NodeD := MyTree.GetNodeData(Node);
|
|
NodeD.BasicND := TBasicNodeData.Create(<COLOR Pink>'Child'</COLOR>);
|
|
<B>end</B>;
|
|
</CODE>
|
|
|
|
|
|
Caution: What must be done if no node has the focus?
|
|
|
|
\-\> e.g. insert the new node as child of a top level nodes.
|
|
|
|
|
|
<CODE>
|
|
<B>if</B> BookmarkTree.FocusedNode = <B>nil</B> <B>then</B>
|
|
<B>begin</B>
|
|
<COLOR Blue>// insert as child of the first top level node</COLOR>
|
|
Node := BookmarkTree.AddChild(BookmarkTree.RootNode.FirstChild);
|
|
<COLOR Blue>// determine data for node</COLOR>
|
|
NodeD := BookmarkTree.GetNodeData(Node);
|
|
NodeD.BasicND := TFolderNodeData.Create(<COLOR Pink>'new folder'</COLOR>);
|
|
<B>end
|
|
else
|
|
begin</B>
|
|
<COLOR Blue>// Ok, a new node must be added.</COLOR>
|
|
Node := BookmarkTree.AddChild(BookmarkTree.FocusedNode);
|
|
<COLOR Blue>// determine data of the node</COLOR>
|
|
NodeD := BookmarkTree.GetNodeData(Node);
|
|
NodeD.BasicND := TFolderNodeData.Create(<COLOR Pink>'new folder'</COLOR>);
|
|
<B>end</B>;
|
|
</CODE>
|
|
|
|
|
|
|
|
|
|
If the node with the focus must be deleted the following happens:
|
|
|
|
|
|
<CODE>
|
|
<B>procedure</B> TMyForm.delClick (Sender: TObject);
|
|
|
|
<B>begin</B>
|
|
<COLOR Blue>// The focused node should be removed. The top level must not be deleted however.</COLOR>
|
|
<B>if</B> MyTree.FocusedNode = <B>nil</B> <B>then</B>
|
|
MessageDlg(<COLOR Pink>'There was no node selected.'</COLOR>, mtInformation, [mbOk], 0)
|
|
<B>else</B>
|
|
<COLOR Blue>// Note: RootNode is the internal (hidden) root node and parent of all top
|
|
// level nodes. To determine whether a node is a top level node you also use
|
|
// GetNodeLevel which returns 0 for top level nodes.</COLOR>
|
|
<B>if</B> MyTree.FocusedNode.Parent = MyTree.RootNode <B>then</B>
|
|
MessageDlg(<COLOR Pink>'The project node must not be deleted.'</COLOR>, mtInformation, [mbOk], 0)
|
|
<B>else</B>
|
|
MyTree.DeleteNode(MyTree.FocusedNode);
|
|
<B>end</B>;
|
|
</CODE>
|
|
|
|
|
|
I want to prevent, however, that the top-level node gets deleted. Hence I check with the comparison
|
|
MyTree.FocusedNode.Parent = MyTree.RootNode whether the focused node is not a top-level node. Here you have to consider
|
|
that the property RootNode returns the (hidden) internal root node, which is the common parent of all top-level nodes.
|
|
|
|
|
|
|
|
While we are at deleting nodes:
|
|
|
|
Every data of the record is automatically free as soon as this is required. In this case it is not enough, however, to
|
|
free the memory, which holds the pointer to the class (object instance), but it is also necessary to free the memory,
|
|
which is allocated by the class itself. This happens by calling the destructor of the class in the OnFreeNode event:
|
|
|
|
|
|
<CODE>
|
|
<B>procedure</B> TMyForm.MyTreeFreeNode(Sender: TBaseVirtualTree; Node: PVirtualNode);
|
|
|
|
<B>begin</B>
|
|
<COLOR Blue>// Free here the node data (Note: type PtreeData = ^rTreeData).</COLOR>
|
|
PTreeData(Sender.GetNodeData(Node)).BasicND.Free;
|
|
<B>end</B>;
|
|
</CODE>
|
|
|
|
|
|
* Adding folder and leafs *
|
|
Now I am ready to add folders to the tree as well as final nodes, which do not have children. For this I derive two new
|
|
node classes from the base class.
|
|
|
|
|
|
<CODE>
|
|
TFolderNodeData = <B>class</B>(TBasicNodeData)
|
|
TItemNodeData = <B>class</B>(TBasicNodeData)
|
|
</CODE>
|
|
|
|
|
|
Depending on which kind of node the user wants to create using the context menu I store a particular class in the node
|
|
record.
|
|
|
|
|
|
<CODE>
|
|
NodeD.BasicND := TFolderNodeData.Create(<COLOR Pink>'new folder'</COLOR>);
|
|
NodeD.BasicND := TItemNodeData.Create(<COLOR Pink>'new node'</COLOR>);
|
|
</CODE>
|
|
|
|
|
|
These classes contain a new property ChildrenAllowed. Based on this property you can now distinct whether the node with
|
|
the focus may get children (folder) or not (items).
|
|
|
|
|
|
|
|
* Storing the tree *
|
|
Now I can finally implement storing the tree. I have already thought a lot about this step. Let us see if this was
|
|
worthwhile.
|
|
|
|
|
|
|
|
Again a quote from Preparations:
|
|
|
|
I want to store a node, okay. I hand over the stream to the MyNodeClass.SaveToFile method and this method writes
|
|
depending upon which node class it actually is automatically the value 1, 2 or 3 as a kind of class ID into the stream
|
|
(alternatively you can use an enumeration type).
|
|
|
|
|
|
|
|
During load I read first the value 1, 2 or 3 from the stream and decide based on it which class we deal with. Then I
|
|
create an instance of this class and call its method LoadFromFile.
|
|
|
|
|
|
|
|
Hint:
|
|
|
|
It would also be possible to store the class name instead of the ID for the class. During read and creation of the class
|
|
\one could use class references and virtual constructors and save so the case-statement as I did in the OnLoadNode event,
|
|
to decide which class instance must be created (example see Delphi 5, written by Elmar Warken, Addison-Wesley, chapter
|
|
4.3.3, page 439).
|
|
|
|
|
|
|
|
Before you can read something it must be written first. Hence I will first implement the necessary procedures to store
|
|
the tree. Since we care ourselves that the identification of the node gets saved the option toSaveCaption can be removed
|
|
from StringOptions. This way data is not stored twice.
|
|
|
|
|
|
|
|
For saving the tree the procedure
|
|
|
|
|
|
<CODE>
|
|
<B>procedure</B> TBaseVirtualTree.SaveToFile(<B>const</B> FileName: TFileName);
|
|
</CODE>
|
|
|
|
|
|
is called. Thereby the structure of the tree is automatically stored. In order to save our additional data there is an
|
|
event OnSaveNode where we can simply store our data into the provided stream.
|
|
|
|
|
|
<CODE>
|
|
<B>property</B> OnSaveNode: TVTSaveNodeEvent <B>read</B> FOnSaveNode <B>write</B> FOnSaveNode;
|
|
</CODE>
|
|
|
|
|
|
If OnSaveNode is triggered then the method SaveNode of the particular node class will be called:
|
|
|
|
|
|
<CODE>
|
|
<B>procedure</B> TMyForm.MyTreeSaveNode(Sender: TBaseVirtualTree; Node: PVirtualNode; Stream: TStream);
|
|
|
|
<B>begin</B>
|
|
PTreeData(Sender.GetNodeData(Node)).BasicND.SaveToFile(Stream);
|
|
<B>end</B>;
|
|
</CODE>
|
|
|
|
|
|
In the SaveNode method of the class fields like node name, image index etc. are stored in the tree:
|
|
|
|
|
|
<CODE>
|
|
<B>procedure</B> TBasicNodeData.SaveNode(Stream: TStream);
|
|
|
|
<B>var</B>
|
|
size: Integer;
|
|
|
|
<B>begin</B>
|
|
<COLOR Blue>// save type of the node</COLOR>
|
|
Stream.Write(Art, SizeOf(Art));
|
|
|
|
<COLOR Blue>// store cName</COLOR>
|
|
Size := Length(cName) + <COLOR Pink>1</COLOR>; <COLOR Blue>// include terminating #0</COLOR>
|
|
Stream.Write(Size, SizeOf(Size)); <COLOR Blue>// store length of the string</COLOR>
|
|
Stream.Write(PChar(cName)^, Size); <COLOR Blue>// now the string itself</COLOR>
|
|
|
|
<COLOR Blue>// store cImageIndex</COLOR>
|
|
Stream.Write(cImageIndex, SizeOf(cImageIndex));
|
|
|
|
<COLOR Blue>// store cImageIndexFocus</COLOR>
|
|
Stream.Write(cImageIndexFocus, SizeOf(cImageIndexFocus));
|
|
|
|
<COLOR Blue>// store cChildrenAllowed</COLOR>
|
|
Stream.Write(cChildrenAllowed, SizeOf(cChildrenAllowed));
|
|
<B>end</B>;
|
|
|
|
Now we can the tree we save also load again. This process could look like:
|
|
|
|
<B>try</B>
|
|
<COLOR Blue>// load tree</COLOR>
|
|
MyTree.LoadFromFile(MainControlForm.Filename);
|
|
<B>except</B>
|
|
<B>on</B> E: Exception <B>do</B>
|
|
<B>begin</B>
|
|
Application.MessageBox(PChar(E.Message), PChar(<COLOR Pink>'Error while loading.'</COLOR>), MB_OK);
|
|
MainControlForm.Filename := <COLOR Pink>''</COLOR>;
|
|
|
|
<COLOR Blue>// create tree with top level node (since loading failed)</COLOR>
|
|
Node := MyTree.AddChild(nil);
|
|
NodeD := MyTree.GetNodeData(Node);
|
|
NodeD.BasicND := TBasicNodeData.Create(<COLOR Pink>'new project'</COLOR>);
|
|
<B>end</B>;
|
|
<B>end</B>;
|
|
</CODE>
|
|
|
|
|
|
By the call of LoadFromFile the event OnLoadNode will be triggered and consequently the method LoadNode:
|
|
|
|
|
|
<CODE>
|
|
<B>procedure</B> TBasicNodeData.LoadNode(Stream: TStream);
|
|
|
|
<B>var</B>
|
|
Size: Integer;
|
|
StrBuffer: PChar;
|
|
|
|
<B>begin</B>
|
|
<COLOR Blue>// load cName</COLOR>
|
|
Stream.Read(Size, SizeOf(Size)); <COLOR Blue>// length of the string</COLOR>
|
|
|
|
StrBuffer := AllocMem(Size); <COLOR Blue>// get temporary memory</COLOR>
|
|
Stream.Read(StrBuffer^, Size); <COLOR Blue>// read the string</COLOR>
|
|
cName := StrBuffer;
|
|
FreeMem(StrBuffer);
|
|
<COLOR Blue>// Alternatively you can simply use:
|
|
// SetLength(cName, Size);
|
|
// Stream.Read(PChar(cName)^, Size);</COLOR>
|
|
|
|
<COLOR Blue>// load cImageIndex</COLOR>
|
|
Stream.Read(cImageIndex, SizeOf(cImageIndex));
|
|
|
|
<COLOR Blue>// load cImageIndexFocus</COLOR>
|
|
Stream.Read(cImageIndexFocus, SizeOf(cImageIndexFocus));
|
|
|
|
<COLOR Blue>// load cChildrenAllowed</COLOR>
|
|
Stream.Read(cChildrenAllowed, SizeOf(cChildrenAllowed));
|
|
<B>end</B>;
|
|
</CODE>
|
|
|
|
|
|
* Two columns in the treeview *
|
|
Now I want to show two columns in the treeview. Therefore I set the new properties of the tree in the object inspector.
|
|
|
|
|
|
|
|
By using Header.Columns you can create the desired columns. After that, you only have to set Header.Options.hoVisible to
|
|
True and the columns will appear in the treeview.
|
|
|
|
|
|
|
|
After you have set all necessary options you can give now the text and the icon for the particular column, respectively.
|
|
This happens in the already existing event handlers OnGetText and OnGetImageIndex where now also the given column index
|
|
must be taken into account.
|
|
|
|
|
|
<CODE>
|
|
<B>procedure</B> TMyForm.MyTreeGetText(Sender: TBaseVirtualTree; Node: PVirtualNode;
|
|
Column: Integer; TextType: TVSTTextType; <B>var</B> Text: WideString);
|
|
|
|
<B>var</B>
|
|
NodeD: ^rTreeData;
|
|
|
|
<B>begin</B>
|
|
NodeD := Sender.GetNodeData(Node);
|
|
|
|
<COLOR Blue>// return the the identifier of the node</COLOR>
|
|
<B>if</B> NodeD.BasicND = <B>nil</B> <B>then</B>
|
|
Text := ''
|
|
<B>else</B>
|
|
<B>begin</B>
|
|
<B>case</B> Column <B>of</B>
|
|
\-1,
|
|
0: <COLOR Blue>// main column, -1 if columns are hidden, 0 if they are shown</COLOR>
|
|
Text := NodeD.BasicND.Name;
|
|
1:
|
|
Text := <COLOR Pink>'This text appears in column 2.'</COLOR>;
|
|
<B>end</B>;
|
|
<B>end</B>;
|
|
<B>end</B>;
|
|
|
|
<B>procedure</B> TMyForm.MyTreeGetImageIndex(Sender: TBaseVirtualTree; Node: PVirtualNode;
|
|
Kind: TVTImageKind; Column: Integer; <B>var</B> Index: Integer);
|
|
|
|
<B>var</B>
|
|
NodeD: ^rTreeData;
|
|
|
|
<B>begin</B>
|
|
NodeD := Sender.GetNodeData(Node);
|
|
|
|
<B>if</B> Column = 0 <B>then</B> <COLOR Blue>// icons only in the first column</COLOR>
|
|
<B>case</B> Kind <B>of</B>
|
|
ikState:
|
|
Index := <COLOR Pink>-1</COLOR>;
|
|
ikNormal, ikSelected:
|
|
Index := NodeD.BasicND.GetImageIndex(Node = Sender.FocusedNode);
|
|
ikOverlay: <COLOR Blue>// e.g. to mark a node whose content changed,</COLOR>
|
|
<COLOR Blue>// Note: don't forget to call ImageList.Overlay for the image.</COLOR>
|
|
<B>if</B> NodeD.BasicND.ImageIndex = 4 <B>then</B>
|
|
Index := 6;
|
|
<B>end</B>;
|
|
<B>end</B>;
|
|
</CODE>
|
|
|
|
|
|
* Accessing the columns *
|
|
I want to demonstrate the access to the columns of a TVirtualStringTrees based on an example. In order to store global
|
|
\options, as in Point 2.12 I want to know the width of a column. This information is updated every time an OnColumnResize
|
|
event is triggered:
|
|
|
|
|
|
<CODE>
|
|
<B>procedure</B> TBookmarkForm.BookmarkTreeColumnResize(Sender: TBaseVirtualTree; Column: Integer);
|
|
|
|
<B>var</B>
|
|
NodeD: PTreeData;
|
|
|
|
<B>begin</B>
|
|
NodeD := Sender.GetNodeData(Sender.RootNode.FirstChild);
|
|
|
|
<COLOR Blue>// Keep the new size of the column in the project node.</COLOR>
|
|
TProjectNodeData(NodeD.BasicND).SetHColumnsWidth(
|
|
TVirtualStringTree(Sender).Header.Columns.Items[Column].Width,Column);
|
|
<B>end</B>;
|
|
</CODE>
|
|
|
|
|
|
The exciting part is the type casting of the sender object. In TBaseVirtualTree the header property is protected and only
|
|
after conversion (casting) to TVirtualTree it becomes accessible.
|
|
|
|
|
|
|
|
* Global tree options *
|
|
Global options like the sizes of the columns, which are adjusted in the project, will be stored as properties of the
|
|
top-level node. It contains so all project related options.
|
|
|
|
|
|
|
|
In order to avoid that all derived classes inherit these fields the top-level node class will be build from a new project
|
|
node class, which will be derived from the base node class.
|
|
|
|
|
|
|
|
The new hierarchy looks now so:
|
|
|
|
» Base node class... unites the properties of all nodes
|
|
|
|
» Project node class... enriches the base with management of project related options
|
|
|
|
» Folder node classes... enriches the base with default properties for all leaf nodes
|
|
|
|
» Leaf node class... the actual node class (special properties)
|
|
|
|
|
|
|
|
Since this involves already very application specific program details I want only make some notes.
|
|
|
|
|
|
|
|
The base node class has the ability to store node data. These methods must be declared as virtual and will be overridden
|
|
in the project node class to allow saving the project data.
|
|
|
|
|
|
|
|
Well, now I am ready to work with VirtualTreeview. It will become interesting later again when I will try to drag data
|
|
from other applications to the tree. But this is a different story...
|
|
|
|
Summary
|
|
Often a simple step by step tutorial gets you much faster started than a long list of features and possibilities. This
|
|
topic describes the basic usage on the basis of a simple project.
|