S
sdcx
Unregistered / Unconfirmed
GUEST, unregistred user!
Virtual Treeview step by step
1 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?
a) All nodes in the tree are equal
In this case a simple record defines the necessary data structure, e.g.:
type
rTreeData = record
Text: WideString;
URL: string[255];
CRC: LongInt;
isOpened: Boolean;
ImageIndex: Integer;
end;
b) There are different nodes in the tree (e.g. folders that can have sub nodes)
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 which 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 1, 2 or 3 I have to read - now known - following values. Now let us consider the record.
type
rTreeData = record
Typ: Integer;
Name: string[255];
pNodeData: Pointer;
end;
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 youdo
n’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.
Alternative solution:
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, respectively.
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).
So Ido
following:
As you can see from the declaration of the internal node of Virtual Tree
TVirtualNode = packed record
Index, // index of node with regard to its parent
ChildCount: Cardinal;
// number of child nodes
...
...
LastChild: PVirtualNode;
// link to the node's last child...
Data: record end;
// this is a placeholder, each node gets extra
// data determined by NodeDataSize
end;
there is another record at the end of the record structure. Which exact structure this is will be determined indirectly.
type
rTreeData = record
Name: string[255];
// the identifier of the node
ImageIndex: Integer;
// the image index of the node
pNodeData: Pointer;
end;
Let the above record be the structure. The Virtual Treeviewdo
es not really know this structure, but it knows how much space must be reserved. We tell it by
myVirtualTree.NodeDataSize := SizeOf(rTreeData);
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.
2 Implementation
2.1 An empty tree
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 thedo
cs 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 nowdo
the following during form creation:
procedure TMyForm.FormCreate(Sender: TObject);
var
Node: PVirtualNode;
begin
...
// create tree
MyTree.NodeDataSize := SizeOf(TTreeData);
if MyForm.filename = '' then
begin
// if there is no tree to load
// create tree with top level node
Node := BookmarkForm.BookmarkTree.AddChild(nil);
// adds a top level node
end
else
begin
// load tree
....
end;
....
end;
2.2 Data for the node
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.
var
...
NodeData: ^rTreeData;
begin
...
// determine data for node
NodeData := BookmarkForm.BookmarkTree.GetNodeData(Node);
NodeData.Name := 'new project';
NodeData.ImageIndex := 0;
...
2.3 Show the node name
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.
procedure TBookmarkForm.BookmarkTreeGetText(Sender: TBaseVirtualTree;
Node: PVirtualNode;
Column: Integer;
TextType: TVSTTextType;
var Text: WideString);
var
NodeData: ^rTreeData;
begin
NodeData := Sender.GetNodeData(Node);
// return identifier of the node
Text := NodeData.Name;
end;
2.4 The icon for the node
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 Ido
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.
procedure TBookmarkForm.BookmarkTreeGetImageIndex(Sender: TBaseVirtualTree;
Node: PVirtualNode;
Kind: TVTImageKind;
Column: Integer;
var Index: Integer);
var
NodeData: ^rTreeData;
begin
NodeData := Sender.GetNodeData(Node);
case Kind of
ikState: // for the case the state icon has been requested
Index := -1;
ikNormal, ikSelected: // normal or the selected icon is required
Index := NodeData.ImageIndex;
end;
end;
Depending on whether a node is selected or not, different icons shall be shown (see step 6).
2.5 Only one node class in the record
Since I want to avoid mixing data in the record and later then
data in the node class I decided to change this record
type
TTreeData = record
Name: string[255];
// the identifier of the node
ImageIndex: Integer;
// the image index of the node
pNodeData: Pointer;
end;
into a record which contains only one pointer to a node class. I declare therefore first a node class
TBasicNodeData = class
...
end;
and then
a structure of the form:
rTreeData = record
BasicND: TBasicNodeData;
end;
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
if NodeD.BasicND = nil then
Text := ''
must be returned or you wrap the entire initialization into a begin
Update/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.
2.5.1 Example class declaration
unit TreeData;
interface
//===========================================
type
// declare common node class
TBasicNodeData = class
protected
cName: ShortString;
cImageIndex: Integer;
public
constructor Create;
overload;
constructor Create(vName: ShortString;
vIIndex: Integer = 0);
overload;
property Name: ShortString read cName write cName;
property ImageIndex: Integer read cImageIndex write cImageIndex;
end;
// declare new structure for node data
rTreeData = record
BasicND: TBasicNodeData;
end;
implementation
constructor TBasicNodeData.Create;
begin
{ not necessary
cName := '';
cImageIndex := 0;
}
end;
constructor TBasicNodeData.Create(vName: ShortString;
vIIndex: Integer = 0);
begin
cName := vName;
cImageIndex := vIIndex;
end;
end.
2.5.2 Example creation of the tree
// Tree will be created when the form is created.
procedure TMyForm.FormCreate(Sender: TObject);
var
Node: PVirtualNode;
NodeD: ^rTreeData;
begin
....
// create tree
MyTree.NodeDataSize := SizeOf(rTreeData);
if MainControlForm.filename = '' then
begin
// create tree with top level node
Node := MyTree.AddChild(nil);
// adds a node to the root of the tree
// assign data for this node
NodeD := MyTree.GetNodeData(Node);
NodeD.BasicND := TBasicNodeData.Create('new project');
end
else
begin
// load tree
end;
...
end;
{______________________________MyTree_________________________________}
// returns the text (the identification) of the node
procedure TMyForm.MyTreeGetText(Sender: TBaseVirtualTree;
Node: PVirtualNode;
Column: Integer;
TextType: TVSTTextType;
var Text: WideString);
var
NodeD: ^rTreeData;
begin
NodeD := Sender.GetNodeData(Node);
// return the identifier of the node
if NodeD.BasicND = nil then
Text:=''
else
Text := NodeD.BasicND.Name;
end;
// returns the index for image display
procedure TMyForm.MyTreeGetImageIndex(Sender: TBaseVirtualTree;
Node: PVirtualNode;
Kind: TVTImageKind;
Column: Integer;
var Index: Integer);
var
NodeD: ^rTreeData;
begin
NodeD := Sender.GetNodeData(Node);
case Kind of
ikState: // for the case the state index has been requested
Index := -1;
ikNormal, ikSelected: // normal icon case
Index := NodeD.BasicND.ImageIndex;
end;
end;
2.6 Icons for selected nodes
If a node is selected a different symbol shall be shown:
Therefore I implement a new method
function GetImageIndex(focus: Boolean): Integer;
virtual;
which gets the normal image index or the index for focused nodes depending on whether the node has the focus or not.
Call:
Index := NodeD.BasicND.GetImageIndex(Node = Sender.FocusedNode);
Implementation of the method:
function TBasicNodeData.GetImageIndex(focus: Boolean): Integer;
begin
if focus then
Result := cImageIndexFocus
else
Result := cImageIndex;
end;
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:
constructor TBasicNodeData.Create(vName: ShortString;
vIIndex: Integer = 0);
begin
cName := vName;
cImageIndex := vIIndex;
cImageIndexFocus := vIIndex + 1;
end;
2.7 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:
procedure TMyForm.addClick (Sender: TObject);
var
Node: PVirtualNode;
NodeD: ^rTreeData;
begin
// Ok, a node must be added.
Node := MyTree.AddChild(MyTree.FocusedNode);
// adds a node as the last child
// determine data of node
NodeD := MyTree.GetNodeData(Node);
NodeD.BasicND := TBasicNodeData.Create('Child');
end;
Caution: What must bedo
ne if no node has the focus?
à e.g. insert the new node as child of a top level nodes.
if BookmarkTree.FocusedNode = nil then
begin
// insert as child of the first top level node
Node := BookmarkTree.AddChild(BookmarkTree.RootNode.FirstChild);
// determine data for node
NodeD := BookmarkTree.GetNodeData(Node);
NodeD.BasicND := TFolderNodeData.Create('new folder');
end
else
begin
// Ok, a new node must be added.
Node := BookmarkTree.AddChild(BookmarkTree.FocusedNode);
// determine data of the node
NodeD := BookmarkTree.GetNodeData(Node);
NodeD.BasicND := TFolderNodeData.Create('new folder');
end;
If the node with the focus must be deleted the following happens:
procedure TMyForm.delClick (Sender: TObject);
begin
// The focused node should be removed. The top level must not be deleted however.
if MyTree.FocusedNode = nil then
MessageDlg('There was no node selected.', mtInformation, [mbOk], 0)
else
// 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.
if MyTree.FocusedNode.Parent = MyTree.RootNode then
MessageDlg('The project node must not be deleted.', mtInformation, [mbOk], 0)
else
MyTree.DeleteNode(MyTree.FocusedNode);
end;
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:
procedure TMyForm.MyTreeFreeNode(Sender: TBaseVirtualTree;
Node: PVirtualNode);
begin
// Free here the node data (Note: type PtreeData = ^rTreeData).
PTreeData(Sender.GetNodeData(Node)).BasicND.Free;
end;
2.8 Adding folder and leafs
Now I am ready to add folders to the tree as well as final nodes, whichdo
not have children. For this I derive two new node classes from the base class.
TFolderNodeData = class(TBasicNodeData)
TItemNodeData = class(TBasicNodeData)
Depending on which kind of node the user wants to create using the context menu I store a particular class in the node record.
NodeD.BasicND := TFolderNodeData.Create('new folder');
NodeD.BasicND := TItemNodeData.Create('new node');
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).
2.9 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
procedure TBaseVirtualTree.SaveToFile(const FileName: TFileName);
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.
property OnSaveNode: TVTSaveNodeEvent read FOnSaveNode write FOnSaveNode;
If OnSaveNode is triggered then
the method SaveNode of the particular node class will be called:
procedure TMyForm.MyTreeSaveNode(Sender: TBaseVirtualTree;
Node: PVirtualNode;
Stream: TStream);
begin
PTreeData(Sender.GetNodeData(Node)).BasicND.SaveToFile(Stream);
end;
In the SaveNode method of the class fields like node name, image index etc. are stored in the tree:
procedure TBasicNodeData.SaveNode(Stream: TStream);
var
size: Integer;
begin
// save type of the node
Stream.Write(Art, SizeOf(Art));
// store cName
Size := Length(cName) + 1;
// include terminating #0
Stream.Write(Size, SizeOf(Size));
// store length of the string
Stream.Write(PChar(cName)^, Size);
// now the string itself
// store cImageIndex
Stream.Write(cImageIndex, SizeOf(cImageIndex));
// store cImageIndexFocus
Stream.Write(cImageIndexFocus, SizeOf(cImageIndexFocus));
// store cChildrenAllowed
Stream.Write(cChildrenAllowed, SizeOf(cChildrenAllowed));
end;
Now we can the tree we save also load again. This process could look like:
try
// load tree
MyTree.LoadFromFile(MainControlForm.Filename);
except
on E: Exceptiondo
begin
Application.MessageBox(PChar(E.Message), PChar('Error while loading.'), MB_OK);
MainControlForm.Filename := '';
// create tree with top level node (since loading failed)
Node := MyTree.AddChild(nil);
NodeD := MyTree.GetNodeData(Node);
NodeD.BasicND := TBasicNodeData.Create('new project');
end;
end;
By the call of LoadFromFile the event OnLoadNode will be triggered and consequently the method LoadNode:
procedure TBasicNodeData.LoadNode(Stream: TStream);
var
Size: Integer;
StrBuffer: PChar;
begin
// load cName
Stream.Read(Size, SizeOf(Size));
// length of the string
StrBuffer := AllocMem(Size);
// get temporary memory
Stream.Read(StrBuffer^, Size);
// read the string
cName := StrBuffer;
FreeMem(StrBuffer);
// Alternatively you can simply use:
// SetLength(cName, Size);
// Stream.Read(PChar(cName)^, Size);
// load cImageIndex
Stream.Read(cImageIndex, SizeOf(cImageIndex));
// load cImageIndexFocus
Stream.Read(cImageIndexFocus, SizeOf(cImageIndexFocus));
// load cChildrenAllowed
Stream.Read(cChildrenAllowed, SizeOf(cChildrenAllowed));
end;
2.10 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.
procedure TMyForm.MyTreeGetText(Sender: TBaseVirtualTree;
Node: PVirtualNode;
Column: Integer;
TextType: TVSTTextType;
var Text: WideString);
var
NodeD: ^rTreeData;
begin
NodeD := Sender.GetNodeData(Node);
// return the the identifier of the node
if NodeD.BasicND = nil then
Text := ''
else
begin
case Column of
-1,
0: // main column, -1 if columns are hidden, 0 if they are shown
Text := NodeD.BasicND.Name;
1:
Text := 'This text appears in column 2.';
end;
end;
end;
procedure TMyForm.MyTreeGetImageIndex(Sender: TBaseVirtualTree;
Node: PVirtualNode;
Kind: TVTImageKind;
Column: Integer;
var Index: Integer);
var
NodeD: ^rTreeData;
begin
NodeD := Sender.GetNodeData(Node);
if Column = 0 then
// icons only in the first column
case Kind of
ikState:
Index := -1;
ikNormal, ikSelected:
Index := NodeD.BasicND.GetImageIndex(Node = Sender.FocusedNode);
ikOverlay: // e.g. to mark a node whose content changed,
// Note:do
n’t forget to call ImageList.Overlay for the image.
if NodeD.BasicND.ImageIndex = 4 then
Index := 6;
end;
end;
2.11 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:
procedure TBookmarkForm.BookmarkTreeColumnResize(Sender: TBaseVirtualTree;
Column: Integer);
var
NodeD: PTreeData;
begin
NodeD := Sender.GetNodeData(Sender.RootNode.FirstChild);
// Keep the new size of the column in the project node.
TProjectNodeData(NodeD.BasicND).SetHColumnsWidth(
TVirtualStringTree(Sender).Header.Columns.Items[Column].Width,Column);
end;
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.
2.12 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...
1 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?
a) All nodes in the tree are equal
In this case a simple record defines the necessary data structure, e.g.:
type
rTreeData = record
Text: WideString;
URL: string[255];
CRC: LongInt;
isOpened: Boolean;
ImageIndex: Integer;
end;
b) There are different nodes in the tree (e.g. folders that can have sub nodes)
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 which 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 1, 2 or 3 I have to read - now known - following values. Now let us consider the record.
type
rTreeData = record
Typ: Integer;
Name: string[255];
pNodeData: Pointer;
end;
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 youdo
n’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.
Alternative solution:
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, respectively.
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).
So Ido
following:
As you can see from the declaration of the internal node of Virtual Tree
TVirtualNode = packed record
Index, // index of node with regard to its parent
ChildCount: Cardinal;
// number of child nodes
...
...
LastChild: PVirtualNode;
// link to the node's last child...
Data: record end;
// this is a placeholder, each node gets extra
// data determined by NodeDataSize
end;
there is another record at the end of the record structure. Which exact structure this is will be determined indirectly.
type
rTreeData = record
Name: string[255];
// the identifier of the node
ImageIndex: Integer;
// the image index of the node
pNodeData: Pointer;
end;
Let the above record be the structure. The Virtual Treeviewdo
es not really know this structure, but it knows how much space must be reserved. We tell it by
myVirtualTree.NodeDataSize := SizeOf(rTreeData);
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.
2 Implementation
2.1 An empty tree
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 thedo
cs 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 nowdo
the following during form creation:
procedure TMyForm.FormCreate(Sender: TObject);
var
Node: PVirtualNode;
begin
...
// create tree
MyTree.NodeDataSize := SizeOf(TTreeData);
if MyForm.filename = '' then
begin
// if there is no tree to load
// create tree with top level node
Node := BookmarkForm.BookmarkTree.AddChild(nil);
// adds a top level node
end
else
begin
// load tree
....
end;
....
end;
2.2 Data for the node
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.
var
...
NodeData: ^rTreeData;
begin
...
// determine data for node
NodeData := BookmarkForm.BookmarkTree.GetNodeData(Node);
NodeData.Name := 'new project';
NodeData.ImageIndex := 0;
...
2.3 Show the node name
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.
procedure TBookmarkForm.BookmarkTreeGetText(Sender: TBaseVirtualTree;
Node: PVirtualNode;
Column: Integer;
TextType: TVSTTextType;
var Text: WideString);
var
NodeData: ^rTreeData;
begin
NodeData := Sender.GetNodeData(Node);
// return identifier of the node
Text := NodeData.Name;
end;
2.4 The icon for the node
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 Ido
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.
procedure TBookmarkForm.BookmarkTreeGetImageIndex(Sender: TBaseVirtualTree;
Node: PVirtualNode;
Kind: TVTImageKind;
Column: Integer;
var Index: Integer);
var
NodeData: ^rTreeData;
begin
NodeData := Sender.GetNodeData(Node);
case Kind of
ikState: // for the case the state icon has been requested
Index := -1;
ikNormal, ikSelected: // normal or the selected icon is required
Index := NodeData.ImageIndex;
end;
end;
Depending on whether a node is selected or not, different icons shall be shown (see step 6).
2.5 Only one node class in the record
Since I want to avoid mixing data in the record and later then
data in the node class I decided to change this record
type
TTreeData = record
Name: string[255];
// the identifier of the node
ImageIndex: Integer;
// the image index of the node
pNodeData: Pointer;
end;
into a record which contains only one pointer to a node class. I declare therefore first a node class
TBasicNodeData = class
...
end;
and then
a structure of the form:
rTreeData = record
BasicND: TBasicNodeData;
end;
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
if NodeD.BasicND = nil then
Text := ''
must be returned or you wrap the entire initialization into a begin
Update/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.
2.5.1 Example class declaration
unit TreeData;
interface
//===========================================
type
// declare common node class
TBasicNodeData = class
protected
cName: ShortString;
cImageIndex: Integer;
public
constructor Create;
overload;
constructor Create(vName: ShortString;
vIIndex: Integer = 0);
overload;
property Name: ShortString read cName write cName;
property ImageIndex: Integer read cImageIndex write cImageIndex;
end;
// declare new structure for node data
rTreeData = record
BasicND: TBasicNodeData;
end;
implementation
constructor TBasicNodeData.Create;
begin
{ not necessary
cName := '';
cImageIndex := 0;
}
end;
constructor TBasicNodeData.Create(vName: ShortString;
vIIndex: Integer = 0);
begin
cName := vName;
cImageIndex := vIIndex;
end;
end.
2.5.2 Example creation of the tree
// Tree will be created when the form is created.
procedure TMyForm.FormCreate(Sender: TObject);
var
Node: PVirtualNode;
NodeD: ^rTreeData;
begin
....
// create tree
MyTree.NodeDataSize := SizeOf(rTreeData);
if MainControlForm.filename = '' then
begin
// create tree with top level node
Node := MyTree.AddChild(nil);
// adds a node to the root of the tree
// assign data for this node
NodeD := MyTree.GetNodeData(Node);
NodeD.BasicND := TBasicNodeData.Create('new project');
end
else
begin
// load tree
end;
...
end;
{______________________________MyTree_________________________________}
// returns the text (the identification) of the node
procedure TMyForm.MyTreeGetText(Sender: TBaseVirtualTree;
Node: PVirtualNode;
Column: Integer;
TextType: TVSTTextType;
var Text: WideString);
var
NodeD: ^rTreeData;
begin
NodeD := Sender.GetNodeData(Node);
// return the identifier of the node
if NodeD.BasicND = nil then
Text:=''
else
Text := NodeD.BasicND.Name;
end;
// returns the index for image display
procedure TMyForm.MyTreeGetImageIndex(Sender: TBaseVirtualTree;
Node: PVirtualNode;
Kind: TVTImageKind;
Column: Integer;
var Index: Integer);
var
NodeD: ^rTreeData;
begin
NodeD := Sender.GetNodeData(Node);
case Kind of
ikState: // for the case the state index has been requested
Index := -1;
ikNormal, ikSelected: // normal icon case
Index := NodeD.BasicND.ImageIndex;
end;
end;
2.6 Icons for selected nodes
If a node is selected a different symbol shall be shown:
Therefore I implement a new method
function GetImageIndex(focus: Boolean): Integer;
virtual;
which gets the normal image index or the index for focused nodes depending on whether the node has the focus or not.
Call:
Index := NodeD.BasicND.GetImageIndex(Node = Sender.FocusedNode);
Implementation of the method:
function TBasicNodeData.GetImageIndex(focus: Boolean): Integer;
begin
if focus then
Result := cImageIndexFocus
else
Result := cImageIndex;
end;
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:
constructor TBasicNodeData.Create(vName: ShortString;
vIIndex: Integer = 0);
begin
cName := vName;
cImageIndex := vIIndex;
cImageIndexFocus := vIIndex + 1;
end;
2.7 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:
procedure TMyForm.addClick (Sender: TObject);
var
Node: PVirtualNode;
NodeD: ^rTreeData;
begin
// Ok, a node must be added.
Node := MyTree.AddChild(MyTree.FocusedNode);
// adds a node as the last child
// determine data of node
NodeD := MyTree.GetNodeData(Node);
NodeD.BasicND := TBasicNodeData.Create('Child');
end;
Caution: What must bedo
ne if no node has the focus?
à e.g. insert the new node as child of a top level nodes.
if BookmarkTree.FocusedNode = nil then
begin
// insert as child of the first top level node
Node := BookmarkTree.AddChild(BookmarkTree.RootNode.FirstChild);
// determine data for node
NodeD := BookmarkTree.GetNodeData(Node);
NodeD.BasicND := TFolderNodeData.Create('new folder');
end
else
begin
// Ok, a new node must be added.
Node := BookmarkTree.AddChild(BookmarkTree.FocusedNode);
// determine data of the node
NodeD := BookmarkTree.GetNodeData(Node);
NodeD.BasicND := TFolderNodeData.Create('new folder');
end;
If the node with the focus must be deleted the following happens:
procedure TMyForm.delClick (Sender: TObject);
begin
// The focused node should be removed. The top level must not be deleted however.
if MyTree.FocusedNode = nil then
MessageDlg('There was no node selected.', mtInformation, [mbOk], 0)
else
// 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.
if MyTree.FocusedNode.Parent = MyTree.RootNode then
MessageDlg('The project node must not be deleted.', mtInformation, [mbOk], 0)
else
MyTree.DeleteNode(MyTree.FocusedNode);
end;
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:
procedure TMyForm.MyTreeFreeNode(Sender: TBaseVirtualTree;
Node: PVirtualNode);
begin
// Free here the node data (Note: type PtreeData = ^rTreeData).
PTreeData(Sender.GetNodeData(Node)).BasicND.Free;
end;
2.8 Adding folder and leafs
Now I am ready to add folders to the tree as well as final nodes, whichdo
not have children. For this I derive two new node classes from the base class.
TFolderNodeData = class(TBasicNodeData)
TItemNodeData = class(TBasicNodeData)
Depending on which kind of node the user wants to create using the context menu I store a particular class in the node record.
NodeD.BasicND := TFolderNodeData.Create('new folder');
NodeD.BasicND := TItemNodeData.Create('new node');
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).
2.9 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
procedure TBaseVirtualTree.SaveToFile(const FileName: TFileName);
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.
property OnSaveNode: TVTSaveNodeEvent read FOnSaveNode write FOnSaveNode;
If OnSaveNode is triggered then
the method SaveNode of the particular node class will be called:
procedure TMyForm.MyTreeSaveNode(Sender: TBaseVirtualTree;
Node: PVirtualNode;
Stream: TStream);
begin
PTreeData(Sender.GetNodeData(Node)).BasicND.SaveToFile(Stream);
end;
In the SaveNode method of the class fields like node name, image index etc. are stored in the tree:
procedure TBasicNodeData.SaveNode(Stream: TStream);
var
size: Integer;
begin
// save type of the node
Stream.Write(Art, SizeOf(Art));
// store cName
Size := Length(cName) + 1;
// include terminating #0
Stream.Write(Size, SizeOf(Size));
// store length of the string
Stream.Write(PChar(cName)^, Size);
// now the string itself
// store cImageIndex
Stream.Write(cImageIndex, SizeOf(cImageIndex));
// store cImageIndexFocus
Stream.Write(cImageIndexFocus, SizeOf(cImageIndexFocus));
// store cChildrenAllowed
Stream.Write(cChildrenAllowed, SizeOf(cChildrenAllowed));
end;
Now we can the tree we save also load again. This process could look like:
try
// load tree
MyTree.LoadFromFile(MainControlForm.Filename);
except
on E: Exceptiondo
begin
Application.MessageBox(PChar(E.Message), PChar('Error while loading.'), MB_OK);
MainControlForm.Filename := '';
// create tree with top level node (since loading failed)
Node := MyTree.AddChild(nil);
NodeD := MyTree.GetNodeData(Node);
NodeD.BasicND := TBasicNodeData.Create('new project');
end;
end;
By the call of LoadFromFile the event OnLoadNode will be triggered and consequently the method LoadNode:
procedure TBasicNodeData.LoadNode(Stream: TStream);
var
Size: Integer;
StrBuffer: PChar;
begin
// load cName
Stream.Read(Size, SizeOf(Size));
// length of the string
StrBuffer := AllocMem(Size);
// get temporary memory
Stream.Read(StrBuffer^, Size);
// read the string
cName := StrBuffer;
FreeMem(StrBuffer);
// Alternatively you can simply use:
// SetLength(cName, Size);
// Stream.Read(PChar(cName)^, Size);
// load cImageIndex
Stream.Read(cImageIndex, SizeOf(cImageIndex));
// load cImageIndexFocus
Stream.Read(cImageIndexFocus, SizeOf(cImageIndexFocus));
// load cChildrenAllowed
Stream.Read(cChildrenAllowed, SizeOf(cChildrenAllowed));
end;
2.10 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.
procedure TMyForm.MyTreeGetText(Sender: TBaseVirtualTree;
Node: PVirtualNode;
Column: Integer;
TextType: TVSTTextType;
var Text: WideString);
var
NodeD: ^rTreeData;
begin
NodeD := Sender.GetNodeData(Node);
// return the the identifier of the node
if NodeD.BasicND = nil then
Text := ''
else
begin
case Column of
-1,
0: // main column, -1 if columns are hidden, 0 if they are shown
Text := NodeD.BasicND.Name;
1:
Text := 'This text appears in column 2.';
end;
end;
end;
procedure TMyForm.MyTreeGetImageIndex(Sender: TBaseVirtualTree;
Node: PVirtualNode;
Kind: TVTImageKind;
Column: Integer;
var Index: Integer);
var
NodeD: ^rTreeData;
begin
NodeD := Sender.GetNodeData(Node);
if Column = 0 then
// icons only in the first column
case Kind of
ikState:
Index := -1;
ikNormal, ikSelected:
Index := NodeD.BasicND.GetImageIndex(Node = Sender.FocusedNode);
ikOverlay: // e.g. to mark a node whose content changed,
// Note:do
n’t forget to call ImageList.Overlay for the image.
if NodeD.BasicND.ImageIndex = 4 then
Index := 6;
end;
end;
2.11 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:
procedure TBookmarkForm.BookmarkTreeColumnResize(Sender: TBaseVirtualTree;
Column: Integer);
var
NodeD: PTreeData;
begin
NodeD := Sender.GetNodeData(Sender.RootNode.FirstChild);
// Keep the new size of the column in the project node.
TProjectNodeData(NodeD.BasicND).SetHColumnsWidth(
TVirtualStringTree(Sender).Header.Columns.Items[Column].Width,Column);
end;
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.
2.12 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...