3

出于性能原因,我需要在不使用递归的情况下浏览树视图的项目。

TTreeview 提供了 GlobalCount 和 ItemByGlobalIndex 方法,但它只返回可见项目
我搜索了根类代码而没有找到所有节点的私有列表,FGlobalItems 似乎只保存需要渲染的项目

有没有办法顺序浏览树视图的所有项目(包括不可见和折叠的节点)?

这个问题适用于 Delphi XE3 / FM2

谢谢,

[编辑 2 月 3 日]
我接受了默认答案(不可能开箱即用),尽管我正在寻找一种方法来修补这方面的 firemonkey 树视图。
经过更多的分析,我发现 FGlobalItems 列表只保存扩展项,并在方法 TCustomTreeView.UpdateGlobalIndexes 中维护;
评论 FMX.TreeView 的第 924 行(如果 AItem.IsExpanded 则...)会导致构建节点的完整索引,并允许使用 ItemByGlobalIndex() 按顺序浏览所有节点,但可能会导致其他性能问题和错误...
没有更多线索,我将保留我的递归代码。

4

6 回答 6

3

这个问题本质上是问如何在没有递归的情况下遍历一棵树。遍历一棵树的方法有很多种;您的树恰好用可视控件中的节点表示这一事实是无关紧要的。

对于某些算法,用递归术语来考虑遍历更容易。这样,您就可以让编程语言通过将当前活动节点作为参数保留在堆栈中来跟踪您在树中的位置。如果您不想使用递归,那么您只需要自己跟踪进度即可。常用的工具包括堆栈和队列。

前序遍历意味着当您访问一个节点时,您先对该节点的数据执行操作,然后再对该节点的子节点执行操作。它对应于从上到下访问树视图控件的每个节点。你可以用一个堆栈来实现它:

procedure PreorderVisit(Node: TTreeNode; Action: TNodeAction);
var
  Worklist: TStack<TTreeNode>;
  i: Integer;
begin
  Worklist := TStack<TTreeNode>.Create;
  try
    Worklist.Push(Node);
    repeat
      Node := Worklist.Pop;
      for i := Pred(Node.Items.Count) downto 0 do
        Worklist.Push(Node.Items[i]);
      Action(Node);
    until Worklist.Empty;
  finally
    Worklist.Free;
  end;
end;

以相反的顺序将孩子推入堆栈,以便它们以所需的顺序弹出。

在该代码中,Action代表您需要对每个节点执行的任何任务。您可以按照代码中的指定将其用作外部函数,也可以编写PreorderVisit包含特定任务代码的专用版本。

不过, TTreeView 实际上并不代表一棵树。它真的是一片森林(树木的集合)。那是因为没有代表根的单个节点。但是,您可以轻松地使用上面的函数来处理树中的所有节点:

procedure PreorderVisitTree(Tree: TTreeView; Action: TNodeAction);
var
  i: Integer;
begin
  for i := 0 to Pred(Tree.Items.Count) do
    PreorderVisit(Tree.Items[i], Action);
end;

另一种利用 TTreeView 的特定结构进行前序遍历的方法是使用GetNext每个节点的内置方法:

procedure PreorderVisitTree(Tree: TTreeView; Action: TNodeAction);
var
  Node: TTreeNode;
begin
  if Tree.Items.Count = 0 then
    exit;
  Node := Tree.Items[0];
  repeat
    Action(Node);
    Node := Node.GetNext;
  until not Assigned(Node);
end;

似乎无法获取 Firemonkey 树视图的隐藏节点。通过迭代内部树数据结构而不是尝试从 GUI 中提取信息,您可能会找到更好的结果。

于 2013-02-01T17:43:09.487 回答
3

这是我以非递归方式遍历树视图的函数。如果您有一个节点并且想要移动到下一个或上一个节点而无需遍历整个树,则使用起来很简单。

GetNextItem 通过查看它的第一个孩子来发挥作用,或者如果没有孩子,则在其自身之后查看下一个孩子的父母(并根据需要进一步通过父母)。

GetPrevItem 查看父项以查找前一项,并使用 GetLastChild 查找该项的最后一个子项(它确实使用递归,顺便说一句)。

请注意,编写的代码仅遍历扩展节点,但可以轻松修改为遍历所有节点(只需删除对 IsExpanded 的引用)。

function GetLastChild(Item: TTreeViewItem): TTreeViewItem;
begin
  if (Item.IsExpanded) and (Item.Count > 0) then
    Result := GetLastChild(Item.Items[Item.Count-1])
  else
    Result := Item;
end;

function GetNextItem(Item: TTreeViewItem): TTreeViewItem;
var ItemParent: TTreeViewItem;
  I: Integer;
  TreeViewParent: TTreeView;
  Parent: TFMXObject;
  Child: TFMXObject;
begin
  if Item = nil then
    Result := nil
  else if (Item.IsExpanded) and (Item.Count > 0) then
    Result := Item.Items[0]
  else
  begin
    Parent := Item.Parent;
    Child := Item;
    while (Parent <> nil) and not (Parent is TTreeView) do
    begin
      while (Parent <> nil) and not (Parent is TTreeView) and not (Parent is TTreeViewItem) do
        Parent := Parent.Parent;

      if (Parent <> nil) and (Parent is TTreeViewItem) then
      begin
        ItemParent := TTreeViewItem(Parent);
        I := 0;
        while (I < ItemParent.Count) and (ItemParent.Items[I] <> Child) do
          inc(I);
        inc(I);
        if I < ItemParent.Count then
        begin
          Result := ItemParent.Items[I];
          EXIT;
        end;
        Child := Parent;
        Parent := Parent.Parent
      end;
    end;

    if (Parent <> nil) and (Parent is TTreeView) then
    begin
      TreeViewParent := TTreeView(Parent);
      I := 0;
      while (I < TreeViewParent.Count) and (TreeViewParent.Items[I] <> Item) do
        inc(I);
      inc(I);
      if I < TreeViewParent.Count then
        Result := TreeViewParent.Items[I]
      else
      begin
        Result := Item;
        EXIT;
      end;
    end
    else
      Result := Item
  end
end;

function GetPrevItem(Item: TTreeViewItem): TTreeViewItem;
var Parent: TFMXObject;
  ItemParent: TTreeViewItem;
  TreeViewParent: TTreeView;
  I: Integer;
begin
  if Item = nil then
    Result := nil
  else
  begin
    Parent := Item.Parent;
    while (Parent <> nil) and not (Parent is TTreeViewItem) and not (Parent is TTreeView) do
      Parent := Parent.Parent;

    if (Parent <> nil) and (Parent is TTreeViewItem) then
    begin
      ItemParent := TTreeViewItem(Parent);
      I := 0;
      while (I < ItemParent.Count) and (ItemParent.Items[I] <> Item) do
        inc(I);
      dec(I);
      if I >= 0 then
        Result := GetLastChild(ItemParent.Items[I])
      else
        Result := ItemParent;
    end
    else if (Parent <> nil) and (Parent is TTreeView) then
    begin
      TreeViewParent := TTreeView(Parent);
      I := 0;
      while (I < TreeViewParent.Count) and (TreeViewParent.Items[I] <> Item) do
        inc(I);
      dec(I);
      if I >= 0 then
        Result := GetLastChild(TreeViewParent.Items[I])
      else
        Result := Item
    end
    else
      Result := Item;
  end;
end;
于 2013-02-03T11:53:36.533 回答
1

在 XE8 中,这对我有用:

function GetNextItem(Item: TTreeViewItem): TTreeViewItem;
var
   Parent: TFMXObject;
   Child: TTreeViewItem;
begin
    Result := nil;
    if Item.Count > 0 then
        Result := Item.Items[0]
    else
    begin
        Parent := Item.ParentItem;
        Child := Item;
        while (Result = nil) and (Parent <> nil) do
        begin
           if Parent is TTreeViewItem then
           begin
               if TTreeViewItem(Parent).Count > (Child.Index + 1) then
                   Result := TTreeViewItem(Parent).Items[Child.Index + 1]
               else
               begin
               Child := TTreeViewItem(Parent);
               if Child.ParentItem <> nil then
                   Parent := Child.ParentItem
               else
                   Parent := Child.TreeView;
               end;
           end
           else
           begin
            if TTreeView(Parent).Count > Child.Index + 1 then
                Result := TTreeView(Parent).Items[Child.Index + 1]
            else
                Parent := nil;
            end;
        end;
    end;
end;
于 2015-12-02T10:41:10.260 回答
1

Item.ParentItem可以为零!这就是我Parent := Item.ParentItem用以下几行替换该行的原因:

  if Item.ParentItem <> nil then
    Parent := Item.ParentItem
  else
    Parent := Item.TreeView;

GetNextItem修正后的完整功能:

function GetNextItem(Item: TTreeViewItem): TTreeViewItem;
var
  Parent: TFMXObject;
  Child: TTreeViewItem;
begin
  Result := nil;
  if Item.Count > 0 then
    Result := Item.Items[0]
  else begin
    if Item.ParentItem <> nil then
      Parent := Item.ParentItem
    else
      Parent := Item.TreeView;
    Child := Item;
    while (Result = nil) and (Parent <> nil) do
    begin
      if Parent is TTreeViewItem then
      begin
        if TTreeViewItem(Parent).Count > (Child.Index + 1) then
          Result := TTreeViewItem(Parent).Items[Child.Index + 1]
        else begin
          Child := TTreeViewItem(Parent);
          if Child.ParentItem <> nil then
            Parent := Child.ParentItem
          else
            Parent := Child.TreeView;
        end;
      end else begin
        if TTreeView(Parent).Count > Child.Index + 1 then
          Result := TTreeView(Parent).Items[Child.Index + 1]
        else
          Parent := nil;
      end;
    end;
  end;
end;

在德尔福 10.3.2 测试

于 2019-08-20T14:32:47.920 回答
0

我会添加一个函数来搜索部分文本到 TreeView,从 TreeView(TV)放置的 TEdit(搜索)。(特别感谢此答案所依据的上一篇文章)

这可以完美地使用 Enter 开始搜索并 F3 继续搜索。

// SEARCH ITEM (text partially or by particular ID in item.tag)

function GetNextItem(Item: TTreeViewItem): TTreeViewItem;
var
  Parent: TFMXObject;
  Child: TTreeViewItem;
begin
  Result := nil;
  if Item.Count > 0 then
    Result := Item.Items[0]
  else begin
    if Item.ParentItem <> nil then
      Parent := Item.ParentItem
    else
      Parent := Item.TreeView;
    Child := Item;
    while (Result = nil) and (Parent <> nil) do
    begin
      if Parent is TTreeViewItem then
      begin
        if TTreeViewItem(Parent).Count > (Child.Index + 1) then
          Result := TTreeViewItem(Parent).Items[Child.Index + 1]
        else begin
          Child := TTreeViewItem(Parent);
          if Child.ParentItem <> nil then
            Parent := Child.ParentItem
          else
            Parent := Child.TreeView;
        end;
      end else begin
        if TTreeView(Parent).Count > Child.Index + 1 then
          Result := TTreeView(Parent).Items[Child.Index + 1]
        else
          Parent := nil;
      end;
    end;
  end;
end;


function FindItem(aFromItem : TTreeViewItem ; Value: String = '' ; aID : integer = -1) : TTreeViewItem;
var I: Integer;
begin
  Result := nil;

  while aFromItem.Index < aFromITem.TreeView.Count do
  begin
    aFromItem := GetNextItem(aFromItem);
    if aFromItem <> nil then
    begin
      if (aID <> -1) and (aFromItem.Tag = aID) then
      begin
        Result := aFromItem;
        EXIT;
      end
      else if pos(Value, uppercase(aFromItem.Text)) > 0 then
      begin
        Result := aFromItem;
        EXIT;
      end;
    end
    else
      exit;
  end;
end;


procedure TCListeMedia.SearchKeyDown(Sender: TObject; var Key: Word; var KeyChar: Char; Shift: TShiftState);
var
  i : integer;
  vSearch : string;
begin
  if (Key = 13) or (Key = vkF3) then
  begin
    // Search or continue to search
    vSearch := Uppercase(Search.Text);
    if Key = 13 then
    begin
      i := 0;
      if TV.Count > 0 then
      begin
        if pos(vSearch, uppercase(TV.Items[0].Text)) > 0 then
          TV.Selected := TV.Items[0]
        else
          TV.Selected := FindItem(TV.Items[0], vSearch);
      end;
    end
    else if TV.Selected <> nil then
    begin
      i := 1 + TV.Selected.Index;
      TV.Selected := FindItem(TV.Selected, vSearch);
    end;
  end;
end;

procedure TCListeMedia.TVKeyDown(Sender: TObject; var Key: Word; var KeyChar: Char; Shift: TShiftState);
begin
  if (Key = vkF3) then
    SearchKeyDown(Sender, Key, KeyChar, Shift);
end;
于 2021-12-02T07:50:44.690 回答
0

我为我的项目制作了这个功能,既快速又简单,你可以试试

function FindItem(const TreeView: TTreeView; const Value: Variant): TTreeViewItem;

      function ItemExist(const AItem: TTreeViewItem): Boolean;
      begin
           Result:= False;

           if AItem <> nil then
           begin
                {Set your condition here}
                if AItem.Text = Value then
                begin
                     FindItem:= AItem;
                     Exit(True);
                end;

                var I: Integer;
                for I := 0 to AItem.Count - 1 do
                begin
                     if ItemExist( AItem.ItemByIndex(I)) then
                          Break;
                end;
           end;
      end;

  var
    AItem: TTreeViewItem;
    I: Integer;
  begin
       Result:= nil;

       for I := 0 to TreeView.Count - 1 do
       begin
            AItem:= TreeView.ItemByIndex(I);
            if ItemExist(AItem) or (Result <> nil) then Break;
       end;
  end;
于 2022-01-29T16:09:51.523 回答