Skip to content

TreeView

Header: winui/ui/collections/TreeView.hpp. Spec: winui/ui/collections/TreeView.hpp.

A hierarchy of expandable rows, with single or multiple (check box) selection, templated rows, and rearranging by drag and drop.

The nodes are plain TreeNode data, set with set_roots(). The rows are the visible nodes in order, and the tree draws each row's chrome itself: backplate, selection indicator, indentation and chevron. Which nodes are expanded and selected is kept by each node's key, so set_roots() with a changed tree keeps them. The tree is a single Tab stop. Up and Down move between rows. Left and Right collapse, expand, or step to the parent or the first child. Enter invokes. With set_can_drag_items() and set_allow_drop() on, a row can be dragged onto another row to become its last child, or into the gap between rows to reorder. on_drag_items_completed() reports the move. The tree is not virtualized. Every visible row is realized, so keep very large trees collapsed.

Example:

auto tree = std::make_unique<ui::TreeViewControl>();
tree->set_roots({{.text = "Documents", .children = {{.text = "Report.docx"}}, .expanded = true},
                 {.text = "Network", .has_children = true}});
tree->on_expanding([raw = tree.get()](const ui::TreeNode& node) {
    if (node.text == "Network" && node.children.empty())
    {
        std::vector<ui::TreeNode> roots = raw->roots();
        roots[1].children.push_back({.text = "Server01"}); // load lazily
        raw->set_roots(std::move(roots));                 // the node goes on expanding
    }
});
// Enter or double-click:
tree->on_item_invoked([](const ui::TreeNode& node) { std::println("open {}", node.text); });

ui::TreeView

A hierarchy of expandable rows over the app's data. Builds a ui::TreeViewControl.

.items are the top-level items, and .children says where an item's children are: a member (&Folder::subfolders) or a function returning them. The data is the model. The tree shows it and keeps only which rows are expanded and selected, by .key, so that state survives a changed collection, such as a reload, a rename or children loaded on expand. A function .children can make an item's children as they are needed, and .has_children shows the chevron before they exist. on_expanding loads them into the bound property, and the row opens.

With a member .children and a bound ui::Property, a drag (with can_drag_items and allow_drop) writes the rearranged items back.

The ready-made item is ui::TreeNode, which needs no .children, .key or .item. An item type of its own needs .item, the row's content. Name the item type as for the other views, ui::TreeView<Folder>. A braced list of ui::TreeNode works with a plain ui::TreeView.

Example:

struct Folder
{
    std::string         path;
    std::string         name;
    bool                has_subfolders = false;
    std::vector<Folder> subfolders;
    friend bool operator==(const Folder&, const Folder&) = default;
};
ui::Property<std::vector<Folder>> folders{{{"C:/", "Local Disk (C:)", true}, {"D:/", "Data (D:)", true}}};

window.set_content(ui::TreeView<Folder>{
    .items        = folders,
    .children     = &Folder::subfolders,
    .key          = &Folder::path,
    .has_children = &Folder::has_subfolders,
    .on_expanding = [&](const Folder& f) {
        std::vector<Folder> drives = folders.get(); // load the drive's folders lazily
        for (Folder& drive : drives)
            if (drive.path == f.path && drive.subfolders.empty())
                drive.subfolders = {{f.path + "Users", "Users"}, {f.path + "Windows", "Windows"}};
        folders = drives;
    },
    .item         = [](const Folder& f) -> ui::Spec { return ui::Text{.text = f.name}; },
});

Every field, in the order a designated initializer names them:

ui::TreeView{
    .items, .children, .key, .has_children, .expanded, .indent_width, .selection_mode,
    .can_drag_items, .allow_drop, .style, .margin, .width, .height, .min_width, .min_height,
    .max_width, .max_height, .horizontal_alignment, .vertical_alignment, .visible, .tool_tip,
    .enabled, .accessibility, .on_selection_changed, .on_item_click, .on_item_invoked,
    .on_expanding, .on_collapsed, .on_drag_items_completed, .on_pointer_entered,
    .on_pointer_exited, .on_got_focus, .on_lost_focus, .on_key_down, .on_wheel, .control,
    .item, .item_context_menu
}

Fields, in declaration order (designators must follow it):

Field Type Default
items ui::Binding<std::vector<T>> The top-level items: a ui::Property<std::vector<T>> the tree follows, or a plain vector.
children ui::detail::TreeChildren<std::type_identity_t<T>> Where an item's children are: a member pointer, or a function returning them. Not needed for ui::TreeNode.
key ui::detail::TreeKey<std::type_identity_t<T>> What identifies an item: a member pointer or a function giving a string or a number. Keys must be unique in the tree. Left out, an item is known by its position.
has_children std::function<bool(const std::type_identity_t<T>&)> Whether an item shows the chevron before its children exist. Load them in on_expanding.
expanded std::function<bool(const std::type_identity_t<T>&)> Whether an item's children show when the tree first sees its key. After that the user's expanding and collapsing count. Left out, every item starts collapsed.
indent_width ui::Binding<float> {16.0f} Indentation per nesting level, DIPs.
selection_mode ui::Binding<ui::TreeViewSelectionMode> {ui::TreeViewSelectionMode::Single} None, Single (the default) or Multiple (check boxes).
can_drag_items ui::Binding<bool> {false} Whether items can be dragged to another place in the tree; off by default.
allow_drop ui::Binding<bool> {false} Whether the tree accepts drops; off by default.
on_selection_changed ui::Callback<void(ui::SelectedItem<std::type_identity_t<T>>)> The selection changed, by a click, the arrow keys, a check box, or a selected item gone from .items. Called with a copy of the selected item, the first one in Multiple mode, or unset when nothing is selected (see ui::SelectedItem).
on_item_click ui::Callback<void(const std::type_identity_t<T>&)> A row was clicked, before the click selects it.
on_item_invoked ui::Callback<void(const std::type_identity_t<T>&)> A row was opened by Enter or a double-click, and under TreeViewSelectionMode::None also by a single click.
on_expanding ui::Callback<void(const std::type_identity_t<T>&)> An item is opening. Load its children into the bound property here.
on_collapsed ui::Callback<void(const std::type_identity_t<T>&)> An item has closed.
on_drag_items_completed ui::Callback<void(const ui::TreeViewMovedArgs<std::type_identity_t<T>>&)> A drag moved an item. Called with the item, its new parent and its position there.
on_pointer_entered ui::Action The pointer moved onto the control.
on_pointer_exited ui::Action The pointer left the control.
on_got_focus ui::Action Keyboard focus arrived on the control, or on anything inside it, from outside it. A control with nothing focusable in it never gets focus, and never raises this.
on_lost_focus ui::Action Keyboard focus left the control and everything inside it.
on_key_down ui::Callback<ui::Handling(const ui::KeyEvent&)> A key pressed while focus is on the control or inside it, before the control's own handling; ui::Handled stops it there (Element::on_key_down()).
on_wheel ui::Callback<ui::Handling(const ui::PointerEvent&)> A wheel turn over the control, before the control scrolls; ui::Handled stops it there (Element::on_wheel()).
control ui::ControlOut<ui::TreeView> Out: the handle that receives the built control.
item ui::Callback<ui::Spec(const std::type_identity_t<T>&)> A row's content for an item. Left out, a ui::TreeNode row shows its text.
item_context_menu ui::Callback<std::vector<ui::MenuItem>(const std::type_identity_t<T>&)> The context menu of an item: a callable taking the item and returning the menu's entries. Called on a right-click on its row, or on Shift+F10 or the Menu key while it is current. The item is selected first. An empty list opens no menu. The reference is valid during the call only, so an entry's on_click captures what it needs by value.

Plus the common fields style, margin, width, height, min_width, min_height, max_width, max_height, horizontal_alignment, vertical_alignment, visible, tool_tip, enabled, accessibility every spec carries (conventions).

build

std::unique_ptr<Element> build()&&

Consumes the spec and returns the built ui::TreeViewControl; the caller owns it.

ui::TreeViewControl

Derives from ui::Element, ui::ValueProvider.

A hierarchy of expandable rows, with single or multiple (check box) selection, templated rows, and rearranging by drag and drop.

The nodes are plain TreeNode data, set with set_roots(). The rows are the visible nodes in order, and the tree draws each row's chrome itself: backplate, selection indicator, indentation and chevron. Which nodes are expanded and selected is kept by each node's key, so set_roots() with a changed tree keeps them. The tree is a single Tab stop. Up and Down move between rows. Left and Right collapse, expand, or step to the parent or the first child. Enter invokes. With set_can_drag_items() and set_allow_drop() on, a row can be dragged onto another row to become its last child, or into the gap between rows to reorder. on_drag_items_completed() reports the move. The tree is not virtualized. Every visible row is realized, so keep very large trees collapsed.

Example:

auto tree = std::make_unique<ui::TreeViewControl>();
tree->set_roots({{.text = "Documents", .children = {{.text = "Report.docx"}}, .expanded = true},
                 {.text = "Network", .has_children = true}});
tree->on_expanding([raw = tree.get()](const ui::TreeNode& node) {
    if (node.text == "Network" && node.children.empty())
    {
        std::vector<ui::TreeNode> roots = raw->roots();
        roots[1].children.push_back({.text = "Server01"}); // load lazily
        raw->set_roots(std::move(roots));                 // the node goes on expanding
    }
});
// Enter or double-click:
tree->on_item_invoked([](const ui::TreeNode& node) { std::println("open {}", node.text); });

ItemTemplate

using ui::TreeViewControl::ItemTemplate = Callback<std::unique_ptr<Element>(const TreeNode&)>

Makes a row's content for a node and may choose a different look per node. Called during measure for each visible row. The result is kept per node until the tree changes.

TreeViewControl

TreeViewControl()

An empty, enabled tree in Single selection mode, with dragging and dropping off.

set_roots

void set_roots(std::vector<TreeNode> roots)

Replaces the tree. Nodes whose key was there before stay expanded and selected as they were. A new key takes its node's expanded. A selected node whose key is gone is deselected, which raises on_selection_changed(). Safe to call from on_expanding(), which is how children added on expand arrive. Invalidates every TreeNode* into the old tree and every row content the template made, and lays the tree out again.

roots

const std::vector<TreeNode>& roots() const

The top-level nodes, each owning its subtree, as last set or rearranged by a drag.

find

const TreeNode* find(std::string_view key) const

The node with key, or nullptr. A node without a key is found by its path of texts.

key_of

const std::string& key_of(const TreeNode& node) const

A node's effective key: its key, or the path of texts from its root.

set_item_template

void set_item_template(ItemTemplate item_template)

Sets the row content template. nullptr, the default, shows each node's text. Rebuilds every row's content on the next layout.

is_expanded

bool is_expanded(const TreeNode& node) const

Whether node's children show.

set_expanded

void set_expanded(std::string_view key, bool expanded)

Expands or collapses the node with key, as the chevron does, raising on_expanding() first.

selection_mode

TreeViewSelectionMode selection_mode() const

The selection mode; Single by default.

set_selection_mode

void set_selection_mode(TreeViewSelectionMode mode)

Switches the mode and adapts the selection. None clears it, Single keeps only the first selected node, and Multiple shows the check boxes. Lays the tree out again.

selected

const TreeNode* selected() const

The selected node, the first one in Multiple mode, or nullptr.

selected_nodes

std::vector<const TreeNode*> selected_nodes() const

Every fully selected node, in tree order.

select

void select(std::string_view key)

Selects the node with key alone, in Single mode. An unknown key clears the selection.

selection_state

TreeNodeSelectionState selection_state(const TreeNode& node) const

A node's state in Multiple mode. A leaf's is whether it is selected. A parent's follows its children: Selected when all are, Unselected when none are, else Partial.

select_all

void select_all()

Selects every node in Multiple mode, and does nothing in the other modes. Fires on_selection_changed() once.

on_selection_changed

void on_selection_changed(Callback<void(const TreeNode*)> handler)

Fired whenever the selection changes, by a click, the arrow keys, a check box, or a selected key gone from set_roots(). Called with selected(): the selected node, the first one in Multiple mode, or nullptr when nothing is selected. Replaces any previous handler.

on_item_click

void on_item_click(Callback<void(const TreeNode&)> handler)

Called with the node on a single click on its row, before the click selects it. Not fired for a click on the chevron or check box.

on_item_invoked

void on_item_invoked(Callback<void(const TreeNode&)> handler)

Called when a row is opened: Enter or a double-click, and under TreeViewSelectionMode::None also a single click. Raised after the click selects. Not fired for a click on the chevron or check box.

on_expanding

void on_expanding(Callback<void(const TreeNode&)> handler)

A node is opening. Add the children of a node with has_children here, by setting a tree that has them (set_roots()). Fires before the node expands. If the node still has no children and no has_children afterwards, it stays collapsed.

on_collapsed

void on_collapsed(Callback<void(const TreeNode&)> handler)

Fired after a node has closed.

can_drag_items

bool can_drag_items() const

Whether rows can be dragged out to rearrange the tree; false by default. Element's allow-drop flag, also off by default, decides whether the tree accepts drops. Rearranging the tree needs both.

set_can_drag_items

void set_can_drag_items(bool can)

Turns dragging rows on or off. Takes effect on the next press.

on_drag_items_completed

void on_drag_items_completed(Callback<void(const TreeViewDragItemsCompletedArgs&)> handler)

Called with the node that moved, at its new place, its new parent (nullptr for a root) and its position there. roots() already holds the rearranged tree.

row_height

float row_height() const

The row height of the last measure, in DIPs, for callers that compute rows. 0 before the first measure.

indent_width

float indent_width() const

Indentation per nesting level, in DIPs; 16 by default.

set_indent_width

void set_indent_width(float width)

Changes the per-level indentation and lays the tree out again.

visible_row_count

std::size_t visible_row_count() const

How many rows are visible, that is not inside a collapsed ancestor.

visible_node_at

const TreeNode* visible_node_at(std::size_t index) const

The node drawn at visible row index, counted from 0 at the top, or nullptr when out of range. Valid until the tree is next rearranged.

focused

bool focused() const

Whether the tree holds keyboard focus and draws the focus ring.

focusable

bool focusable() const override

A Tab stop only while enabled and non-empty.

set_item_context_menu

void set_item_context_menu(Callback<std::vector<MenuItem>(const TreeNode&)> menu)

Sets the context menu of a node. It is asked for on a right-click on its row, or on Shift+F10 or the Menu key while the node is the current one. The node is selected first, unless it already is or the selection mode is not Single. An empty list opens no menu. Replaces any previous one.

raise_context_requested

bool raise_context_requested(std::optional<Point> position) override

Offers a right-click first to the templated content of the row under position, then to the tree's own context flyout, and then opens the node's context menu.

for_each_child

void for_each_child(ChildVisitor visit) const override

The node context menu while it shows, so the window finds its popup.

drag_enter

void drag_enter(const DragEventArgs& e) override

The drop events. drag_over() is asked first on the innermost drop target under the pointer, then on its drop targets up the tree until one accepts; every element asked has heard drag_enter() first and hears drag_leave() once the drag is no longer its business. The one that accepted gets drop(). A control overrides them; the defaults run the handlers set with on_drag_enter()/on_drag_over()/on_drag_leave()/on_drop().

drag_enter(): the drag has come over this element; drag_over() follows at once.

drag_over

DragResponse drag_over(const DragEventArgs& e) override

The drag is over this element: answer what a drop here would do. Asked when it arrives and at every move. The default runs the on_drag_over() handler, and refuses without one.

drag_leave

void drag_leave(const DragEventArgs& e) override

The drag left this element, was canceled over it, or was dropped on another target.

drop

void drop(const DragEventArgs& e) override

The pointer was released over this element while its drag_over() accepted. What the drop did is the operation it accepted.

drag_completed

void drag_completed(DataPackageOperation result) override

A drag this element started has ended. result is what the target did (None when the drag was canceled or dropped nowhere).

automation_children

void automation_children(std::vector<Element*>& out) const override

One accessibility item per root row. Each lists its content and its visible children.

automation_parent_of

Element* automation_parent_of(const Element& child) const override

A row's accessibility item is the parent of its content and of its child rows' items.

automation_focus

Element* automation_focus() const override

The row the keyboard is on, so a screen reader reads it as the arrow keys move.

automation_role

AutomationRole automation_role() const override

Reported to screen readers as a Tree.

measure_override

Size measure_override(Size available, const MeasureContext& context) const override

Implement these instead of measure() and arrange(). The base class has already applied margin, size limits and alignment by the time these run. measure_override() is pure (every concrete Element must size itself); arrange_override() defaults to a no-op since most leaves need nothing beyond having bounds() set, which the base already did.

arrange_override

void arrange_override(const Rect& final_rect, const ArrangeContext& context) override

Position children within the rect arrange() settled on (also in bounds(), window DIPs). Default: nothing, for a leaf.

pointer_override

bool pointer_override(const PointerEvent& e) override

A pointer event for this element: the element under the pointer, one inside it that did not handle the event, or the element holding the capture. The position is in layout coordinates (handle_pointer()). Return true when the event did something here, and it goes no further up; a handled press also captures the pointer until its release. A control that acts on a press (a click, a toggle, a caret placed, a drag started) returns true for the press and its release, so a list row or a card around it does not act on the same click. Default: false.

render_override

void render_override(Renderer& renderer, const RenderContext& context) const override

What the control draws, in layout coordinates; render() has already applied the visual layer. A container draws its children through their render(), never their render_override().

tick_override

void tick_override(Duration delta) override

Ticks the templated row content and drives the hover-to-expand timer during a drag.

clear_dirty_override

void clear_dirty_override(DirtyFlags flags) override

This element's own part of clear_dirty(): clear any part it owns but leaves out of for_each_child() (a parked page, a hidden button), through that part's clear_dirty(). Default: nothing.

enabled_changed

void enabled_changed() override

is_enabled() flipped, through set_enabled() here or on an ancestor, or a move to another parent. Drop any press or hover state. The element is redrawn either way. No-op by default.

focus_changed

void focus_changed(bool focused) override

Keyboard focus arrived (true) or left (false); is_focused() already says so. The element is redrawn either way. No-op by default.

key_override

bool key_override(const KeyEvent& e) override

A key for this element: the focused one, or one inside it that did not handle it. Return true when the key did something here, and it goes no further; false lets it bubble on to the parent and then to the shortcuts, a dialog's default button and arrow-key focus moves. Default: false.

focus_ring_rect

Rect focus_ring_rect(const Theme& theme) const override

Inside the row the keyboard is on. Around the whole tree when there is none.

focus_ring_corner_radius

float focus_ring_corner_radius(const Theme& theme) const override

The ring's corner radius: the theme's control radius by default.

focus_ring_inside

bool focus_ring_inside() const override

Whether the ring goes inside focus_ring_rect() in every era, its outer edge on the rect. False by default, which leaves it to the theme. An item inside a viewport (a list row, a tab, a menu item) returns true, as does a control in a tight spot, so a neighbor or the viewport edge never cuts its ring.

struct TreeNode

A node of a tree: plain data, not an Element. The ready-made item of a ui::TreeView, and what a TreeViewControl is given.

A tree keeps which nodes are expanded and selected by key. A new tree with the same keys, after a reload, a rename or children added on expand, keeps them. A node without a key is known by the path of texts from its root ("Documents/Report.docx").

Example:

ui::TreeView{
    .items = {
        {.text = "Documents", .children = {{.text = "Report.docx"}, {.text = "Budget.xlsx"}}, .expanded = true},
        {.text = "Music", .has_children = true}, // children added on expand
    },
}

Field Type Default
text std::string The row's label, and the name screen readers read for the node. Not displayed when an item template is set.
key std::string What the tree knows the node by. Empty uses the path of texts from its root.
children std::vector<TreeNode> The child nodes, in display order, owned by value.
expanded bool false Whether the children show when the tree first sees this node's key. After that the user's expanding and collapsing count.
has_children bool false Whether to show the chevron before the children exist. An on_expanding handler adds them.

struct TreeViewDragItemsCompletedArgs

What a TreeViewControl's on_drag_items_completed gets: the node that moved, its new parent and its position there.

Field Type Default
node const TreeNode& The moved node, at its new place.
new_parent const TreeNode* nullptr Its new parent; nullptr for a root.
index std::size_t 0 Its position among the new parent's children (or the roots).

struct TreeViewMovedArgs

template <typename T> struct TreeViewMovedArgs

What a ui::TreeView's on_drag_items_completed gets: the item that moved, its new parent (nullptr for a root) and its position among the new parent's children. Valid during the call.

Field Type Default
item const T& The moved item.
new_parent const T* nullptr Its new parent; nullptr for a root.
index std::size_t 0 Its position among the new parent's children (or the roots).

Enumerations

TreeViewSelectionMode

enum class TreeViewSelectionMode

How many nodes a TreeView lets the user select.

Value
None No selection. Clicks only invoke.
Single One node, selected by a click or the arrow keys. The default.
Multiple Any number, through a check box per row or Space. A click only invokes.

TreeNodeSelectionState

enum class TreeNodeSelectionState

A node's selection state in Multiple mode, as its check box shows it.

Value
Unselected Neither the node nor any descendant leaf is selected.
Partial Some of the node's descendants are selected, not all. The check box shows indeterminate.
Selected The node, or every descendant leaf, is selected.

Tests

  • tests/test_tree_view.cpp