【问题标题】:Tree-like Datastructure (for use with VirtualTreeview)树状数据结构(用于 VirtualTreeview)
【发布时间】:2011-07-18 22:22:49
【问题描述】:

我已经到了需要停止将数据存储在 VCL 组件中的地步,并拥有一个“基础数据结构”,如 Mr. Rob Kennedy suggested.

首先,这个问题是关于“我如何制作底层数据结构”。 :)

我的层次结构由 2 级节点组成。

现在,我通过循环根节点来遍历我的东西,其中我循环遍历根节点的子节点,以获得我需要的(数据)。我希望能够将我的所有数据存储在所谓的基础数据结构中,以便我可以使用线程轻松修改条目(我想我可以做到吗?)

但是,当循环遍历我的条目时(现在),结果取决于节点的 Checkstate - 如果我使用的是底层数据结构,我如何知道我的节点是否被检查,当它的数据结构我循环,而不是我的节点?

假设我想使用 2 个级别。

这将是父母:

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

还有孩子:

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

这基本上就是我现在所做的。评论指出这不是最好的解决方案,所以我愿意接受建议。 :)

希望您能理解我的问题。 :)

谢谢!

【问题讨论】:

  • @Jeff,恕我直言,您的问题对于没有阅读您之前的问题的人来说是无法理解的,因为您没有提供链接(作为最后的手段),但如果您最好写下每个问题,就好像它是您在此处发布的唯一问题一样。这样,任何人都可以轻松回答问题,但最重要的是,未来的读者也可以理解。
  • @jach - 很高兴你告诉我,我会尝试找到以前的帖子。 :)
  • @Jeff:没有冒犯,但如果我做对了,你就是一个初学者程序员——但如果你继续学习,你将(几年后)掌握更复杂的编程等级。然而,在那之前,我认为当有更简单的选择时不要尝试太努力是明智之举。你真的确定你不能使用TListBox吗?我的意思是,如果您需要更高级的控件来显示您的数据,例如 Virtual TreeView,您应该首先拥有一些“底层数据结构”中的数据。
  • 这个问题看起来很有用:stackoverflow.com/questions/1841621
  • @Jeff,即使您确实得到了有效的答案,立即接受答案也不符合您的最佳利益,也不符合社区的最佳利益。除非问题很简单,例如,您应该等待一两天才能接受答案:也许会出现更好的答案!事实上,这种问题可以从多个答案中受益。例如,我今天早上看到了这个问题,没有时间处理它,当我回来时,我看到了一个建议使用数据库的答案的刻度线。我讨厌人们推荐数据库,就好像它们是万能药一样。

标签: delphi data-structures tree virtualtreeview


【解决方案1】:

您请求的数据结构非常简单,我建议使用 windows 提供的 TTreeView:它允许将文本和 ID 直接存储到树的节点中,而无需额外工作。


尽管我建议使用更简单的TTreeView,但我将提供我对数据结构问题的看法。首先,我将使用 classes,而不是 records。在您非常短的代码示例中,您正在以一种非常不幸的方式混合记录和类:当您复制 TRoot 记录时(分配记录会生成完整的副本,因为记录总是被视为“值”),您'不要制作树的“深拷贝”:TRoot 的完整副本将包含与原始相同的 Kids:TList,因为类与记录不同,是引用:您正在处理引用的值。

当您拥有带有对象字段的记录时,另一个问题是生命周期管理:记录没有析构函数,因此您需要其他机制来释放拥有的对象(@ 987654326@)。您可以将TList 替换为array of Tkid,但是在传递怪物记录时您需要非常小心,因为您可能会在您最不希望的时候结束对大量记录的深度复制它。

在我看来,最谨慎的做法是将数据结构基于,而不是记录:类实例(对象)作为引用传递,因此您可以随意移动它们想要没有问题。您还可以获得内置的生命周期管理(析构函数

基类看起来像这样。您会注意到它可以用作 Root 或 Kid,因为 Root 和 Kid 共享数据:两者都有名称和 ID:

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

如果这个类被用作一个根,它需要一种方法来存储孩子。我假设您使用的是 Delphi 2010+,所以您有泛型。这个类,带有一个列表,看起来像这样:

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;

您可能不会立即意识到这一点,但仅这个类就足以实现多级树!下面是一些用一些数据填充树的代码:

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

您需要一个递归过程来使用这种类型填充虚拟树视图:

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

使用对象时,虚拟树中的不同节点可能有不同类型的对象关联。在我们的示例中,我们仅添加 TNode 类型的节点,但在现实世界中,您可能拥有 TContactTContactCategoryTRecentCall 类型的节点,所有这些都在一个 VT 中。您将使用 is 运算符来检查 VT 节点中对象的实际类型,如下所示:

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;

下面是一个为什么将 VirtualNode 指针存储到节点实例的示例:

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;

您知道有一个简单树数据结构的工作示例。您需要“增长”这个数据结构以满足您的需求:可能性是无穷无尽的!为您提供一些想法和探索方向:

  • 您可以将Name:string 转换为虚拟方法GetText:string;virtual,然后创建TNode 的专门后代,覆盖GetText 以提供专门的行为。
  • 创建一个TNode.AddPath(Path:string; ID:Integer),允许您执行Root.AddPath('Contacts\Abraham', 1); - 即自动创建所有中间节点到最终节点的方法,以便轻松创建树。
  • PVirtualNode 包含到TNode 本身中,这样您就可以在虚拟树中检查节点是否已“检查”。这将是数据-GUI 分离的桥梁。

【讨论】:

  • 首先,感谢您的回答!我回家后一定会试一试。但是,我在 Virtual Treeview 文档中读到,使用记录比使用类更好?我不知道我应该相信什么,因为我还是个新手。
  • @Jeff,如果您将数据存储在 treview 节点本身中,记录可能会更好。如果您将数据移动到不同的数据结构中,您将存储一个指针:为什么 VT 会关心您存储的是指向记录的指针还是指向对象实例的指针?
  • @Cosmin - 我一直在阅读指针 (about.com),但当我阅读 “如果将数据移动到不同的数据结构,您将存储一个指针”。另外,记录不是占用更少的内存吗?
  • @Jeff:与类似的类相比,记录可能会少占用几个字节的内存,但记录没有析构函数,并且通过值而不是引用传递。我以为我在回答中解释了这一点。在我看来,类更适合树数据结构。或者,您可以使用指针和记录实现自己的树。
  • @daemon, TNode 是一个指针,所以PNode = ^TNode 实际上是一个指向指针的指针,所以它们不都是“只是指针”。一个是指针,一个是双间接指针。在VTGetText 中使用它是错误的类型,特别是如果您希望有效负载是TNode 其他东西:更好的选择是PObject = ^TObject;
【解决方案2】:

我相信找到一个包含通用树实现的现有库将为您提供最好的服务,然后您可以重新使用它来满足您的需求。

为了让您了解原因,这里是我编写的一些代码,用于说明对可以想象的最简单树结构的最简单操作。

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;

注意:我没有测试过这段代码,所以不能保证它的正确性。我预计它有缺陷。

所有这一切都是在树中添加一个新节点。它使您几乎无法控制在树中添加节点的位置。如果只是添加一个新节点作为指定父节点的最后一个兄弟节点。

要采用这种方法,您可能需要处理:

  • 在指定的同级之后插入。实际上,这是上述方法的一个非常简单的变体。
  • 删除节点。这有点复杂。
  • 在树中移动现有节点。
  • 走树。
  • 将树连接到您的 VST。

这样做当然是可行的,但建议您找到已经实现该功能的 3rd 方库。

【讨论】:

    【解决方案3】:

    我问过类似的问题here。我没有得到任何有用的答案,所以我决定自己实现,你可以找到here

    编辑: 我将尝试发布如何使用我的数据结构的示例:

    uses
      svCollections.GenericTrees;
    

    声明你的数据类型:

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

    在代码中的某处声明主数据树对象:

    MyTree: TSVTree<TMainData>;
    

    创建它(以后不要忘记释放):

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

    将您的 VirtualStringTree 分配给我们的数据结构:

    MyTree.VirtualTree := VST;
    

    然后你可以用一些值来初始化你的数据树:

    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;
    

    并设置 VST 事件以显示您的数据:

    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;
    

    此时您只使用您的 MyTree 数据结构,对其所做的所有更改都将反映在您分配的 VST 中。然后,您始终可以将底层结构保存(和加载)到流或文件中。希望这会有所帮助。

    【讨论】:

    • 我不知道在哪里可以下载它? :)
    • 我刚刚通过添加 1000 个根和 1000 个子节点来测试使用 VT 存储数据和 SVTree 之间的区别。事实证明,SVTree 比 VT 使用大约 100 兆,而 VT 大约快两倍。是因为我们使用的是指针而不是 VT 中的整个记录​​吗?
    • 会有一些额外的内存使用,因为 TSVTreeNode 对象是在构建数据结构时额外创建的。不知道您是如何测试的,但构建我的数据结构有点慢,因为它需要生成唯一节点的哈希,以便以后能够在 VT 事件中检索它。我发现它在体面的测试中表现良好,但如果您需要添加数百万个具有巨大层次结构的节点,您可以考虑其他一些方法。
    • 你的非常好用且易于使用,而且我想也是线程安全的?我可能会使用大约 20.000 个节点,但是内存使用已经是一个问题:P
    【解决方案4】:

    如果我理解正确,您的树需要一个数据结构。每个单独的节点都需要一条记录来保存其数据。但是可以通过几种不同的方式管理潜在的层次结构。我猜这一切都将在某种数据库中进行管理 - 这已经在这个网站上讨论过,所以我会指出你:

    Implementing a hierarchical data structure in a database

    这里:

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

    这里:

    SQL - How to store and navigate hierarchies?

    嵌套集模型:

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

    【讨论】:

    • 该问题与数据库或平面文件无关。
    • 这有点模棱两可——但他可以看到通过这些链接在(某些表/矩阵/数组等)中存储树结构的原理。
    【解决方案5】:

    如果您使用的是支持泛型的最新版本的 Delphi,请查看GenericTree

    【讨论】:

      【解决方案6】:

      Delphi 现在有泛型。我刚刚发明了一个非常好的树数据结构。暂时不会泄露代码,不是真正的开源人,也许在不久的将来,还有其他原因见下文。

      但我会给出一些关于如何重新创建它的提示:

      假设你的所有节点都可以包含相同的数据结构(从上面看似乎是这种情况,一个字符串,一个 id,然后是链接。

      您需要重新创建它的成分如下:

      1. 泛型
      2. 泛型类型 T
      3. 此类型 T 需要约束为类和构造函数,如下所示:

      (在您的情况下,将类替换为记录,未经测试,但也可能有效)

      1. 两个字段:自己的节点数组(提示提示),数据:T;
      2. 一个属性

      3. 不只是任何属性,一个默认属性;)

      4. 吸气剂。

      5. 具有深度和子级的递归构造函数。

      6. 一些 if 语句来停止构造。
      7. 当然还有 SetLength 来创建链接/节点并在 for 循环中调用一些创建,然后减去一些内容;)

      给你们足够多的提示,看看是否有人可以重新创建它会很有趣和有趣,否则我不妨申请专利,开个玩笑,不会花钱反对它,虽然可能会扩大课程其他设施。

      类在构造过程中分配所有节点,就像一个真正的数据结构......注意添加和删除等,至少现在不是。

      现在是这个(秘密)设计最有趣和最有趣的方面,我有点想要,现在已经成为现实。我现在可以编写如下代码:

      TGroup 只是一个例子,只要在我的例子中是一个类,它就可以是任何东西。 在这种情况下,它只是一个带有 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';
      

      现在您可能会从这个 实际 测试代码中注意到,它允许像我很少看到的那样“数组扩展”,这要归功于这个属性,它的自引用有点...

      所以 [] [] 是深度 2。 [][][] 将是深度 3。

      我仍在评估它的用途。

      一个潜在的问题是 Delphi 没有真正的技术来自动扩展这些数组,尽管我还没有找到并对此进行了统计。

      我想要一种技术,我可以编写一些可以进入任何深度级别的代码:

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

      还不知道怎么做...最简单的选项是“递归”。

      真实的例子:

      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;
      

      是不是很有趣。

      仍在探索它对“实际应用程序”的有用性以及我是否想使用递归;)

      也许有一天我会开源我的所有代码。我快 40 岁了,当我超过 40 岁,从 39 岁到 40 岁时,我有点打算开源。距离40还有4个月=D

      (我必须说这是我第一次对泛型印象深刻,很久以前测试过,当时它有超级错误,可能在设计方面无法使用,但现在修复了错误并限制了泛型,它在2018 年 8 月最新的 Delphi Toyko 10.2.3 版本!;) :))

      我只是触及了最新 Delphi 技术不可能实现的表面,也许使用匿名方法编写递归例程来处理此数据结构可能会变得更容易一些,也可能会考虑并行处理,Delphi 帮助提到了这一点匿名方法。

      再见, 斯凯巴克。

      【讨论】:

        猜你喜欢
        • 2014-04-06
        • 1970-01-01
        • 1970-01-01
        • 1970-01-01
        • 1970-01-01
        • 1970-01-01
        • 1970-01-01
        • 2023-04-02
        • 2010-10-30
        相关资源
        最近更新 更多