Tree-like Datastructure (for use with VirtualTreeview)

前端 未结 6 469
遥遥无期
遥遥无期 2020-12-15 14:20

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 al

相关标签:
6条回答
  • 2020-12-15 15:01

    I believe you will be best served by finding an existing library containing a general tree implementation which you can then re-use to serve your needs.

    To give you an idea why, here is some code I wrote to illustrate the most simple operation on the most simple tree structure imaginable.

    type
      TNode = class
        Parent: TNode;
        NextSibling: TNode;
        FirstChild: TNode;
      end;
    
      TTree = class
        Root: TNode;
        function AddNode(Parent: TNode): TNode;
      end;
    
    function TTree.AddNode(Parent: TNode);
    var
      Node: TNode;
    begin
      Result := TNode.Create;
    
      Result.Parent := Parent;
      Result.NextSibling := nil;
      Result.FirstChild := nil;
    
      //this may be the first node in the tree
      if not Assigned(Root) then begin
        Assert(not Assigned(Parent));
        Root := Result;
        exit;
      end;
    
      //this may be the first child of this parent
      if Assigned(Parent) and not Assigned(Parent.FirstChild) then begin
        Parent.FirstChild := Result;
      end;
    
      //find the previous sibling and assign its next sibling to the new node
      if Assigned(Parent) then begin
        Node := Parent.FirstChild;
      end else begin
        Node := Root;
      end;
      if Assigned(Node) then begin
        while Assigned(Node.NextSibling) do begin
          Node := Node.NextSibling;
        end;
        Node.NextSibling := Result;
      end;
    end;
    

    Note: I have not tested this code and so cannot vouch for its correctness. I expect it has defects.

    All this does is add an new node to the tree. It gives you little control over where in the tree the node is added. If simply adds a new node as the last sibling of a specified parent node.

    To take this sort of approach you would likely need to deal with:

    • Inserting after a specified sibling. Actually this is quite a simple variant of the above.
    • Removing a node. This is somewhat more complex.
    • Moving existing nodes within the tree.
    • Walking the tree.
    • Connecting the tree to your VST.

    It's certainly feasible to do this, but you may be better advised to find a 3rd party library that already implements the functionality.

    0 讨论(0)
  • 2020-12-15 15:09

    Delphi has generics nowadays. I just invented a very nice tree data structure. Not gonna give code away just yet, not really an open source person, maybe in the near future though, also other reasons see below.

    But I will give some hints on how to re-create it:

    Assuming all your nodes can contain the same data structure (which seems to be the case from above, a string, an id, and then links.

    The ingredients you need to re-create this is the following:

    1. Generics
    2. A generic type T
    3. This type T needs to be constrained to class and constructor as follows:

    (In your case replace class with record, untested, but may also work)

    1. two fields: node array of self (hint hint), data : T;
    2. A property

    3. Not just any propery, a default property ;)

    4. A getter.

    5. A recursive constructor with depth and child.

    6. Some if statement to stop the construction.
    7. And ofcourse SetLength to create the links/nodes and calling some creates in a for loop and then some subtraction of something ;)

    Given all of you enough hints, would be fun and interesting to see if anybody can re-create it, otherwise I might just as well patent it, just kidding, not gonna throw money against it, might expand the class though with other facilities.

    The class allocates all nodes during construction like a true data structure... noting with add and remove and such, at least not for now.

    Now comes the most interesting and funny aspect of this (secret) design, something I kinda wanted and is now a reality. I can now write code as follows:

    TGroup is just an example can be anything as long as it's a class in my case. In this case it's just a class with mString

    var
      mGroupTree : TTree<TGroup>;
    
    procedure Main;
    var
      Depth : integer;
      Childs : integer;
    begin
    
      Depth := 2;
      Childs := 3;
    
      mGroupTree := TTree<TGroup>.Create( Depth, Childs );
    
      mGroupTree.Data.mString := 'Basket'; // notice how nice this root is ! ;)
    
      mGroupTree[0].Data.mString := 'Apples';
      mGroupTree[1].Data.mString := 'Oranges';
      mGroupTree[2].Data.mString := 'Bananas';
    
      mGroupTree[0][0].Data.mString := 'Bad apple';
      mGroupTree[0][1].Data.mString := 'Average apple';
      mGroupTree[0][2].Data.mString := 'Good apple';
    
      mGroupTree[1][0].Data.mString := 'Tiny orange';
      mGroupTree[1][1].Data.mString := 'Medium orange';
      mGroupTree[1][2].Data.mString := 'Big orange';
    
      mGroupTree[2][0].Data.mString := 'Straight banana';
      mGroupTree[2][1].Data.mString := 'Curved banana';
      mGroupTree[2][2].Data.mString := 'Crooked banana';
    

    Now what you may notice from this actual test code is that it allows "array expansion" like I have rarely seen thanks to this property, which self-references sort of...

    So [] [] is depth 2. [][][] would be depth 3.

    I am still evaluating the use of this.

    One potential problem is Delphi has no real technique to auto-expand these arrays, though none I have yet found and are statisfied with.

    I would like a technique where I can write some code which can go to any depth level:

    [0][0][0][0][0]

    Not yet sure how to do that... simpelst option is "recursion".

    real example:

    procedure DisplayString( Depth : string; ParaTree : TTree<TGroup>);
    var
      vIndex : integer;
    begin
      if ParaTree <> nil then
      begin
    //    if ParaTree.Data.mString <> '' then
        begin
          writeln( ParaTree.Data.mString );
    
          Depth := Depth + ' ';
          for vIndex := 0 to ParaTree.Childs-1 do
          begin
            DisplayString( Depth, ParaTree[vIndex] );
          end;
        end;
      end;
    end;
    

    Kinda interesting isn't it.

    Still exploring it's usefullness for "real applications" and if I want to go with recursion or not ;)

    Maybe some day I will open source all of my code. I am close to 40 years old, when I go beyond 40, from 39 to 40, I was kinda planning on going open source. Still 4 months away from 40 =D

    (I must say this is the first time I am impressed by Generics, tested it long ago, it was super buggy back then and maybe design-wise unusable, but now with the bugs fixed and constrained generics, it's very impressive in latest Delphi Toyko 10.2.3 version august 2018 ! ;) :))

    I am just scratching the surface of what is impossible with latest Delphi tech, maybe with anonymous methods writing recursive routines to process this data structure might become a bit easier, also maybe parallel processing might come into consideration, Delphi help mentions this for anonymous methods.

    Bye, Skybuck.

    0 讨论(0)
  • 2020-12-15 15:13

    I asked similar question here. I didn't got any useful answers so I decide to make my own implementation, which you can find here.

    EDIT: I'll try to post example how you could use my data structure:

    uses
      svCollections.GenericTrees;
    

    Declare your data type:

    type
      TMainData = record
        Name: string;
        ID: Integer;
      end;
    

    Declare your main data tree object somewhere in your code:

    MyTree: TSVTree<TMainData>;
    

    Create it (and do not forget to free later):

    MyTree: TSVTree<TMainData>.Create(False);
    

    Assign your VirtualStringTree to our data structure:

    MyTree.VirtualTree := VST;
    

    Then you can init your data tree with some values:

    procedure TForm1.BuildStructure(Count: Integer);
    var
      i, j: Integer;
      svNode, svNode2: TSVTreeNode<TMainData>;
      Data: TMainData;
    begin
      MyTree.BeginUpdate;
      try
        for i := 0 to Count - 1 do
        begin
          Data.Name := Format('Root %D', [i]);
          Data.ID := i;
          svNode := MyTree.AddChild(nil, Data);
          for j:= 0 to 10 - 1 do
          begin
            Data.Name := Format('Child %D', [j]);
            Data.ID := j;
            svNode2 := MyTree.AddChild(svNode, Data);
          end;
        end;
      finally
        MyTree.EndUpdate;
      end;
    end;
    

    And set VST events to display your data:

    procedure TForm1.vt1InitChildren(Sender: TBaseVirtualTree; Node: PVirtualNode;
      var ChildCount: Cardinal);
    var
      svNode: TSVTreeNode<TMainData>;
    begin
      svNode := MyTree.GetNode(Sender.GenerateIndex(Node));
      if Assigned(svNode) then
      begin
        ChildCount := svNode.FChildCount;
      end;
    end;
    
    procedure TForm1.vt1InitNode(Sender: TBaseVirtualTree; ParentNode,
      Node: PVirtualNode; var InitialStates: TVirtualNodeInitStates);
    var
      svNode: TSVTreeNode<TMainData>;
    begin
      svNode := MyTree.GetNode(Sender.GenerateIndex(Node));
      if Assigned(svNode) then
      begin
        //if TSVTree<TTestas> is synced with Virtual Treeview and we are building tree by
        // setting RootNodeCount, then we must set svNode.FVirtualNode := Node to
        // have correct node references
        svNode.FVirtualNode := Node;  // Don't Forget!!!!
        if svNode.HasChildren then
        begin
          Include(InitialStates, ivsHasChildren);
        end;
      end;
    end;
    
    //display info how you like, I simply get name and ID values
    procedure TForm1.vt1GetText(Sender: TBaseVirtualTree; Node: PVirtualNode;
      Column: TColumnIndex; TextType: TVSTTextType; var CellText: string);
    var
      svNode: TSVTreeNode<TMainData>;
    begin
      svNode := MyTree.GetNode(Sender.GenerateIndex(Node));
      if Assigned(svNode) then
      begin
        CellText := Format('%S ID:%D',[svNode.FValue.Name, svNode.FValue.ID]);
      end;
    end;
    

    At this point you work only with your MyTree data structure and all the changes made to it will be reflected in your assigned VST. You then can always save (and load) underlying structure to stream or file. Hope this helps.

    0 讨论(0)
  • 2020-12-15 15:14

    If you are using recent versions of Delphi that supports Generics, check GenericTree

    0 讨论(0)
  • 2020-12-15 15:16

    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:

    • You can turn the Name:string into a virtual method GetText:string;virtual and then create specialized descendants of TNode that override GetText to provide specialized behavior.
    • Create a TNode.AddPath(Path:string; ID:Integer) that allows you to do Root.AddPath('Contacts\Abraham', 1); - that is, a method that automatically creates all intermediary nodes to the final node, to allow easy creation of the tree.
    • Include an PVirtualNode into TNode itself so you can check rather the Node is "checked" in the Virtual Tree. This would be a bridge of the data-GUI separation.
    0 讨论(0)
  • 2020-12-15 15:19

    If I understand correctly, you need a datastructure for your tree. Each individual node requires a record to hold its data. But the underlying heirarchy can be managed in a few different ways. Im guessing this is all to be managed in some sort of database - This has already been talked about on this site, so i will point you to:

    Implementing a hierarchical data structure in a database

    and here:

    What is the most efficient/elegant way to parse a flat table into a tree?

    and here:

    SQL - How to store and navigate hierarchies?

    Nested Set Model:

    http://mikehillyer.com/articles/managing-hierarchical-data-in-mysql/

    0 讨论(0)
提交回复
热议问题