TreeControl

June 24, 2026 ยท View on GitHub

Hierarchical tree control that displays nodes in a collapsible structure with keyboard and mouse navigation.

Overview

TreeControl displays data as an expandable, collapsible tree of TreeNode objects. Each node has text, an optional color, an optional Tag for custom data, and a list of child nodes. Parent nodes can be expanded or collapsed; leaf nodes can be activated.

The control flattens visible nodes into a scrollable list and supports a vertical scrollbar, mouse-wheel scrolling, hover highlighting, and configurable guide-line styles (Line, Ascii, DoubleLine, BoldLine). Tree structure can be built up front via the builder or modified at runtime through AddRootNode, RemoveRootNode, TreeNode.AddChild, and related methods.

Three primary events report interaction: SelectedNodeChanged (selection moved), NodeExpandCollapse (a parent toggled), and NodeActivated (a leaf was activated via Enter/Space or double-click). Each has an async counterpart. The control supports lazy loading by adding placeholder children and populating them in the NodeExpandCollapse handler.

See also: ListControl

Quick Start

var tree = Controls.Tree()
    .WithGuide(TreeGuide.Line)
    .WithName("fileTree")
    .OnSelectedNodeChanged((sender, args, window) =>
    {
        if (args.Node != null)
        {
            var status = window.FindControl<MarkupControl>("status");
            status?.SetContent($"Selected: {args.Node.Text}");
        }
    })
    .Build();

var root = tree.AddRootNode("Project");
var src = root.AddChild("src");
src.AddChild("Program.cs");
src.AddChild("Window.cs");
root.AddChild("README.md");

window.AddControl(tree);

Builder API

Create a builder with Controls.Tree().

Nodes

.AddRootNode(TreeNode node)              // Add an existing node (returns builder)
.AddRootNode("text")                     // Create + add a node (returns the TreeNode)
.AddRootNodes(params TreeNode[] nodes)   // Add multiple nodes
.AddRootNodes(IEnumerable<TreeNode> nodes)

Appearance

.WithGuide(TreeGuide.Line)               // Guide line style
.WithIndent("  ")                        // Indent string per level
.WithColors(foreground, background)
.WithBackgroundColor(color)
.WithForegroundColor(color)
.WithHighlightColors(foreground, background)  // Selected/highlighted item colors
.WithScrollbarVisibility(ScrollbarVisibility.Auto)

Layout

.WithMaxVisibleItems(count)              // Cap visible rows
.WithWidth(width)
.WithHeight(height)
.WithAlignment(HorizontalAlignment.Left)
.Centered()
.WithVerticalAlignment(VerticalAlignment.Top)
.WithMargin(left, top, right, bottom)
.WithMargin(uniform)
.WithStickyPosition(StickyPosition.None)

State and Identity

.Visible(true)
.Enabled(true)
.Disabled()
.WithName("treeName")                    // For window.FindControl lookup
.WithTag(object)

Events

.OnSelectedNodeChanged((sender, args) => { ... })
.OnSelectedNodeChanged((sender, args, window) => { ... })
.OnNodeExpandCollapse((sender, args) => { ... })
.OnNodeExpandCollapse((sender, args, window) => { ... })
.OnGotFocus((sender, e) => { ... })
.OnGotFocus((sender, e, window) => { ... })
.OnLostFocus((sender, e) => { ... })
.OnLostFocus((sender, e, window) => { ... })

Note: the builder wires up SelectedNodeChanged, NodeExpandCollapse, GotFocus, and LostFocus. To handle NodeActivated, subscribe to the event on the built control directly.

Properties

TreeControl

PropertyTypeDefaultDescription
RootNodesReadOnlyCollection<TreeNode>EmptySnapshot of the root nodes (read-only)
SelectedIndexint0Index of the selected node in the flattened list
SelectedNodeTreeNode?nullCurrently selected node (null if none)
GuideTreeGuideLineGuide line style (Line, Ascii, DoubleLine, BoldLine)
Indentstring" "Indent string applied per level
MaxVisibleItemsint?nullMax rows shown at once (fits available height if null)
Heightint?nullExplicit control height (content-based if null)
BackgroundColorColor?nullBackground color (uses theme if null)
ForegroundColorColorTheme / WhiteText color
HighlightBackgroundColorColor?nullBackground for the highlighted/selected item
HighlightForegroundColorColorWhiteForeground for the highlighted/selected item
ScrollbarVisibilityScrollbarVisibilityAutoScrollbar display mode (Auto, Always, Never)
HoverEnabledbooltrueEnable mouse hover highlighting
SelectOnRightClickboolfalseSelect the node under the cursor before MouseRightClick fires
LastRightClickedNodeTreeNode?nullNode under the cursor at the most recent right-click
IsEnabledbooltrueEnable/disable interaction
HasFocusboolfalseWhether the tree has keyboard focus
CanReceiveFocusboolIsEnabledWhether the tree can receive focus
ContentWidthint?computedRendered width in characters

TreeNode

PropertyTypeDefaultDescription
Textstring(constructor)Node label (supports markup)
ChildrenReadOnlyCollection<TreeNode>EmptyChild nodes (read-only view)
IsExpandedbooltrueWhether children are shown
Tagobject?nullCustom data associated with the node
TextColorColor?nullOptional per-node text color

Events

EventArgumentsDescription
SelectedNodeChangedEventHandler<TreeNodeEventArgs>Fired when the selected node changes
SelectedNodeChangedAsyncAsyncEventHandler<TreeNodeEventArgs>Async counterpart of SelectedNodeChanged
NodeExpandCollapseEventHandler<TreeNodeEventArgs>Fired when a node is expanded or collapsed
NodeExpandCollapseAsyncAsyncEventHandler<TreeNodeEventArgs>Async counterpart of NodeExpandCollapse
NodeActivatedEventHandler<TreeNodeEventArgs>Fired when a leaf node is activated (Enter/Space or double-click)
NodeActivatedAsyncAsyncEventHandler<TreeNodeEventArgs>Async counterpart of NodeActivated
MouseClickEventHandler<MouseEventArgs>Fired on a single left-click
MouseDoubleClickEventHandler<MouseEventArgs>Fired on a double-click
MouseRightClickEventHandler<MouseEventArgs>Fired on a right-click
MouseLeaveEventHandler<MouseEventArgs>Fired when the mouse leaves the control
MouseMoveEventHandler<MouseEventArgs>Fired when the mouse moves over the control
GotFocusEventHandlerFired when the tree receives focus
LostFocusEventHandlerFired when the tree loses focus

TreeNodeEventArgs exposes a single Node property (TreeNode?) identifying the affected node.

Keyboard Support

KeyAction
Up ArrowMove selection up
Down ArrowMove selection down
Page UpMove selection up by one page
Page DownMove selection down by one page
HomeSelect first node
EndSelect last node
Right ArrowExpand the selected node (if collapsed)
Left ArrowCollapse the selected node (if expanded with children)
EnterToggle expand/collapse on a parent; activate a leaf node
SpaceToggle expand/collapse on a parent; activate a leaf node

Keys combined with Shift, Alt, or Control are ignored, allowing focus navigation (Tab/Shift+Tab) to pass through.

Mouse Support

ActionResult
Left Click (node)Select node and give focus
Left Click (expand indicator)Toggle expand/collapse
Double Click (parent)Toggle expand/collapse
Double Click (leaf)Activate node (fire NodeActivated)
Right ClickFire MouseRightClick; sets LastRightClickedNode (selects node if SelectOnRightClick is true)
Scroll WheelScroll the tree up/down
HoverHighlight node under cursor (when HoverEnabled)
Scrollbar drag / arrows / trackScroll the tree

Methods

TreeControl

MethodDescription
AddRootNode(TreeNode)Adds an existing root node
AddRootNode(string)Creates and adds a root node, returning it
RemoveRootNode(TreeNode)Removes a root node; returns true if found
Clear()Removes all nodes
ExpandAll()Expands every node
CollapseAll()Collapses every node
SelectNode(TreeNode)Selects a node, expanding parents if needed; returns true if found
EnsureNodeVisible(TreeNode)Expands all ancestors so the node is visible; returns true if found
FindNodeByTag(object)Returns the first node with a matching Tag, or null
FindNodeByText(string, TreeNode? searchRoot = null)Returns the first node with matching text, or null
PulseNode(TreeNode, Color, int pulseCount, TimeSpan pulseDuration)Animates a node's text color as a pulse; returns the animation or null

TreeNode

MethodDescription
AddChild(TreeNode)Adds a child node, returning it
AddChild(string)Creates and adds a child node, returning it
RemoveChild(TreeNode)Removes a child; returns true if found
ClearChildren()Removes all child nodes

Examples

Simple Tree

var tree = Controls.Tree()
    .WithName("tree")
    .Build();

var fruits = tree.AddRootNode("Fruits");
fruits.AddChild("Apple");
fruits.AddChild("Banana");

var veggies = tree.AddRootNode("Vegetables");
veggies.AddChild("Carrot");
veggies.AddChild("Potato");

window.AddControl(tree);

Nodes with Custom Data (Tag)

var tree = Controls.Tree()
    .WithName("tree")
    .Build();

foreach (var person in people)
{
    var node = tree.AddRootNode(person.Name);
    node.Tag = person;  // Store the domain object
}

tree.SelectedNodeChanged += (sender, args) =>
{
    if (args.Node?.Tag is Person p)
    {
        windowSystem.NotificationStateService.ShowNotification(
            "Selected", $"{p.Name} ({p.Age})", NotificationSeverity.Info);
    }
};

window.AddControl(tree);

Activating Leaf Nodes

var tree = Controls.Tree()
    .WithName("menuTree")
    .Build();

var actions = tree.AddRootNode("Actions");
actions.AddChild("Open");
actions.AddChild("Save");
actions.AddChild("Close");

// NodeActivated is not exposed on the builder โ€” subscribe directly
tree.NodeActivated += (sender, args) =>
{
    switch (args.Node?.Text)
    {
        case "Open": OpenFile(); break;
        case "Save": SaveFile(); break;
        case "Close": window.Close(); break;
    }
};

window.AddControl(tree);

Lazy-Loading Directory Tree

const string Placeholder = "...";

var tree = Controls.Tree()
    .WithGuide(TreeGuide.Line)
    .WithName("dirTree")
    .Build();

void AddChildren(TreeNode parent, string dirPath)
{
    foreach (var dir in Directory.GetDirectories(dirPath))
    {
        var node = parent.AddChild($"[cyan]{Path.GetFileName(dir)}[/]");
        node.Tag = dir;
        node.IsExpanded = false;
        // Add a placeholder so the node shows as expandable
        node.AddChild(Placeholder);
    }
}

var root = tree.AddRootNode("[cyan]Root[/]");
root.Tag = rootPath;
AddChildren(root, rootPath);

// Populate children only when the node is expanded
tree.NodeExpandCollapse += (sender, args) =>
{
    if (args.Node is { IsExpanded: true, Tag: string path })
    {
        if (args.Node.Children.Count == 1 && args.Node.Children[0].Text == Placeholder)
        {
            args.Node.ClearChildren();
            AddChildren(args.Node, path);
        }
    }
};

window.AddControl(tree);

Master-Detail with Selection

var tree = Controls.Tree()
    .WithName("nav")
    .OnSelectedNodeChanged((sender, args, window) =>
    {
        var detail = window.FindControl<MarkupControl>("detail");
        detail?.SetContent($"[bold]{args.Node?.Text}[/]");
    })
    .Build();

window.AddControl(tree);
window.AddControl(
    Controls.Markup()
        .AddLine("[dim]Select a node[/]")
        .WithName("detail")
        .Build()
);

Styling and Highlight Colors

var tree = Controls.Tree()
    .WithGuide(TreeGuide.BoldLine)
    .WithColors(Color.White, Color.Grey15)
    .WithHighlightColors(Color.Black, Color.Aqua)
    .WithMaxVisibleItems(15)
    .WithScrollbarVisibility(ScrollbarVisibility.Auto)
    .Build();

var root = tree.AddRootNode("Settings");
root.TextColor = Color.Yellow;  // Per-node color
root.AddChild("Display");
root.AddChild("Audio");

window.AddControl(tree);

Best Practices

  1. Use Tag for data: Store domain objects in TreeNode.Tag and read them in event handlers via args.Node.Tag.
  2. Lazy-load large trees: Add a placeholder child and populate real children in NodeExpandCollapse to avoid building the whole tree up front.
  3. Subscribe to NodeActivated directly: It is not exposed on the builder; attach it to the built control for leaf activation.
  4. Choose a guide style: Use TreeGuide.Ascii for terminals with limited box-drawing support.
  5. Cap visible rows: Use WithMaxVisibleItems so the tree fits its layout and gets a scrollbar.
  6. Marshal background updates: When modifying nodes from background threads, use windowSystem.EnqueueOnUIThread for UI state and Container?.Invalidate(Invalidation.Relayout) to refresh (safe to call directly from a background thread).

See Also


Back to Controls | Back to Main Documentation