|
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.
Written by Sven H. ([email protected]), Revision and translation by Mike Lischke ([email protected])
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 [email protected]. 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.
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 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.
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:
- Typ is an integer value, from which I can determine what kind of node this is, in my example 1, 2 or 3.
- 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).
- 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.
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. 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 I do 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 Treeview does 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.
Implementation
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 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:
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;
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; ...
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;
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 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.
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).
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 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.
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.
// 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; // 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;
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;
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 be done 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;
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.
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).
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: Exception do 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;
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: don't forget to call ImageList.Overlay for the image. if NodeD.BasicND.ImageIndex = 4 then Index := 6; end; end;
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.
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...
What do you think about this topic? Send feedback!
|