3

正如Rob Kennedy 先生所建议的那样,我已经到了需要停止将数据存储在 VCL 组件中并拥有“基础数据结构”的地步。

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

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

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

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

假设我想使用 2 个级别。

这将是父母:

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

还有孩子:

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

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

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

谢谢!

4

6 回答 6

9

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


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

当您拥有带有对象字段的记录时,另一个问题是生命周期管理:记录没有析构函数,因此您需要其他机制来释放拥有的对象(Kids:TList)。您可以TList用 an替换,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类型节点,但在现实世界中,您可能在一个 VT 中拥有TContactTContactCategory、 、 类型的节点。TRecentCall您将使用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);——即自动创建所有中间节点到最终节点的方法,以允许轻松创建树。
  • 将 an 包含PVirtualNodeTNode自身中,以便您可以检查节点是否在虚拟树中“检查”。这将是数据-GUI 分离的桥梁。
于 2011-03-20T19:39:38.373 回答
4

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

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

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 中。然后,您始终可以将底层结构保存(和加载)到流或文件中。希望这可以帮助。

于 2011-03-19T23:10:08.647 回答
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。

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

于 2011-03-20T17:17:07.420 回答
2

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

在数据库中实现分层数据结构

和这里:

将平面表解析为树的最有效/优雅的方法是什么?

和这里:

SQL - 如何存储和导航层次结构?

嵌套集模型:

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

于 2011-03-20T00:22:18.600 回答
0

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

于 2016-07-27T09:31:04.613 回答
-1

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

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

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

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

  1. 泛型
  2. 泛型类型 T
  3. 这种类型 T 需要被限制为类和构造函数,如下所示:

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

  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

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

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

再见,斯凯巴克。

于 2018-08-08T22:07:54.717 回答