VirtualStringTree - Correct way to add/handle subnodes/childnodes when using objects?

TheSteven picture TheSteven · Nov 4, 2011 · Viewed 7k times · Source

I am using Delphi2010 and trying to wrap my head around using VirtualStringTree.

I've been trying to get it to work with objects and was having no luck until I followed Philipp Frenzel's Virtual TreeView tutorial which I found on the soft-gems.net web site. What I've come up with so far works but I don't think I'm handling the subnodes (i.e. childnodes) correctly.

The only thing I've been able to get working is to link the whole object in again for each child and then just display the field I need - but it just feels so wrong.

Suggestions/feedback much appreciated.


I have list of objects that I'm trying to hookup with VirtualStringTree where I'm trying to achieve something like this where one of the fields will act as the label to the parent and the rest of the fields show up as childnodes.

  • Robert Lane
    • Male
    • 35
    • Los Angeles
    • Brunette
  • Jane Doe
    • Female
    • 19
    • Denver
    • Redhead

This how my class is set up.

type
  PTreeData = ^TTreeData;
  TTreeData = record
    FObject : TObject;
  end;

  TCustomerNode = class(TObject)
  private
    fName: string;
    fSex: string;
    fAge: integer;
    fHair: string;
    //...
  public
    property Name: string read fName write fName;
    //...
  end;

Once I populate the objects I add them to another class (CustomerObjectList) based off of TList which is mentioned below.

Here is where I connect VirtualStringTree with my object list

procedure TfrmMain.btnLoadDataClick(Sender: TObject);
var
  i, j : integer;
  CustomerDataObject: TCustomerNode;
  RootXNode, XNode: PVirtualNode;
  Data: PTreeData;
begin
  vstree.NodeDataSize := SizeOf( TTreeData );

  vstree.BeginUpdate;
  for i := 0 to CustomerObjectList.Count - 1 do
  begin
    CustomerDataObject := CustomerObjectList[i];

    //load data for parent node
    RootXNode := vstree.AddChild(nil);
    Data  := vstree.GetNodeData(RootXNode);
    Data^.FObject:= PINodeSource;

    //now add children for rest of fields
    //Isn't there a better way to do this?
    for j := 1 to NUMBERofFIELDS -1 do  //first field is label for parent so -1
    begin
      XNode := vstree.AddChild(RootXNode);
      Data  := vstree.GetNodeData(XNode);
      Data^.FObject:= PINodeSource;
    end;

  end;
  vstree.EndUpdate; 
end;    

procedure TfrmMain.vstreeGetText(Sender: TBaseVirtualTree; Node: PVirtualNode;
 Column: TColumnIndex; TextType: TVSTTextType; var CellText: string);
var
  Data : PTreeData;
begin
  Data := vstObjects.GetNodeData(Node);
  ////if Node.ChildCount  = 0 then //edit - oops typo!
  if Node.ChildCount  > 0 then
  begin
    CellText := TCustomerNode(Data.FObject).Name;
    exit;
  end;

  //handle childnodes
  case Node.Index of
    0:  CellText := TCustomerNode(Data.FObject).Sex;
    1:  CellText := IntToStr(TCustomerNode(Data.FObject).Age);
    2:  CellText := TCustomerNode(Data.FObject).Hair;
    3:  CellText := TCustomerNode(Data.FObject).City;
  end;  
end;

Answer

norgepaul picture norgepaul · Nov 5, 2011

You don't need to load all the data into the tree. You can use the 'virtualness' of the tree. Here's how I would do it.

Set the RootNodeCount of the tree to the number of records in CustomerObjectList:

vstree.RootNodeCount := CustomerObjectList.Count;

Then, in the OnInitChildren event, set the child count of level 0 nodes to the number of properties you want to display:

procedure TfrmMain.vstreeInitNode(Sender: TBaseVirtualTree; ParentNode, Node: PVirtualNode; var InitialStates: TVirtualNodeInitStates);
begin
  if Sender.GetNodeLevel(Node) = 0 then
  begin
    Sender.ChildCount[Node] := 4;

    // Comment this out if you don't want the nodes to be initially expanded
    Sender.Expanded[Node] := TRUE;
  end;
end;

Now, just retrieve the correct data in the OnGetText event.

procedure TfrmMain.vstreeGetText(Sender: TBaseVirtualTree;
  Node: PVirtualNode; Column: TColumnIndex; TextType: TVSTTextType;
  var CellText: string);
begin
  if Column <= 0 then
  begin
    if Sender.GetNodeLevel(Node) = 0 then
      CellText := CustomerObjectList[Node.Index].Name else
    if Sender.GetNodeLevel(Node) = 1 then
    begin
      case Node.Index of
        0: CellText := CustomerObjectList[Parent.Node.Index].Sex;
        1: CellText := CustomerObjectList[Parent.Node.Index].Age;
        2: CellText := CustomerObjectList[Parent.Node.Index].Hair;
        3: CellText := CustomerObjectList[Parent.Node.Index].City;
      end; // of case
     end;
end;

You'll probably want to add in some more range checking, just in case.

Hope this helps.