How to view all TTreeView nodes sequentially under Firemonkey and Delphi XE3?
I need to traverse the elements of a tree without using recursion, for performance reasons.
TTreeview provides GlobalCount and ItemByGlobalIndex methods, but only returns visible items
I searched the root of the class without finding a private list of all nodes, FGlobalItems seems to only contain the items to be displayed
Is there a way to sequentially view all the elements (including invisible and collapsed nodes) of the tree?
This question is specific to Delphi XE3 / FM2
Thank,
[Edit Feb 3]
I accepted the default answer (not possible out of the box), even though I was looking for a way to fix the firemonkey tree in this aspect.
After more analysis, I found out that the FGlobalItems list contains only extended items and is supported in the TCustomTreeView.UpdateGlobalIndexes method; Commenting out line 924 FMX.TreeView (if AItem.IsExpanded then ...) results in a complete index of nodes and allows all nodes to be viewed sequentially using ItemByGlobalIndex (), BUT can lead to other performance issues and errors ...
Without any clue, I will keep my recursive code.
source to share
Here are my functions for walking through a tree structure in a non-recursive manner. Easy to use if you have a node and want to go to the next or previous one without having to walk the entire tree.
GetNextItem by looking at it for the first child, or if there are no children, looking at it as a parent for the next child after itself (and, if necessary, through the parents).
GetPrevItem looks for a parent to find the previous item and uses GetLastChild to find the last child of that item (which uses recursion, BTW).
Note that the code written only for the Extended Nodes traversal, but can be easily modified to traverse all nodes (just remove the references to 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;
source to share
The question basically asks the question of how to traverse a tree without recursion. There are many ways to cross the tree; the fact that your tree ends up being represented by nodes in visual control doesn't matter.
For some algorithms, it is easier to think of traversal in recursive terms. This way, you let the programming language keep track of where in the tree you are storing the currently active node as an argument on the stack. If you don't want to use recursion, you just need to track the progress yourself. The usual tools for this are stacks and queues.
Preorder traversal means that when you visit a node, you do your action on the node's data before you do the action on the children of the node. This corresponds to visiting each node of the treeview control from top to bottom. You can implement it like this with a stack:
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;
Push the children onto the stack in reverse order so they pop out in the correct order.
In this code, Action
denotes any task that needs to be done with each node. You can use it as indicated in the code as an external function or write a specialized version PreorderVisit
that includes task-specific code.
TTreeView is not actually a tree. It really is a forest (collection of trees). This is because there is no single node that represents the root. You can easily use the function above to process all nodes in the tree:
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;
Another way to do order traversal using a specific TTreeView structure is to use the built-in method GetNext
for each node:
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;
There seems to be no way to get the hidden nodes of the Firemonkey tree tree. You can find the best results by iterating over your internal tree data structure instead of trying to extract information from the GUI.
source to share
In XE8 this works for me:
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;
source to share
Item.ParentItem
can also be null! That is why I replaced the line with the Parent := Item.ParentItem
following lines:
if Item.ParentItem <> nil then
Parent := Item.ParentItem
else
Parent := Item.TreeView;
Full function GetNextItem
after fix:
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;
Tested in Delphi 10.3.2
source to share