docfx/docs/treeview.md
TreeView displays and navigates hierarchical data with expandable/collapsible branches.
It comes in two forms:
T : class using an xref:Terminal.Gui.ITreeBuilder`1.Both share the same rendering, navigation, selection, and command behavior.
The simplest approach uses the non-generic xref:Terminal.Gui.TreeView with xref:Terminal.Gui.TreeNode objects:
TreeView tree = new ()
{
Width = 40,
Height = 20
};
TreeNode root1 = new () { Text = "Root1" };
root1.Children.Add (new TreeNode { Text = "Child1.1" });
root1.Children.Add (new TreeNode { Text = "Child1.2" });
TreeNode root2 = new () { Text = "Root2" };
root2.Children.Add (new TreeNode { Text = "Child2.1" });
root2.Children.Add (new TreeNode { Text = "Child2.2" });
tree.AddObject (root1);
tree.AddObject (root2);
This produces:
├-Root1
│ ├─Child1.1
│ └─Child1.2
└-Root2
├─Child2.1
└─Child2.2
When your data model already exists (e.g. Army and Unit classes), use the generic TreeView<T> with an xref:Terminal.Gui.ITreeBuilder`1 to tell the tree how objects relate:
TreeView<GameObject> tree = new ()
{
Width = 40,
Height = 20,
TreeBuilder = new DelegateTreeBuilder<GameObject> (
childGetter: o => o is Army a ? a.Units : Enumerable.Empty<GameObject> (),
canExpand: o => o is Army)
};
tree.AddObject (new Army
{
Designation = "3rd Infantry",
Units = [new Unit { Name = "Orc" }, new Unit { Name = "Troll" }]
});
xref:Terminal.Gui.ITreeNode is the interface for nodes in the non-generic TreeView:
| Member | Type | Description |
|---|---|---|
Text | string | Display text |
Children | IList<ITreeNode> | Child nodes |
Tag | object? | User data |
xref:Terminal.Gui.TreeNode is the default concrete implementation. Children is mutable — add or remove nodes at any time, then call RefreshObject to update the display.
For TreeView<T>, implement xref:Terminal.Gui.ITreeBuilder`1 to describe the hierarchy:
class GameObjectTreeBuilder : ITreeBuilder<GameObject>
{
public bool SupportsCanExpand => true;
public bool CanExpand (GameObject model) => model is Army;
public IEnumerable<GameObject> GetChildren (GameObject model)
{
if (model is Army a)
{
return a.Units;
}
return Enumerable.Empty<GameObject> ();
}
}
SupportsCanExpand enables a fast check for the expand/collapse symbol without fetching children. Set it to true when CanExpand is cheap.
xref:Terminal.Gui.DelegateTreeBuilder`1 provides the same thing with lambdas:
tree.TreeBuilder = new DelegateTreeBuilder<GameObject> (
childGetter: o => o is Army a ? a.Units : Enumerable.Empty<GameObject> (),
canExpand: o => o is Army);
The first delegate returns children; the second is the CanExpand check (both are required).
You can subclass xref:Terminal.Gui.TreeNode to wrap your own data:
class House : TreeNode
{
public string Address { get; set; } = "";
public List<Room> Rooms { get; set; } = [];
public override IList<ITreeNode> Children => Rooms.Cast<ITreeNode> ().ToList ();
public override string Text { get => Address; set => Address = value; }
}
class Room : TreeNode
{
public string Name { get; set; } = "";
public override string Text { get => Name; set => Name = value; }
}
Then add your objects directly:
tree.AddObject (new House
{
Address = "23 Nowhere Street",
Rooms = [new Room { Name = "Ballroom" }, new Room { Name = "Bedroom" }]
});
TreeView integrates with the Terminal.Gui command system. Input flows through IInputProcessor → KeyBindings/MouseBindings → Command → handler.
| Key | Command | Behavior |
|---|---|---|
| Enter | Command.Accept | Raises Accepting/Accepted (CWP) |
| Space | Command.Activate | Raises Activating/Activated; toggles expand/collapse |
| → | Command.Expand | Expand selected node |
| Ctrl+→ | Command.ExpandAll | Expand node and all descendants |
| ← | Command.Collapse | Collapse selected node, or navigate to parent node |
| Ctrl+← | Command.CollapseAll | Collapse node and all descendants |
| ↑ | Command.Up | Move selection up one row |
| ↓ | Command.Down | Move selection down one row |
| Shift+↑ | Command.UpExtend | Extend selection up (multi-select) |
| Shift+↓ | Command.DownExtend | Extend selection down (multi-select) |
| Ctrl+↑ | Command.LineUpToFirstBranch | Jump to first sibling |
| Ctrl+↓ | Command.LineDownToLastBranch | Jump to last sibling |
| PageUp | Command.PageUp | Move selection up one page |
| PageDown | Command.PageDown | Move selection down one page |
| Shift+PageUp | Command.PageUpExtend | Extend selection up one page |
| Shift+PageDown | Command.PageDownExtend | Extend selection down one page |
| Home | Command.Start | Select first node |
| End | Command.End | Select last node |
| Ctrl+A | Command.SelectAll | Select all (when MultiSelect is enabled) |
| Any letter | (collection navigator) | Jump to next matching node |
| Input | Behavior |
|---|---|
| Single click | Select the clicked node. If the click lands on the expand/collapse symbol (+/-), toggle expansion. |
| Double click | Raises Command.Accept → fires Accepting/Accepted (CWP). Also toggles expand/collapse. |
| Wheel up/down | Scroll viewport vertically |
| Wheel left/right | Scroll viewport horizontally |
TreeView registers handlers for Command.Activate and Command.Accept:
Command.Activate (OnActivated): Toggles expand/collapse on the selected node. For mouse clicks, only toggles when clicking the expand symbol. This is the handler for Space key and single click.
Command.Accept (OnAccepted): Follows the standard Cancellable Workflow Pattern — raises Accepting, then Accepted if not canceled. For mouse double-clicks, also toggles expand/collapse. Enter raises Accept without toggling.
Command.Toggle: Directly toggles expand/collapse on the selected node regardless of context. Space is bound to Command.Activate in the base View class, but TreeView also supports Command.Toggle explicitly.
TreeView follows the standard Cancellable Workflow Pattern for Accept:
tree.Accepting += (sender, e) =>
{
// Fires on Enter key or double-click
// Set e.Cancel = true to prevent Accepted from firing
};
tree.Accepted += (_, _) =>
{
// Node was accepted (confirmed)
ITreeNode? selected = tree.SelectedObject;
};
| Trigger | Event Raised |
|---|---|
| Enter key | Accepting → Accepted |
| Double click | Accepting → Accepted |
| Space key | Activating → Activated (not Accept) |
| Single click on expand symbol | Activating → Activated (not Accept) |
Fires whenever SelectedObject changes:
tree.SelectionChanged += (sender, e) =>
{
// e.OldValue is the previously selected object
// e.NewValue is the newly selected object
};
Fires for each visible line during rendering, allowing per-line customization:
tree.DrawLine += (sender, e) =>
{
// e.Model is the object being drawn
// e.IndexOfModelText is the column where text starts
// e.Cells is the list of cells to render
// Set e.Handled = true to suppress default rendering
};
Control rendering via the xref:Terminal.Gui.TreeStyle property:
| Property | Default | Description |
|---|---|---|
ShowBranchLines | true | Show │, ├, └ connector lines |
ExpandableSymbol | + | Symbol for collapsed expandable nodes |
CollapseableSymbol | - | Symbol for expanded nodes |
ColorExpandSymbol | false | Render expand symbol in highlight color |
InvertExpandSymbolColors | false | Swap foreground/background on expand symbol |
HighlightModelTextOnly | false | Highlight only the text, not the full row |
Set symbols to null to hide them entirely.
By default, TreeView renders each node using its ToString() method. Override this with AspectGetter:
treeViewFiles.AspectGetter = f => f.FullName;
Assign per-node colors:
tree.ColorGetter = node =>
{
if (node is HiddenFile)
{
return new Scheme { Normal = new Attribute (Color.Gray, Color.Black) };
}
return null; // Use default scheme
};
| Method | Description |
|---|---|
GoTo (T obj) | Select and scroll to a specific object |
GoToFirst () | Select the first root node |
GoToEnd () | Select the last visible node |
EnsureVisible (T obj) | Scroll so the object is in the viewport |
Expand (T obj) | Expand a specific node |
ExpandAll (T obj) | Expand a node and all its descendants |
Collapse (T obj) | Collapse a specific node |
CollapseAll (T obj) | Collapse a node and all its descendants |
Toggle (T obj) | Toggle expand/collapse |
IsExpanded (T obj) | Check if a node is expanded |
GetParent (T obj) | Get the parent node (only works for expanded branches) |
GetChildren (T obj) | Get the visible child nodes |
GetObjectOnRow (int row) | Get the object at a viewport row |
Enable multi-selection with the MultiSelect property (default: true):
tree.MultiSelect = true;
| Key | Behavior |
|---|---|
| Shift+↑/↓ | Extend selection by one row |
| Shift+PageUp/PageDown | Extend selection by one page |
| Ctrl+A | Select all visible nodes |
Retrieve the selection with GetAllSelectedObjects (). Single navigation clears extended selections.
When AllowLetterBasedNavigation is true (the default), pressing a letter key jumps to the next node whose AspectGetter text matches. This uses the KeystrokeNavigator property, which supports typing multiple characters in quick succession to refine the match.
Disable this when you have custom key bindings that conflict:
tree.AllowLetterBasedNavigation = false;
Apply a filter to show only matching nodes (and their ancestor paths):
tree.Filter = new TreeViewTextFilter<FileSystemInfo> (tree)
{
Text = "*.cs"
};
Implement xref:Terminal.Gui.ITreeViewFilter`1 for custom logic:
class MyFilter : ITreeViewFilter<GameObject>
{
public bool IsMatch (GameObject model) => model.ToString ().Contains ("Orc");
}
When a filter is active, parent nodes leading to matches remain visible even if they don't match, ensuring the tree structure is navigable.
Set Filter to null to remove filtering.
TreeView caches the expanded tree structure. After modifying nodes at runtime:
| Method | When to Use |
|---|---|
RefreshObject (T obj) | After changing a node's children or text |
RebuildTree () | After replacing the TreeBuilder or making sweeping changes |
InvalidateLineMap () | After changes that affect which lines are visible |
Example — adding a child node at runtime:
TreeNode parent = (TreeNode)tree.SelectedObject!;
parent.Children.Add (new TreeNode { Text = "New Child" });
tree.RefreshObject (parent);
Example — removing a node:
ITreeNode toDelete = tree.SelectedObject!;
if (tree.Objects.Contains (toDelete))
{
// It's a root node
tree.Remove (toDelete);
}
else
{
// It's a child node — remove from parent's children
ITreeNode? parent = tree.GetParent (toDelete);
parent?.Children.Remove (toDelete);
tree.RefreshObject (parent);
}
Accepting/Accepted event model