December 1998

Mastering the TTreeView component, part 2

by Bill Whitney

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.

The application

Figure A shows our sample application's main window; the complete code for the application appears in Listing A at the end of this article.

Figure A: Our TTreeView component displays baseball league information.
[ Figure A ]

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.

Clicking buttons to add nodes

Adding leagues to the TTreeView is as simple as adding a root node. Since your application will offer several ways to add leagues (button, drag and drop, or pop-up menu), you'll place the code in a separate AddLeague method that they can all use. The League button event handler and AddLeague method appear in sections 6 and 8 of Listing A.

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_league
you 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.

Responding to the pop-up menu

In Figure A, you can see the pop-up menu at runtime. It contains the same options as the buttons on the main form. To avoid code duplication, the menu events simply call the button event handlers. Sections 7, 15, 16, and 17 of Listing A contain a few examples.

Adding drag and drop

When adding drag-and-drop capabilities, you can reuse the Add methods you created for the buttons. You must, however, determine where the additions should take place based on where the TShapes are dropped on the TTreeView. Before you write the code, be sure to set the DragMode property to dmAutomatic for each of the shapes--this setting will let you drag them. If you've used drag and drop in the past, then you've probably noticed that the item you're dragging changes its appearance when you drag it over a valid target. This is accomplished in the OnDragOver event, which determines whether the control's OnDragDrop method knows how to deal with the object you're trying to drop on it. You'll implement TTreeView's OnDragOver event to determine whether it's appropriate to accept the dragged TShape object. Look at the code in section 4 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.

Drag and drop within the TTreeView

You can also drag and drop objects within the TTreeView to relocate them. For example, you might move a player from one team to another. Again, see section 4 of Listing A; the OnDragOver method accepts an item dragged from the TTreeView (be sure to change TTreeView's DragMode attribute to dmAutomatic). If you look at the OnDragDrop method for TreeView1 (called TreeView1DragDrop) in section 5 of Listing A, you'll see that the first Source check is for TShape. Toward the bottom of that method, however, you check for a TTreeView source. Once you determine that the source is a player and the target is a category, you call MovePlayer to relocate the player's node to the new team.

Deleting nodes

The application also allows you to delete a node. Again, using the tests for node level, you can determine whether you should follow through on the request. You wouldn't, for example, let a user delete the categories listed below a team--they're static members (children) of the team mode. Section 12 of Listing A contains the delete routine that allows the user to delete all nodes with the exception of player categories.

Saving and restoring a TTreeView

The TTreeView component comes with methods to save TTreeView nodes to a file (SaveToFile) and reload them again later (LoadFromFile). Each method takes an AnsiString identifying the name of the file. Here's how they would look in your program:
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;
    }
  }
}

Conclusion

TTreeView is a powerful visual component. Unfortunately, it's often difficult to work with. In this article, we've given you some tools that will help you take advantage of some of the TTreeView's capabilities.