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

7.9k Views Asked by At

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;
2

There are 2 best solutions below

7
On BEST ANSWER

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.

1
On

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.

You have to do that because you are mixing levels.

Your tree is set up to expect object instances at every level. But you instance list only has objects at the main level and object properties at the sub level.

To all intents and purposes there is nothing wrong with that.

If you really want to avoid it, you would have to work with a composite pattern where an object owns a list of attributes which themselves are objects again. Would work, but would also make working with your classes a heck of lot messier. Though you could always provide access to the owned attributes by using indexed properties.

TSomeObject = class(TObject)
private
  MyObjects: TList<TSomeObject>;
protected
  function GetStringProperty(const aIndex: Integer): string;
public
  property Name: string index 1 read GetStringProperty;
end;

This is known as an Entity Attribute Value model. ( The EAV/CR Model of Data Representation ) It provides flexibility but does have drawbacks, especially if you were to structure your database in a similar manner.