【问题标题】:How to sequentially browse all nodes of a TTreeView under Firemonkey and Delphi XE3?Firemonkey和Delphi XE3下如何依次浏览一个TTreeView的所有节点?
【发布时间】:2013-01-16 20:29:22
【问题描述】:

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

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

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

此问题适用于 Delphi XE3 / FM2

谢谢,

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

【问题讨论】:

  • 我非常怀疑你所要求的能不能做到。 FMX 的设计是每个控件都可以作为任何其他控件的父级。所以你只需要像任何其他控件一样迭代孩子。为什么框架会在递归版本的同时维护树结构的线性版本。
  • 是什么让您认为避免递归可以提高性能?
  • @jachguate:对于每个浏览的节点,您可以避免使用参数堆叠和局部变量创建/销毁的调用成本。在大树上检查了以前的商业代码。
  • @David:在 VCL 方面,例如 dev expree 树维护一个内部节点列表。使用 FMX 树视图,还维护了一个顺序列表,但仅适用于可见节点。为什么要维护它:即使不需要,拥有原始项目集合和反映层次结构的单独数据似乎是一个不错的设计。
  • VCL 完全不同。忘记所有关于 VCL 的事情。

标签: delphi firemonkey


【解决方案1】:

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

使用 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;

【讨论】:

    【解决方案2】:

    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;
    

    在 Delphi 10.3.2 测试

    【讨论】:

      【解决方案3】:

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

      【讨论】:

        【解决方案4】:

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

        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;
        

        【讨论】:

        • 谢谢,有趣的方法
        【解决方案5】:

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

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

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

        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 中提取信息,您可能会发现更好的结果。

        【讨论】:

        • 感谢您的回答。恐怕手动管理堆栈而不是让编译器为我们做它没有附加价值,至少对于性能限制。关于您的第二个建议,TTreeViewItem 没有 GetNext 方法(我的问题与 firemonkey 有关)
        • 编译器从不为您做任何与视觉控件相关的事情。这总是在库代码中。无论是您编写的,还是 FMX 的一部分,都可能对性能影响不大。您会接受 FMX 不具备您要求的功能的答案吗?
        • @David:编译器生成代码以将参数推送到堆栈。当您使用递归时,将执行此代码。 Rob 的提议是重写编译器所做的事情,但是使用我们自己的代码,使用 Worklist 而不是 stack&heap。这与 FMX 无关,我从未写过。我看到 FMX 似乎没有开箱即用的功能。我正在通过类助手甚至直接更新 FMX 代码寻求任何解决方法...
        • 所以现在你说你不介意使用递归?您是说只要使用的堆栈是线程堆栈而不是用户定义的堆栈,递归就可以了。问题是您要求非递归解决方案,而 Rob 给出了唯一可能的非递归解决方案。也许您可以告诉我们您的性能限制是什么。
        • err.. 不,有一个误解,我介意避免递归成本。我说 Rob 的提议并没有提供比原始递归更好的性能,因为它重新创建了递归机制。我会接受 Rob 解决方案作为默认答案(不可能开箱即用),但我正在寻找解决方法:请参阅我在问题中的评论
        猜你喜欢
        • 2016-01-12
        • 1970-01-01
        • 1970-01-01
        • 1970-01-01
        • 1970-01-01
        • 1970-01-01
        • 1970-01-01
        • 1970-01-01
        • 1970-01-01
        相关资源
        最近更新 更多