如何在火猴和Delphi XE3下顺序浏览一个TTreeView的所有节点?

8e2ybdfx  于 2022-09-21  发布在  其他
关注(0)|答案(7)|浏览(174)

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

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

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

此问题适用于Delphi XE3/FM2

谢谢,

[编辑2月3日]

我接受了默认的答案(不可能开箱即用),尽管我正在寻找一种方法来修补火猴TreeView在这方面的问题。
经过进一步分析,我发现FGlobalItems列表只包含展开的项,并且在TCustomTreeView.UpdateGlobalIndex.方法中进行维护;
注解FMX.TreeView的第924行(如果AItem.IsExpanded Then...)导致构建节点的完整索引,并允许使用ItemByGlobalIndex()按顺序浏览所有节点,但可能会导致其他性能问题和错误...
如果没有更多的线索,我将保留我的递归代码。

kknvjkwl

kknvjkwl1#

下面是我以非递归方式遍历TreeView的函数。如果您有一个节点,并且希望移动到下一个或上一个节点,而不必遍历整个树,则使用起来很简单。

GetNextItem的功能是查看它的第一个子项,或者如果没有子项,则查看它的父项以查找其后面的下一个子项(并在必要时进一步查看父项)。

GetPrevItem查看父项以查找前一项,并使用GetLastChild查找该项的最后一个子项(它使用递归BTW)。

注意,编写的代码只遍历展开的节点,但可以很容易地修改为遍历所有节点(只需删除对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;
ulydmbyx

ulydmbyx2#

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

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

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

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;

似乎没有办法获得火猴树视图的隐藏节点。通过迭代内部树数据结构而不是尝试从图形用户界面中提取信息,您可能会得到更好的结果。

ztigrdn8

ztigrdn83#

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

kgsdhlau4#

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上测试

zpf6vheq

zpf6vheq5#

我会添加一个功能,将部分文本搜索到树视图中,从树视图(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;
6ju8rftf

6ju8rftf6#

我已经为我的项目做了这个功能,又快又简单,你可以试一下

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

v6ylcynt7#

我利用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;

干杯!

相关问题