树状数据结构(用于VirtualTreeview)

时间:2011-03-19 22:29:42

标签: delphi data-structures tree virtualtreeview

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

首先,这个问题是关于“我如何建立基础数据结构”。 :)

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

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

然而,当循环我的条目(现在)时,结果取决于节点的Checkstate - 如果我使用的是基础数据结构,我怎么知道我的节点是否被检查,何时我的数据结构我循环,而不是我的节点?

假设我想使用2个级别。

这将是父母:

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

孩子:

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

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

我希望你理解我的问题。 :)

谢谢!

6 个答案:

答案 0 :(得分:8)

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


尽管我建议使用更简单的TTreeView我将提出我对数据结构问题的看法。首先,我将使用,而不是记录。在非常简短的代码示例中,您将以非常不完美的方式混合记录和类:当您复制TRoot记录时(分配记录会生成完整的副本,因为记录总是被视为“值”),你没有对树进行“深层复制”:TRoot的完整副本将包含与原始版本相同的Kids:TList,因为与记录不同的类是引用:你正在应对参考值。

当您拥有包含对象字段的记录时,另一个问题是生命周期管理:记录没有析构函数,因此您需要其他机制来释放拥有的对象({ {1}})。您可以将Kids:TList替换为TList但是在传递怪物记录时您需要小心非常,因为您可能会结束制作大量记录的深层副本当你最不期望的时候。

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

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

array of Tkid

如果此类用作Root,则需要一种存储Kids的方法。我假设您使用的是Delphi 2010+,因此您拥有泛型。这个类有一个列表,如下所示:

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

您可能没有立即意识到这一点,但仅此类就足以实现多级树!这里有一些代码用一些数据填充树:

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); 类型的节点,但在现实世界中,您可能拥有类型TNodeTContactTContactCategory的节点,所有节点都在一个VT中。您将使用TRecentCall运算符来检查VT节点中对象的实际类型,如下所示:

is

以下是将VirtualNode指针存储到节点实例的示例:

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;

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

  • 您可以将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,这样您就可以检查虚拟树中的“已检查”节点。这将是数据G​​UI分离的桥梁。

答案 1 :(得分:4)

我问了类似的问题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中。然后,您可以始终将基础结构保存(并加载)为流或文件。希望这会有所帮助。

答案 2 :(得分:4)

我相信通过查找包含常规树实现的现有库,您可以获得最佳服务,然后可以重新使用它来满足您的需求。

为了让你知道为什么,这里有一些我编写的代码来说明可以想象的最简单的树结构上最简单的操作。

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。

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

答案 3 :(得分:2)

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

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/

答案 4 :(得分:0)

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

答案 5 :(得分:-1)

Delphi如今拥有泛型。我刚刚发明了一个非常漂亮的树数据结构。暂时还不会放弃代码,不是真正的开源人,也许在不久的将来,还有其他原因请参见下文。

但是我将提供一些有关如何重新创建它的提示:

假设所有节点都可以包含相同的数据结构(从上面看来,字符串,id和链接)。

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

  1. 泛型
  2. 通用类型T
  3. 此类型T需要按以下方式限制在类和构造函数中:

(在您的情况下,用记录替换类,未经测试,但也可以使用)

  1. 两个字段:self(提示)的节点数组,data:T;
  2. 财产

  3. 不仅是属性,还是默认属性;)

  4. 吸气剂。

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

  6. 某些if语句可停止构建。
  7. 当然还有SetLength来创建链接/节点,并在for循环中调用某些create,然后对某些对象进行减法;)

给大家足够的提示,看看是否有人可以重新创建它会很有趣,否则我可能会像申请专利一样,只是开个玩笑,而不是向它扔钱,尽管可以其他设施。

该类在构造过程中会像真正的数据结构一样分配所有节点...请注意add和remove等,至少现在不是。

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

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

现在您可以从该 actual 测试代码中注意到的是,它允许“数组扩展”,就像我很少见到的归功于此属性,该属性可以自我引用...

所以[] []是深度2。 [] [] []的深度为3。

我仍在评估此方法的使用。

一个潜在的问题是Delphi没有自动扩展这些数组的真正技术,尽管我还没有发现并已对其进行统计。

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

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

不确定该怎么做... simpelst选项是“递归”。

真实示例:

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岁时,我有点想开源。距离40 = D还有4个月

(我必须说这是我第一次对Generics印象深刻,很久以前就对其进行了测试,那是当时的超级bug,也许在设计上无法使用,但是现在由于bug已修复并受约束,generics非常令人印象深刻。最新的Delphi Toyko 10.2.3版本于2018年8月发布!;):))

我只是在探讨最新的Delphi技术不可能做到的事情,也许是使用匿名方法编写递归例程来处理此数据结构可能变得容易一些,也许还可以考虑并行处理,Delphi帮助提到了这一点匿名方法。

再见,   Skybuck。