In the November issue of C++Builder Developer's Journal, we showed you how to build a TTreeView component programmatically and gave you some tips for runtime navigation. This month, we'll create a similar application, but we'll start with an empty TTreeView and write the code to respond to user-generated events--including drag and drop. Finally, we'll demonstrate how you can save the information in a TTreeView so that it can be retrieved when your program runs again later.
Figure A: Our TTreeView component displays baseball league information.
The TTreeView component can contain leagues, divisions, teams, and baseball players by category (pitcher, infield, catcher, and outfield). As was true in last month's example, the root nodes represent leagues. The child of any league is a division, the child of any division is a team, the child of any team is a player category, and the child of any player category is a player.
The goal is to let the user create nodes and assign names to them. You'll implement code to build the TTreeView by responding to clicks, drag-and-drop operations, and pop-up menus.
In Figure A, you can see buttons for adding leagues, divisions, teams, and players. You'll be able to select an existing node in the TTreeView (by clicking on it with the mouse) and then click one of the buttons to add or delete an item.
The colored squares (TShape objects) to the left of the buttons will demonstrate drag-and-drop operations. Each square represents the drag-and-drop equivalent of its adjacent button; you can drag it into the TTreeView to create a new node. We'll show you not only how to tell where the square was dropped, but also how to determine whether the operation is valid within the context of the application.
Within AddLeague, you should set the ActiveControl to TreeView1. If you don't, then the focus will remain on the button you just clicked (you can comment out this line to see what happens). The Add method adds a new root node (remember from part 1 of this article series that the NULL argument creates a root node). You then call the new node's EditText() method to highlight the new node's text and place the user in edit mode.
After the user edits the name and presses [Enter], the TTreeView's default behavior sets the topmost root node to the selected node. This doesn't really look right, nor is it convenient if you want to take further action on the new node. Fortunately, an Edited event is generated when any node name is updated. By adding a single line of code to this event, you can guarantee that the new node remains selected (see section 13 of Listing A.)
Adding divisions is equally simple--you just need to be sure the user has clicked on an existing league. The AddDivision and DivisionButtonClick methods are shown in sections 9 and 14 of Listing A.
Before you call AddDivision, you check for a selected node and then make sure the selected node is at the league level. Because you know the exact structure of the TTreeView, you know for sure which entries are allowed at each level. The ev_league parameter is defined in Unit1.h, as shown in section 1 of Listing A.
If your TTreeView isn't so rigidly structured, then you can't always rely on a node's level to determine its contents. In these cases, you can place a handle in the void* attribute of each node. As you'll recall from part 1, it's possible to point the void* to any object or structure. The nodeHandle structure in section 2 of Listing A contains the node's type (nodeType) and still lets the node point to any void* object (through nodeHandle's obj attribute).
To use node handles, you need to point each node's Data attribute to a valid nodeHandle structure with an appropriate nodeType value. (Part 1 provided an example of how to use handles.)
Once you're sure a league is selected--that is, when
TreeView1->Selected->Level == ev_leagueyou call AddDivision with a temporary new name and the selected league's node pointer. AddDivision then ensures that TreeView1 has focus and calls the AddChild method with the new division name and parent node (league) pointer. You should also call Expanded(true) on the league node to ensure that the new division is visible--otherwise, the node won't show up when the EditText call is made.
Next, you'll add a team. This is a bit more involved, because you must add the category nodes automatically when a new team is created. By now, you can guess the button event-handler code. The AddTeam code appears in section 10 of Listing A.
As usual, you set the active control and add the team node to the selected division. You then perform the expand prior to adding the categories, so that you end up with the team name preceded by a plus sign (+)--an indicator that the node has children (categories, in this case). The categories are "hidden" from view until a user clicks on the plus sign.
Adding players to the categories is as simple as selecting a category and
clicking the Player button. You must first be certain that an item is selected
at the category level--to see how to do so, check out the AddPlayer method in
section 11
of Listing A.
The TreeView1DragOver method is provided with some useful arguments. You can determine the type of object being dragged by looking at the value of Source--in this code, you check to see whether it's a TShape object. If it is, then you set Accept to true; this indicates that dropping the object here is a valid action. Setting Accept to true also changes the object's appearance to let the user know it can be dropped.
You can use the X and Y values to figure out exactly where you are in the TTreeView and determine whether a particular drop operation is allowed. For example, you can drop leagues, divisions, teams, and players onto the TTreeView (because it can contain them all). However, you shouldn't be able to drop a player on a league, division, or team--players must go in a category. You can impose this behavior in either OnDragOver or OnDragDrop.
The code in sections 5a and 5b of Listing A determines whether a new division request was dropped in the correct location. The GetNodeAt method retrieves the node under the cursor at the time of the drop. From the node's level (or by identification information stored in the handle pointed to by the Data attribute's void*), you can discern whether the operation should be allowed. You do this by checking to see if the source is a DivisionShape. Since you know that the only place you can drop a division is on a league, you'll check to see if the target node's Level is ev_league. If the drop is valid, you call the AddDivision method you wrote earlier.
The example program checks the validity of the drop in the OnDragDrop method. Essentially, the OnDragOver method accepts any TShape you drag onto the TTreeView. For practice, you can try moving the GetNodeAt(X, Y) method call into the OnDragOver method. If the target node isn't valid for the operation (dropping a player on a league, for instance), you can simply set Accept to false.
void __fastcall TForm1::SaveClick(
TObject *Sender)
{
TreeView1->SaveToFile("saved.ttv");
}
void __fastcall TForm1::LoadClick(
TObject *Sender)
{
TreeView1->LoadFromFile("saved.ttv");
}
The
only problem with SavetoFile and LoadFromFile is that when the information is
saved, nothing is preserved but the hierarchy and node names. If a node was
pointing to an object, that information is lost when the nodes are reloaded.
Unfortunately, there's no single solution to the problem--especially if a node
points to an object instance. Writing and reading objects from a file requires
special consideration and is beyond the scope of this article. If you must
attach something to the void* of a TTreeNode, it's best to use structures, for
the time being. If you need to use objects, copy the values of the object's
attributes to structure members and save the structures to disk, instead.Our example implementation works for this application. As you write the TTreeView to disk, you'll preserve information about each node, such as its level and name. If you wish to follow along, please refer to the SaveClick and LoadClick methods in sections 18 and 19 of Listing A.
The code captures each node's name, its level, and whether the node's data pointer points to something. You'll save this information in a structure called SaveNode, shown in section 3 of Listing A.
As you write nodes to disk, you walk the items in the TTreeView in their absolute order. You copy the name and level (stored in type) and set the hasStruct flag if you're using the Data attribute (void*). (Remember, the sample application is using the Data attribute only for player nodes--so, only those nodes will have this flag set.) You then write the structure to a file. If the node is a player, you write the structure the player is pointing to right behind the SaveNode structure. You continue this activity until there are no more nodes and then close the file.
Reading the nodes back in is a bit more complicated, because you must re-insert nodes into the correct levels in the TTreeView you're building. You must also check the hasStruct flag in SaveNode to be certain you read any player structures stored behind the player nodes. The while loop reads each saved node and figures out where it should be added; to do so, it uses an if/then structure adapted from the VCL Object Pascal source upon which LoadFromFile is based.
Listing A: Sample TTreeView application
//--------------------------------
#ifndef Unit1H
#define Unit1H
//--------------------------------
#include <Classes.hpp>
#include <Controls.hpp>
#include <StdCtrls.hpp>
#include <Forms.hpp>
#include <ComCtrls.hpp>
#include <ExtCtrls.hpp>
#include <Menus.hpp>
//--------------------------------
enum entryValue {ev_league = 0, ev_division,
ev_team, ev_categories, ev_player};
struct nodeHandle
{
entryValue nodeType;
void* obj;
};
struct PlayerStr
{
int number;
int age;
float weight;
int height;
};
struct SaveNode
{
char name[64];
int type;
int hasStruct;
};
//--------------------------------
class TForm1 : public TForm
{
__published: // IDE-managed Components
TTreeView *TreeView1;
TImageList *ImageList1;
TShape *DivisionShape;
TShape *LeagueShape;
TShape *TeamShape;
TShape *PlayerShape;
TButton *DeleteButton;
TButton *QuitButton;
TButton *LeagueButton;
TButton *DivisionButton;
TButton *TeamButton;
TButton *PlayerButton;
TPopupMenu *PopupMenu1;
TMenuItem *NewLeague1;
TMenuItem *Division1;
TMenuItem *Team1;
TMenuItem *Player1;
TMenuItem *Delete1;
TMenuItem *Quit1;
TButton *Load;
TButton *Save;
void __fastcall QuitButtonClick(Tobject *Sender);
void __fastcall TreeView1DragOver(TObject *Sender,
TObject *Source, int X, int Y, TDragState State,
bool &Accept);
void __fastcall TreeView1DragDrop(TObject *Sender, TObject
*Source, int X, int Y);
void __fastcall LeagueButtonClick(TObject *Sender);
void __fastcall NewLeague1Click(TObject *Sender);
void __fastcall DeleteButtonClick(TObject *Sender);
void __fastcall TreeView1Change(TObject *Sender,
TTreeNode *Node);
void __fastcall TreeView1Edited(TObject *Sender,
TTreeNode *Node, AnsiString &S);
void __fastcall DivisionButtonClick(TObject *Sender);
void __fastcall TeamButtonClick(TObject *Sender);
void __fastcall PlayerButtonClick(TObject *Sender);
void __fastcall Division1Click(TObject *Sender);
void __fastcall Team1Click(TObject *Sender);
void __fastcall Player1Click(TObject *Sender);
void __fastcall Delete1Click(TObject *Sender);
void __fastcall Quit1Click(TObject *Sender);
void __fastcall SaveClick(TObject *Sender);
void __fastcall LoadClick(TObject *Sender);
private: // User declarations
void AddLeague(char* name);
void AddDivision(char* name, TTreeNode* leagueNode);
void AddTeam(char* name, TTreeNode* divisionNode);
void AddPlayer(char* name, TTreeNode* categoryNode);
void MovePlayer(TTreeNode* player, TTreeNode* newTeam);
public: // User declarations
__fastcall TForm1(TComponent* Owner);
};
//--------------------------------
extern PACKAGE TForm1 *Form1;
//--------------------------------
#endif
//--------------------------------
#include <vcl.h>
#pragma hdrstop
#include <iostream>
#include <fstream>
#include "Unit1.h"
//--------------------------------
#pragma package(smart_init)
#pragma resource "*.dfm"
TForm1 *Form1;
//--------------------------------
__fastcall TForm1::TForm1(TComponent* Owner)
: TForm(Owner)
{
}
//--------------------------------
void __fastcall TForm1::QuitButtonClick(TObject *Sender)
{
Close();
}
//--------------------------------
void __fastcall TForm1::TreeView1DragOver(TObject *Sender,
TObject *Source, int X, int Y, TDragState State, bool &Accept)
{
if ( (Source->InheritsFrom(__classid(TShape))) ||
(Source->InheritsFrom(__classid(TTreeView))))
Accept = true;
else
Accept = false;
}
//--------------------------------
void __fastcall TForm1::TreeView1DragDrop(TObject *Sender,
TObject *Source, int X, int Y)
{
if (Source->InheritsFrom(__classid(TShape)))
{
TTreeNode *target = TreeView1>GetNodeAt(X, Y);
if ( Source == LeagueShape )
{
if ( target == NULL )
AddLeague("(New League)");
else
Application->MessageBox(
"Leagues must be dropped in dead space",
"OOPS!", MB_OK);
}
else if ( Source == DivisionShape )
{
if ( (target != NULL) && (target->Level ==
ev_league) )
AddDivision("(New Division)", target);
else
Application->MessageBox(
"Divisions must be dropped in Leagues",
"OOPS!", MB_OK);
}
else if ( Source == TeamShape )
{
if ( (target != NULL) && (target->Level ==
ev_division) )
AddTeam("(New Team)", target);
else
Application->MessageBox(
"Teams must be dropped in Divisions", "OOPS!", MB_OK);
}
else if ( Source == PlayerShape )
{
if ( (target != NULL) && (target->Level ==
ev_categories) )
AddPlayer("(New Player)", target);
else
Application->MessageBox(
"Players must be dropped in categories", "OOPS!", MB_OK);
}
}
else if (Source->InheritsFrom(__classid(TTreeView)))
{
TTreeNode *target = TreeView1->GetNodeAt(X, Y);
TTreeNode *source = (TTreeNode*) TreeView1->Selected;
if ( source->Level != ev_player )
{
Application->MessageBox(
"Only a player can be dragged and dropped",
"OOPS!", MB_OK);
}
else
{
if ( target->Level == ev_categories )
{
if ( (target != NULL) )
MovePlayer(source, target);
}
else
{
Application->MessageBox(
"A player can only be dropped on a category",
"OOPS!", MB_OK);
}
}
}
}
//--------------------------------
void TForm1::MovePlayer(TTreeNode* player,
TTreeNode* newTeam)
{
TTreeNode* n = TreeView1->Items->AddChild
(newTeam, "x");
n->Assign(player);
player->Delete();
}
//--------------------------------
void __fastcall TForm1::LeagueButtonClick
(TObject *Sender)
{
AddLeague("(New League)");
}
//--------------------------------
void __fastcall TForm1::NewLeague1Click
(TObject *Sender)
{
LeagueButtonClick(Sender);
}
//--------------------------------
void TForm1::AddLeague(char* name)
{
ActiveControl = TreeView1;
TTreeNode* n = TreeView1->Items->Add
(NULL, name);
n->EditText();
}
//--------------------------------
void TForm1::AddDivision(char* name, TTreeNode*
leagueNode)
{
ActiveControl = TreeView1;
TTreeNode* n = TreeView1->Items->AddChild
(leagueNode, name);
leagueNode->Expand(true);
n->EditText();
}
//--------------------------------
void TForm1::AddTeam(char* name, TTreeNode*
divisionNode)
{
ActiveControl = TreeView1;
TTreeNode* n = TreeView1->Items->AddChild
(divisionNode, name);
divisionNode->Expand(true);
TreeView1->Items->AddChild(n, "Pitchers");
TreeView1->Items->AddChild(n, "Catchers");
TreeView1->Items->AddChild(n, "Infielders");
TreeView1->Items->AddChild(n, "Outfielders");
n->EditText();
}
//--------------------------------
void TForm1::AddPlayer(char* name, TTreeNode* categoryNode)
{
ActiveControl = TreeView1;
TTreeNode *n = TreeView1->Items->AddChild
(categoryNode, name);
// Add a structure to the Data (void*)
// attribute of this node
PlayerStr *p = new PlayerStr;
p->number = 20;
p->age = 31;
p->weight = 210.0;
p->height = 74;
n->Data = (void*) p;
categoryNode->Expand(true);
n->EditText();
}
//--------------------------------
void __fastcall TForm1::DeleteButtonClick(TObject *Sender)
{
// Delete a selected node.
ActiveControl = TreeView1;
if ( TreeView1->Selected == NULL )
{
Application->MessageBox("You must select a node to delete",
"OOPS!", MB_OK);
}
else
{
switch ( TreeView1->Selected->Level )
{
case ev_categories :
Application->MessageBox("You can't delete a category!",
"OOPS!", MB_OK);
break;
default :
TreeView1->Items->Delete(TreeView1->Selected);
}
}
}
//--------------------------------
void __fastcall TForm1::TreeView1Edited(TObject *Sender,
TTreeNode *Node, AnsiString &S)
{
Node->Selected = true;
}
//--------------------------------
void __fastcall TForm1::DivisionButtonClick(TObject *Sender)
{
if ((TreeView1->Selected != NULL) &&
(TreeView1->Selected->Level == ev_league))
{
AddDivision("(New Division)", TreeView1->Selected);
}
else
{
Application->MessageBox("Divisions are added to Leagues!",
"OOPS!", MB_OK);
}
}
//--------------------------------
void __fastcall TForm1::TeamButtonClick(TObject *Sender)
{
if ( (TreeView1->Selected != NULL) &&
(TreeView1->Selected->Level == ev_division) )
{
AddTeam("(New Team)", TreeView1->Selected);
}
else
{
Application->MessageBox("Teams are added to Divisions!",
"OOPS!", MB_OK);
}
}
//--------------------------------
void __fastcall TForm1::PlayerButtonClick(TObject *Sender)
{
if ( (TreeView1->Selected != NULL) &&
(TreeView1->Selected->Level == ev_categories) )
{
AddPlayer("(Player Name)", TreeView1->Selected);
}
else
{
Application->MessageBox("Players are added to categories!",
"OOPS!", MB_OK);
}
}
//--------------------------------
void __fastcall TForm1::Division1Click(TObject *Sender)
{
DivisionButtonClick(Sender);
}
//--------------------------------
void __fastcall TForm1::Team1Click(TObject *Sender)
{
TeamButtonClick(Sender);
}
//--------------------------------
void __fastcall TForm1::Player1Click(TObject *Sender)
{
PlayerButtonClick(Sender);
}
//--------------------------------
void __fastcall TForm1::Delete1Click(TObject *Sender)
{
DeleteButtonClick(Sender);
}
//--------------------------------
void __fastcall TForm1::Quit1Click(TObject *Sender)
{
QuitButtonClick(Sender);
}
//--------------------------------
void __fastcall TForm1::SaveClick(TObject *Sender)
{
ofstream nodeFile;
TTreeNode* curNode;
SaveNode savNode;
PlayerStr plyStruct;
nodeFile.open("saved.ttv", ios::binary | ios::trunc);
for ( int a = 0; a < TreeView1->Items->Count; a++ )
{
curNode = TreeView1->Items->Item[a];
strcpy(savNode.name, curNode->Text.c_str());
savNode.type = curNode->Level;
if ( curNode->Level == ev_player )
savNode.hasStruct = 1;
else
savNode.hasStruct = 0;
nodeFile.write((unsigned char*)&savNode, sizeof
(SaveNode));
if ( curNode->Level == ev_player )
{
memcpy(&plyStruct, (PlayerStr*)curNode->Data,
sizeof(PlayerStr));
nodeFile.write((unsigned char*)
&plyStruct, sizeof(PlayerStr));
}
}
}
//--------------------------------
void __fastcall TForm1::LoadClick(TObject *Sender)
{
ifstream nodeFile;
TTreeNode* curNode;
TTreeNode* nxtNode;
SaveNode savNode;
PlayerStr plyStruct;
PlayerStr* pStr;
entryValue curLvl;
nodeFile.open("saved.ttv", ios::binary);
curNode = (TTreeNode*) NULL;
while (nodeFile.read((unsigned char*)
&savNode, sizeof(SaveNode)))
{
curLvl = savNode.type;
if ( curNode == NULL )
{
curNode = TreeView1->Items->AddChild
(NULL, savNode.name);
}
else if ( curNode->Level == curLvl )
{
curNode = TreeView1->Items->AddChild
(curNode->Parent,
savNode.name);
}
else if ( curNode->Level == (curLvl - 1) )
{
curNode = TreeView1->Items->AddChild
(curNode,
savNode.name);
}
else if ( curNode->Level > curLvl )
{
nxtNode = curNode->Parent;
while ( nxtNode->Level > curLvl )
{
nxtNode = nxtNode->Parent;
}
curNode = TreeView1->Items->AddChild
(nxtNode->Parent,
savNode.name);
}
if ( savNode.type == ev_player )
{
nodeFile.read((unsigned char*)
&plyStruct, sizeof(PlayerStr));
pStr = new PlayerStr;
pStr->number = plyStruct.number;
pStr->age = plyStruct.age;
pStr->weight = plyStruct.weight;
pStr->height = plyStruct.height;
curNode->Data = (void*) pStr;
}
}
}