You can download our sample files as part of the file nov98.zip. Visit www.zdjournals.com/cpb and click on the Source Code hyperlink.
This article is the first in a two-part series covering the TTreeView component. If you aren't familiar with what this component can do for you, you can see its behavior by experimenting with the left pane in the Windows Explorer application. In this pane, the TTreeView represents system elements, such as disk drives, the Recycle Bin, the Printers folder, and the Control Panel.
While TTreeView is a powerful tool for visualizing hierarchical relationships, it's somewhat difficult to take advantage of as a data structure. We'll try to take the pain out of implementing TTreeView by looking at its basic elements and discussing their relationships. Then, we'll examine the methods and attributes used to build and maneuver within a TTreeView.
Figure A: Our sample TTreeView displays information about Major League Baseball.
TTreeNode* __fastcall Add(TTreeNode* Node, const System::AnsiString S);Any time the Node parameter is NULL, you add the node to level 0 (a root node). The AnsiString argument is simply the text that will appear in the TTreeView representation of the new node (such as Boston). Add appends the new node to the end of the current group of siblings, whereas AddFirst inserts it at the beginning. Here's some code to insert the two root nodes shown in Figure A:
TreeView1->Items->Add(NULL, "American League"); TreeView1->Items->Add(NULL, "National League");All the methods that add nodes to the TTreeView take the same arguments as Add--an existing TTreeNode and an AnsiString. Later, we'll cover some exceptions (such as assigning something to the new TTreeNode object's void*).
Add and AddFirst operate on the current group of siblings--they take a TTreeNode pointer to any existing node, then adding a sibling to it. For example, you can add a new pitcher named Tom Gordon to the bottom of Boston's pitching roster by calling Add and passing in a pointer to an existing Boston pitcher:
TTreeNode* tomGordonNode =
TreeView1->Items->Add(pedroMartinezNode,
"Tom Gordon");
The Insert method gives you a bit more precision when adding new nodes. Insert
takes a pointer to an existing node and inserts a new node just prior to it.
For example, the following code will place Tom Gordon between Martinez and
Wakefield on the Boston pitching roster:TTreeNode* tomGordonNode = TreeView1->Items-> Insert(timWakefieldNode, "Tom Gordon");Like Add and AddFirst, Insert operates on the current sibling group. Note that calling Insert with a NULL Node parameter will append the new node to the end of the list of root nodes--essentially the equivalent of calling Add with a NULL Node parameter.
TreeView1->Items-> AddChild(bostonInfieldersNode, "Mo Vaughn");(AddChildFirst would have placed Mo at the top). Now, let's see how to delete a TTreeNode. When we added Tom Gordon to the list of pitchers, we got back a pointer to the new node and stored it in tomGordonNode. To get rid of him, we can simply make the following call:
tomGordonNode->Delete();
| Note: Be careful with Delete |
|---|
| Calling Delete on a node that has children associated with it will delete all the children, as well--this includes all nodes subsequent to the node being deleted (children, grandchildren, and so on). |
You can just as easily remove all the children of any parent node without removing the parent itself, by calling the DeleteChildren method. The following code wipes out the entire Boston pitching staff, but leaves the Pitchers node:
bostonPitchers->DeleteChildren();
Table A: Add and Insert methods that accept objects
| Method | Method With Object |
|---|---|
| Add | AddObject |
| AddFirst | AddObjectFirst |
| AddChild | AddChildObject |
| AddChildFirst | AddChildObjectFirst |
| Insert | InsertObject |
The signature changes slightly to include the void*:
TTreeNode* __fastcall AddObject(TTreeNode* Node, const System::AnsiString S, void * Ptr);The add and insert methods invoke their Object versions behind the scenes. They simply provide NULL as the value for the void* parameter, indicating that no object is present. Besides supplying a pointer through the add and insert methods, you can also set and access the pointer through the node's Data property.
Suppose we have a Stats object containing a pitcher's current statistics. The following code creates a new Stats object and attaches it to a new TTreeNode:
Stats *newStats = new Stats(); tomGordonNode = TreeView1->Items-> AddObject(pedroMartinezNode, "Tom Gordon", newStats);You can also attach this object to a pitcher's TTreeNode later by setting the Data property:
tomGordonNode->Data = (void*) newStats;And the following code gets the object back:
Stats* tomsStats = (Stats*) tomGordonNode->Data;Being able to associate any object with a TTreeNode adds exciting possibilities to your code. However, you must treat the Data property with caution. For example, the TTreeNode Delete method doesn't delete the memory associated with the void*--it's up to your code to take care of that. Here's a simple way to do it:
Stats* tomsStats = (Stats*) tomGordonNode->Data; delete tomsStats; tomGordonNode->Delete();If you're going to delete the object pointed to by Data but leave the TTreeNode, it's a good idea to set the Data pointer to NULL in case it's checked by subsequent operations in your program. You can do so as follows:
Stats* tomsStats = (Stats*) tomGordonNode->Data; delete tomsStats; tomGordonNode->Data = (void*) NULL;You now have the tools to build a TTreeView. Next, we'll discuss some methods you can use to navigate a TTreeView from within your code.
Note: Watch out for lowercase
|
Some of the methods in this section (getFirstChild, getNextSibling, and
getPrevSibling) don't start with a capital letter, as do the other methods.
This is a bug in C++Builder that has been reported. | |
|---|
TTreeNode* firstRoot = TreeView1->Items->GetFirstNode();Since all root nodes are siblings of each other (part of the same sibling group), you can then determine the other root nodes using the getNextSibling method. This code adds the names of a tree's root nodes to a ListBox:
????
TTreeNode* rootNode =
TreeView1->Items->GetFirstNode();
while ( rootNode )
{
ListBox1->Items->Add(rootNode->Text);
rootNode = rootNode->getNextSibling();
}
????
If
we executed this code on the TTreeView in Figure A, ListBox1 would
contain two items: American League and National League. The getNextSibling
method's getPrevSibling counterpart returns the previous sibling in a sibling
group. If there are no previous or next siblings, the method will return a
NULL--your code should check for this before trying to perform some operation
on the resulting TTreeNode pointer.In addition to the sibling methods, four methods return information about the children of a TTreeNode: getFirstChild, GetNextChild, GetPrevChild, and GetLastChild. Here are two short examples--one that walks the teams in the American League East division from top to bottom, and one that goes from bottom to top (we've set alEast to the correct parent node):
TTreeNode* team = alEast->getFirstChild();
while ( team )
{
ListBox1->Items->Add(team->Text);
team = alEast->GetNextChild(team);
}
TTreeNode* team = alEast->GetLastChild();
while ( team )
{
ListBox1->Items->Add(team->Text);
team = alEast->GetPrevChild(team);
}
Here's
another way to walk the children of alEast:if ( alEast->HasChildren )
{
ListBox1->Items->
Add("Here are the teams in the AL East:");
for ( int team = 0; team < alEast->Count;
team++ )
ListBox1->Items->
Add(" " + alEast->Item[team]->Text);
}
else
ListBox1->Items->
Add("There are no teams in the AL East!");
This
example exposes some additional methods and properties we haven't covered yet,
but their purpose will be obvious. Again, we're dumping information into
ListBox1. Notice that you can address an individual child by its item index. For any TTreeNode with children, each child has a unique index in its sibling group and can be addressed by the parent via parentNode->Item[x]. A child can also tell you its position, as follows:
int x = childNode->Index;You can find out any TTreeNode's parent using the Parent property (this will return a NULL for root nodes):
TTreeNode* parent = childNode->Parent;Interestingly enough, C++Builder also provides a method to find out if one node is the parent of another. Here, node A checks to see if node B is its parent:
bool isParent = nodeA->HasAsParent(nodeB);You can also test in the opposite direction, checking to see whether node A is a child of node B. You do this using the IndexOf method, which returns the position of the child node among its siblings (starting with zero):
int pos = nodeB->IndexOf(nodeA);The method returns -1 if the child node isn't found. Finally, you can ask any TTreeNode its level by using the Level property:
int nodeLvl = anyNode->Level;
Table B: AbsoluteIndex values from Figure A
| TTreeNode | Index |
|---|---|
| American League | 0 |
| East | 1 |
| Boston | 2 |
| Pitchers | 3 |
| Pedro Martinez | 4 |
| Tim Wakefield | 5 |
If the (etc) node didn't exist, then Catchers would be 6. We don't recommend relying heavily on AbsoluteIndex in your code, because the values change regularly.
The application consists of one form containing a TTreeView, a list box to display method output, and several buttons that invoke the methods demonstrated in the article. Because the application demonstrates how to build a TTreeView, you should click the buttons in a logical order: first the left column from top to bottom, then the right column from top to bottom. Figure B shows the application's main window.
Figure B: Our sample application illustrates many of the concepts we've discussed.
Listing A: Unit1.cpp
//--------------------------------------------------
#include <vcl.h>
#pragma hdrstop
#include "Unit1.h"
//--------------------------------------------------
#pragma package(smart_init)
#pragma resource "*.dfm"
TForm1 *Form1;
//--------------------------------------------------
__fastcall TForm1::TForm1(TComponent* Owner) : TForm(Owner)
{
tomGordonNodeAdd = NULL;
tomGordonNodeInsert = NULL;
}
//--------------------------------------------------
void __fastcall TForm1::CreateFigAClick(TObject *Sender)
{
// Add the root nodes
al = TreeView1->Items->Add(NULL, "American League");
nl = TreeView1->Items->Add(NULL, "National League");
// Add the divisions in both leagues
alEast = TreeView1->Items->AddChild(al, "East");
alCentral = TreeView1->Items->AddChild(al, "Central");
alWest = TreeView1->Items->AddChild(al, "West");
nlEast = TreeView1->Items->AddChild(nl, "East");
nlCentral = TreeView1->Items->AddChild(nl, "Central");
nlWest = TreeView1->Items->AddChild(nl, "West");
// Add teams only to American League East to save space
bosNode = TreeView1->Items->AddChild(alEast, "Boston");
TreeView1->Items->AddChild(alEast, "New York");
TreeView1->Items->AddChild(alEast, "Toronto");
TreeView1->Items->AddChild(alEast, "Baltimore");
TreeView1->Items->AddChild(alEast, "Tampa Bay");
// Insert player categories under each team
for ( int i = 0; i < alEast->Count; i++ )
{
TreeView1->Items->AddChild(alEast->Item[i], "Pitchers");
TreeView1->Items->AddChild(alEast->Item[i], "Catchers");
TreeView1->Items->AddChild(alEast->Item[i], "Infielders");
TreeView1->Items->AddChild(alEast->Item[i], "Outfielders");
}
// Add the Boston pitchers
pitNode = bosNode->getFirstChild();
pedroMartinezNode = TreeView1->Items->
AddChild(pitNode, "Pedro Martinez");
timWakefieldNode = TreeView1->Items->
AddChild(pitNode, "Tim Wakefield");
TreeView1->Items->
AddChild(pitNode, "(etc)");
// Find the infield for use later
// pitchers
bostonInfieldersNode = bosNode->getFirstChild();
// Catchers
bostonInfieldersNode = bostonInfieldersNode->getNextSibling();
// Infielders!
bostonInfieldersNode = bostonInfieldersNode->getNextSibling();
// Show the entire tree from the top
TreeView1->FullExpand();
TreeView1->TopItem = TreeView1->Items->GetFirstNode();
}
//--------------------------------------------------
void __fastcall TForm1::QuitClick(TObject *Sender)
{
Close();
}
//--------------------------------------------------
void __fastcall TForm1::AddTomGordonClick(TObject *Sender)
{
// Add a Tom Gordon and put Stats in the Data property
Stats* lclStats = new Stats(1);
tomGordonNodeAdd = TreeView1->
Items->Add(pedroMartinezNode, "Tom Gordon");
tomGordonNodeAdd->Data = (void *) lclStats;
ListBox1->Items->Add("Inserted Tom with a new stats object");
ListBox1->Items->Add("containing the number 1");
}
//--------------------------------------------------
void __fastcall TForm1::InsertTomGordonClick(TObject *Sender)
{
// Insert a Tom Gordon with a Stats object
Stats* lclStats = new Stats(2);
tomGordonNodeInsert = TreeView1->
Items->InsertObject(timWakefieldNode,"Tom Gordon", lclStats);
ListBox1->Items->Add("Inserted Tom with a new stats object");
ListBox1->Items->Add("containing the number 2");
}
//--------------------------------------------------
void __fastcall TForm1::DeleteGordonClick(TObject *Sender)
{
if ( tomGordonNodeAdd != NULL )
{
// Retrieve Stats object from Data property
Stats* statsInstance = (Stats*) tomGordonNodeAdd->Data;
String number = statsInstance->getStats();
ListBox1->Items->Add("Stats contained a " + number);
// Delete Stats and then the Tom Gordon node.
delete statsInstance;
tomGordonNodeAdd->Data = (void*) NULL;
tomGordonNodeAdd->Delete();
ListBox1->Items->Add("Deleted the added Tom Gordon node.")
tomGordonNodeAdd = NULL;
}
if ( tomGordonNodeInsert != NULL )
{
// Retrieve Stats object from Data property
Stats* statsInstance = (Stats*) tomGordonNodeInsert->Data;
String number = statsInstance->getStats();
ListBox1->Items->Add("Stats contained a " + number);
// Delete Stats and then the Tom Gordon node.
delete statsInstance;
tomGordonNodeInsert->Data = (void*) NULL;
tomGordonNodeInsert->Delete();
ListBox1->Items->
Add("Deleted the inserted Tom Gordon node.");
tomGordonNodeInsert = NULL;
}
}
//--------------------------------------------------
void __fastcall TForm1::AddVaughnClick(TObject *Sender)
{
// Add Vaughn as child to Boston's infielders. Test handle
// object with Vaughn. Adapted from calander example.
nodeIdHandle* moData = new nodeIdHandle;
nodeIdHandle* moDataOut;
nodeIdHandle* moDataIn;
moData->nodeType = ev_player;
moData->obj = NULL;
moVaughn = TreeView1->Items->AddChild(bostonInfieldersNode, "Mo Vaughn");
Stats* moStats = new Stats(3);
moVaughn->Data = (void*) moData;
moDataOut = (nodeIdHandle*) moVaughn->Data;
moDataOut->obj = (void*) moStats;
moStats = NULL;
ListBox1->Items->Add("Added Mo with a Stats value of 3.");
moDataIn = (nodeIdHandle*) moVaughn->Data;
if ( moDataIn->nodeType == ev_player )
ListBox1->Items->Add("Retrieved Mo -- he's a player");
moStats = (Stats*) moDataIn->obj;
String moVal = moStats->getStats();
ListBox1->Items->Add("Mo's stats value is a " + moVal);
}
//--------------------------------------------------
void __fastcall TForm1::DeleteChildrenClick(TObject *Sender)
{
// Delete all Boston pitchers
pitNode->DeleteChildren();
}
//--------------------------------------------------
void __fastcall TForm1::GetFirstRootClick(TObject *Sender)
{
// Show the name of the first root node
ListBox1->Items->Add("First root node is " +
TreeView1->Items->GetFirstNode()->Text);
}
//--------------------------------------------------
void __fastcall TForm1::ShowLevelClick(TObject *Sender)
{
// Show the level of the selected node
TTreeNode *fc = TreeView1->Selected;
if ( fc != NULL )
ListBox1->Items->Add("Node " + fc->Text + " is at level " +
fc->Level);
else
ListBox1->Items->Add("Nothing selected!");
}
//--------------------------------------------------
void __fastcall TForm1::WalkRootsClick(TObject *Sender)
{
// Get the first tree node (root)
TTreeNode* rootNode = TreeView1->Items->GetFirstNode();
while ( rootNode )
{
ListBox1->Items->Add(rootNode->Text); // Continue as long as there are siblings
rootNode = rootNode->getNextSibling();
}
}
//--------------------------------------------------
void __fastcall TForm1::WalkChildrenDownClick(TObject *Sender)
{
// Get the first AL East child (team)
TTreeNode* team = alEast->getFirstChild();
while ( team )
{
ListBox1->Items->Add(team->Text); // Continue as long as there are children
team = alEast->GetNextChild(team);
}
}
//--------------------------------------------------
void __fastcall TForm1::WalkChildrenUpClick(TObject *Sender)
{
// Get the last child node in the AL East (team)
TTreeNode* team = alEast->GetLastChild();
while ( team )
{
ListBox1->Items->Add(team->Text);
// Walk backwards as long as there are previous children
team = alEast->GetPrevChild(team);
}
}
//--------------------------------------------------
void __fastcall TForm1::WalkChildrenForClick(TObject *Sender)
{
// If AL East has children, add names to ListBox1
if ( alEast->HasChildren )
{
ListBox1->Items->Add("Here are the teams in the AL East:");
for ( int team = 0; team < alEast->Count; team++ )
ListBox1->Items->Add(" " + alEast->Item[team]->Text);
}
else
ListBox1->Items->Add("There are on AL East teams!");
}
//--------------------------------------------------
void __fastcall TForm1::ChildIndexClick(TObject *Sender)
{
// Get the index of the selected node
TTreeNode *fc = TreeView1->Selected;
if ( fc != NULL )
ListBox1->Items->Add("Child node index for " + fc->Text +
" is " + fc->Index);
else
ListBox1->Items->Add("Nothing selected!");
}
//--------------------------------------------------
void __fastcall TForm1::TestParentChildClick(TObject *Sender)
{
// Test out Parent, HasAsParent, and Index
TTreeNode* parent = bosNode->Parent;
bool isparent = bosNode->HasAsParent(alEast);
int bosPos = alEast->IndexOf(bosNode);
if ( isparent )
{
String bosLoc = bosPos;
ListBox1->Items->
Add("Checked to see if Boston's parent was");
ListBox1->Items->Add("AL East...it was! Its child index is "
+ bosLoc);
ListBox1->Items->Add("Boston's parent name via the Parent");
ListBox1->Items->Add("was " + parent->Text);
}
}
//--------------------------------------------------
void __fastcall TForm1::Button1Click(TObject *Sender)
{
// Empty the ListBox
ListBox1->Items->Clear();
}
//--------------------------------------------------
Listing B: Unit1.h//--------------------------------------------------
#ifndef Unit1H
#define Unit1H
//--------------------------------------------------
#include <Classes.hpp>
#include <Controls.hpp>
#include <StdCtrls.hpp>
#include <Forms.hpp>
#include <ComCtrls.hpp>
//--------------------------------------------------
class Stats
{
public:
Stats(int x) { s = x; }
~Stats() { }
int getStats(void) { return s; }
private:
int s;
};
//--------------------------------------------------
enum entryValue {ev_league = 0, ev_division, ev_team, ev_player};
struct nodeIdHandle
{
entryValue nodeType;
void* obj;
};
//--------------------------------------------------
class TForm1 : public TForm
{
__published: // IDE-managed Components
TTreeView *TreeView1;
TButton *CreateFigA;
TButton *AddTomGordon;
TButton *InsertTomGordon;
TButton *AddVaughn;
TButton *DeleteGordon;
TButton *DeleteChildren;
TButton *GetFirstRoot;
TButton *WalkRoots;
TButton *WalkChildrenDown;
TButton *Quit;
TListBox *ListBox1;
TButton *WalkChildrenUp;
TButton *WalkChildrenFor;
TButton *ChildIndex;
TButton *TestParentChild;
TButton *ShowLevel;
TButton *Button1;
void __fastcall CreateFigAClick(TObject *Sender);
void __fastcall QuitClick(TObject *Sender);
void __fastcall AddTomGordonClick(TObject *Sender);
void __fastcall InsertTomGordonClick(TObject *Sender);
void __fastcall DeleteGordonClick(TObject *Sender);
void __fastcall AddVaughnClick(TObject *Sender);
void __fastcall DeleteChildrenClick(TObject *Sender);
void __fastcall GetFirstRootClick(TObject *Sender);
void __fastcall ShowLevelClick(TObject *Sender);
void __fastcall WalkRootsClick(TObject *Sender);
void __fastcall WalkChildrenDownClick(TObject *Sender);
void __fastcall WalkChildrenUpClick(TObject *Sender);
void __fastcall WalkChildrenForClick(TObject *Sender);
void __fastcall ChildIndexClick(TObject *Sender);
void __fastcall TestParentChildClick(TObject *Sender);
void __fastcall Button1Click(TObject *Sender);
private: // User declarations
// Pointers will help find specific parts of TTreeView later.
TTreeNode *al, *alEast, *alCentral, *alWest, *nl, *nlEast,
*nlCentral, *nlWest, *bosNode, *pitNode, *pedroMartinezNode,
*timWakefieldNode, *tomGordonNodeAdd, *tomGordonNodeInsert,
*bostonInfieldersNode, *moVaughn;
Stats* stats;
public: // User declarations
__fastcall TForm1(TComponent* Owner);
};
//--------------------------------------------------
extern PACKAGE TForm1 *Form1;
//--------------------------------------------------
#endif