delphidata-structurestreevirtualtreeview

Tree-like Datastructure (for use with VirtualTreeview)


I have come to the point where I need to stop storing my data in a VCL component, and have an "underlying datastructure", as Mr. Rob Kennedy suggested.

First of all, this question is about "how do I make an underlying datastructure". :)

My hierachy consists of 2 levels of nodes.

Right now, I go thru my stuff by looping rootnodes, wherein I loop thru the rootnode's childnodes, to get what I need (Data). I would love to be able to store all my data in a so-called Underlying Datastructure, so that I can easily modify the entries using threads (I suppose I am able to do that?)

However, when looping through my entries (right now), the results are depending on the node's Checkstate - if I am using an underlying data structure, how do I know if my node is checked or not, when its my datastructure I loop thru, and not my nodes?

Let's say I wanted to use 2 levels.

This would be the Parent:

TRoot = Record
  RootName : String;
  RootId : Integer;
  Kids : TList; //(of TKid)
End;

And the kid:

TKid = Record
  KidName : String;
  KidId : Integer;
End;

Thats basically what I do now. Comments state that this is not the best solution, so I am open to suggestions. :)

I hope you understand my question(s). :)

Thanks!


Solution

  • The data structure you're requesting is very simple, it's so simple I'd recommend using the windows-provided TTreeView: it allows storing the text and an ID straight into the tree's node with no additional work.


    Despite my recommendation to use the simpler TTreeView I'm going to provide my take on the data structure problem. First of all I'm going to use classes, not records. In your very short code sample you're mixing records and classes in a very unfrotunate way: When you make a copy of the TRoot record (assigning records makes complete copies, because records are allways treated as "values"), you're not making a "deep copy" of the tree: The complete copy of TRoot will contain the same Kids:TList as the original, because classes, unlike records, are references: you're coping the value of the reference.

    An other problem when you have a record with an object field is life cycle management: A record doesn't have an destructor so you'll need an other mechanism to free the owned object (Kids:TList). You could replace the TList with an array of Tkid but then you'll need to be very careful when passing the monster record around, because you might end making deep copies of huge records when you least expect it.

    In my opinion the most prudent thing to do is to base the data structure on classes, not records: class instances (objects) are passed around as references, so you can move them around all you want with no problems. You also get built-in life cycle management (the destructor)

    The base class would look like this. You'll notice it can be used as either the Root or the Kid, because both Root and Kid share data: The both have a name and an ID:

    TNodeClass = class
    public
      Name: string;
      ID: Integer;
    end;
    

    If this class is used as an Root, it needs a way to store the Kids. I assume you're on Delphi 2010+, so you have generics. This class, complete with a list, looks like this:

    type
      TNode = class
      public
        ID: integer;
        Name: string;
        VTNode: PVirtualNode;
        Sub: TObjectList<TNode>;
    
        constructor Create(aName: string = ''; anID: integer = 0);
        destructor Destroy; override;
      end;
    
    constructor TNode.Create(aName:string; anID: Integer);
    begin
      Name := aName;
      ID := anID;
    
      Sub := TObjectList<TNode>.Create;
    end;
    
    destructor TNode.Destroy;
    begin
      Sub.Free;
    end;
    

    You might not immediately realize this, but this class alone is enough to implement a multi-level tree! Here's some code to fill up the tree with some data:

    Root := TNode.Create;
    
    // Create the Contacts leaf
    Root.Sub.Add(TNode.Create('Contacts', -1));
    // Add some contacts
    Root.Sub[0].Sub.Add(TNode.Create('Abraham', 1));
    Root.Sub[0].Sub.Add(TNode.Create('Lincoln', 2));
    
    // Create the "Recent Calls" leaf
    Root.Sub.Add(TNode.Create('Recent Calls', -1));
    // Add some recent calls
    Root.Sub[1].Sub.Add(TNode.Create('+00 (000) 00.00.00', 3));
    Root.Sub[1].Sub.Add(TNode.Create('+00 (001) 12.34.56', 4));
    

    You need a recursive procedure to fill the virtual tree view using this type:

    procedure TForm1.AddNodestoTree(ParentNode: PVirtualNode; Node: TNode);
    var SubNode: TNode;
        ThisNode: PVirtualNode;
    
    begin
      ThisNode := VT.AddChild(ParentNode, Node); // This call adds a new TVirtualNode to the VT, and saves "Node" as the payload
    
      Node.VTNode := ThisNode; // Save the PVirtualNode for future reference. This is only an example,
                               // the same TNode might be registered multiple times in the same VT,
                               // so it would be associated with multiple PVirtualNode's.
    
      for SubNode in Node.Sub do
        AddNodestoTree(ThisNode, SubNode);
    end;
    
    // And start processing like this:
    VT.NodeDataSize := SizeOf(Pointer); // Make sure we specify the size of the node's payload.
                                        // A variable holding an object reference in Delphi is actually
                                        // a pointer, so the node needs enough space to hold 1 pointer.
    AddNodesToTree(nil, Root);
    

    When using objects, different nodes in your Virtual Tree may have different types of objects associated with them. In our example we're only adding nodes of TNode type, but in the real world you might have nodes of types TContact, TContactCategory, TRecentCall, all in one VT. You'll use the is operator to check the actual type of the object in the VT node like this:

    procedure TForm1.VTGetText(Sender: TBaseVirtualTree; Node: PVirtualNode;
      Column: TColumnIndex; TextType: TVSTTextType; var CellText: string);
    var PayloadObject:TObject;
        Node: TNode;
        Contact : TContact;      
        ContactCategory : TContactCategory;
    begin
      PayloadObject := TObject(VT.GetNodeData(Node)^); // Extract the payload of the node as a TObject so
                                                       // we can check it's type before proceeding.
      if not Assigned(PayloadObject) then
        CellText := 'Bug: Node payload not assigned'
      else if PayloadObject is TNode then
        begin
          Node := TNode(PayloadObject); // We know it's a TNode, assign it to the proper var so we can easily work with it
          CellText := Node.Name;
        end
      else if PayloadObject is TContact then
        begin
          Contact := TContact(PayloadObject);
          CellText := Contact.FirstName + ' ' + Contact.LastName + ' (' + Contact.PhoneNumber + ')';
        end
      else if PayloadObject is TContactCategory then
        begin
          ContactCategory := TContactCategory(PayloadObject);
          CellText := ContactCategory.CategoryName + ' (' + IntToStr(ContactCategory.Contacts.Count) + ' contacts)';
        end
      else
        CellText := 'Bug: don''t know how to extract CellText from ' + PayloadObject.ClassName;
    end;
    

    And here's an example why to store VirtualNode pointer to your node instances:

    procedure TForm1.ButtonModifyClick(Sender: TObject);
    begin
      Root.Sub[0].Sub[0].Name := 'Someone else'; // I'll modify the node itself
      VT.InvalidateNode(Root.Sub[0].Sub[0].VTNode); // and invalidate the tree; when displayed again, it will
                                                    // show the updated text.
    end;
    

    You know have an working example for a simple tree data structure. You'll need to "grow" this data structure to suite your needs: the possibilities are endless! To give you some ideas, directions to explore: