如何在Firemonkey和Delphi XE3下按顺序浏览TTreeView的所有节点



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

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

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

此问题适用于德尔福XE3/FM2

谢谢

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

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

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;

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

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

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

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

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

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 进行测试

我会添加一个函数,从树视图(电视)放置的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;

我已经为我的项目制作了这个功能,既快速又简单,您可以尝试一下

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;

我利用 Delphi 中的类帮助程序和匿名过程来循环访问 TreeView 中的项目。这可以很容易地扩展以构建索引列表。

我的类助手是这样的:

{ TTreeViewHelper }
  TTreeViewHelper
  = Class helper for FMX.TreeView.TTreeView
      Public
        Procedure LoopThroughItems(const Func: TProc<TTreeViewItem>; const AExpandedOnly: Boolean);
    End;
  Procedure TTreeViewHelper.LoopThroughItems(const Func: TProc<TTreeViewItem>; const AExpandedOnly: Boolean);
    var
      i : integer;
      procedure ProcessItem(const AItem: TTreeViewItem);
        var
          I: Integer;
        begin
          if(AItem=nil) then exit;
          Func(AItem);
          for I := 0 to AItem.Count - 1 do ProcessItem(AItem.ItemByIndex(I));
        end;
    begin
      if not Assigned(Func)then exit;
      if(GlobalCount<1)then exit;
      if(AExpandedOnly)
        then for i:=0 to Count-1 do Func(self.Items[i])
        else for i:=0 to Count-1 do ProcessItem(ItemByGlobalIndex(i));
    end;

我是这样使用它的:

  TreeView1.LoopThroughItems(
    procedure(E: TTreeViewItem)
    begin 
      if Assigned(E)and(E is TTreeNode)
        then TN := E as TTreeNode { My own subclass }
        else exit;
      if Assigned(TN.DataObject)and(TN.DataObject is TIOTSensorData)
        then IOT := TN.DataObject as TIOTSensorData
        else exit;
      if(IOT<>AFormula)then exit;
      TreeView1.Selected := TN;
    end,
    False
  );

上面的示例来自我的实际项目,您将在匿名过程中使用自己的逻辑,但真正整洁的部分是最后一个TreeView1.Selected := TN;,因为即使 TN 是不可见的项目,TreeView 也会选择它并展开其所有父节点。

现在,你说你想避免递归,但实际上你想避免递归递归。因为您必须先构建索引,并且在构建索引时可以使用递归一次。遵循相同的方法,继续向类帮助程序添加一个新方法:

{ TTreeViewHelper }
  TTreeViewHelper
  = Class helper for FMX.TreeView.TTreeView
      Public
        Procedure LoopThroughItems(const Func: TProc<TTreeViewItem>; const AExpandedOnly: Boolean);
        Function  BuildFullIndex: TList<TTreeViewItem>;
    End;
  Function  TTreeViewHelper.BuildFullIndex: TList<TTreeViewItem>;
    var
      i : integer;
      procedure Publish(const AItem: TTreeViewItem);
        var
          I: Integer;
        begin
          if(AItem=nil) then exit;
          Result.Add(AItem);
          for I := 0 to AItem.Count - 1 do Publish(AItem.ItemByIndex(I));
        end;
    begin
      Result := TList<TTreeViewItem>.Create;
      if(GlobalCount<1)then exit;
      for i:=0 to Count-1 do Publish(ItemByGlobalIndex(i))
    end;

并像这样使用它:

uses
  System.Generics.Collections;
var
  Index : TList<TTreeViewItem>;
begin
  Index := Formulas.BuildFullIndex; 
  try
    if(Index.Count<1)then exit;
    for i:=0 to Index.Count-1 do
      begin
        { do your thing here }
      end;
  finally
    FreeAndNil(Index);
  end;
end;

干杯!

最新更新