Skip to content

TableView

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

A table: a header strip over virtualized rows, with sorting, resizable columns, frozen leading and trailing columns, four selection modes, cell templates, cell editing and collapsible groups.

Only the rows in view are realized, so a grid of 100,000 rows costs what a screenful does. Columns are fixed, Auto or fraction-sized (TableViewColumnWidth). When they are wider than the viewport, the table scrolls sideways with Shift+wheel, a horizontal wheel, Left and Right, or the horizontal scroll bar. Leading and trailing frozen columns stay in place. A click on a header sorts by that column through its TableViewSortCycle and draws a chevron. Sorting reorders the table's own view of the rows, never rows(), and the selection stays on the same rows. Dragging a header's trailing divider resizes the column between its min and max width.

A cell shows its text in rows() unless set_cell_factory() builds an Element for it, such as a check box, a progress bar or an icon beside a caption. Those are built only for the rows in view and kept while their row stays in view or holds focus. They are children of the table, so they take pointer input and focus and screen readers see them. An Auto column measures them too, and the column still sorts by its text.

Editing is off until set_read_only(false). Then F2, or a double-click on a cell, opens an editor in the current cell. That is a TextBox over a text cell, or what set_cell_editor_factory() builds, which is the only way to edit a template cell. Enter commits and Escape cancels. A press elsewhere in the table, or focus leaving it, commits. on_cell_edited() sees every commit and can reject it, which keeps the editor open. While editing is possible, Left and Right move the current column instead of scrolling sideways, and scrolling follows it. The current column holds the cell F2 edits, ringed when focused.

set_group_keys() files the rows under collapsible group header rows, the groups in the order their keys first appear. A sort orders the rows within each group. A header row toggles on a click, Space or Enter. Left collapses it and Right expands it. Selection never lands on a header row, and it stays put while the columns scroll sideways.

The grid itself is the single tab stop. Up, Down, PageUp, PageDown, Home and End move through the rows and group headers in view order and scroll the row into view. Space selects in Single and Extended mode and toggles in Multiple mode. Shift extends a range in Multiple and Extended mode, and Ctrl moves without selecting there. Row indices everywhere in this API (selected_row(), selected_rows(), on_selection_changed(), on_cell_edited()) are indices into rows(), whatever the sort or grouping. Up from the first row moves onto the column headers. There Left and Right move between them, Enter or Space sorts, Ctrl+Left and Ctrl+Right narrow and widen the column, and Down goes back to the rows.

Example:

// Declarative: ui::TableView builds a ui::TableViewControl, one row per item.
using ui::operator""_fr;
struct Person { std::string name; std::string role; int years = 0; };
ui::Property<std::string> status{"Nothing selected"};
window.set_content(ui::VerticalStack{
    .spacing  = 8,
    .content  = {
        ui::TableView<Person>{
            .items   = {{"Ada Lovelace", "Engineer", 5},
                        {"Grace Hopper", "Architect", 12},
                        {"Alan Turing", "Researcher", 8}},
            .columns = {{.header = "Name", .width = 1_fr, .text = [](const Person& p) { return p.name; }},
                        {.header = "Role", .width = 160.0f, .text = [](const Person& p) { return p.role; }},
                        {.header  = "Years",
                         .width   = 80.0f,
                         .text    = [](const Person& p) { return std::to_string(p.years); },
                         .compare = [](const Person& a, const Person& b) { return a.years <=> b.years; }}},
            .height  = 240,
            .on_selection_changed = [&status](std::optional<Person> p)
            { status = p ? p->name + " selected" : "Nothing selected"; },
        },
        ui::Text{.text = status},
    },
});

// Imperative: the same control built by hand.
auto stack = std::make_unique<ui::StackControl>(ui::Orientation::Vertical);
auto grid  = std::make_unique<ui::TableViewControl>();
grid->set_columns({{.header = "Name", .width = 100.0f}, {.header = "Age", .width = 60.0f}});
grid->set_rows({{"Ada", "36"}, {"Grace", "85"}});
grid->on_selection_changed([&status](std::optional<std::size_t> row)
                           { status = row ? std::format("Row {}", *row) : ""; });
grid->set_selected_row(1); // fires the handler, scrolls row 1 into view
grid->sort_by_column(1, ui::TableViewSortDirection::Descending);
grid->set_read_only(false); // F2 or a double-click edits a cell
grid->on_cell_edited([](const ui::TableViewCellEditArgs& args)
                     { return args.text.empty() ? ui::Cancel : ui::Proceed; });
grid->set_group_keys({"Staff", "Guests"}); // one key per row
stack->add(std::move(grid));

ui::TableView

A virtualized table: one row per item, one cell per column. Builds a ui::TableViewControl.

.items is the collection and takes the same sources as ui::ListView. Each column says which text it shows for an item, or gives a cell template that builds the cell's content from the item. Only visible rows are realized, so a large table costs what the viewport costs. When a bound collection changes, the rows are read again.

A header click sorts the table's view of the rows by that column, never .items, and the selection stays on the same items. Dragging a header's divider resizes the column and reports it through .on_column_resized. Columns wider than the table scroll sideways, except frozen ones. Row indices (.selected_row, .selected_indices) are positions in .items, whatever the sort or grouping. Nothing is selected until the user or .selected_row picks a row.

.read_only = false lets the user edit cells with F2 or a double-click. Enter commits and Escape cancels. .on_cell_edited writes each value into its item or rejects it. .group_by files the rows under collapsible group headers.

Example:

struct Person
{
    std::string name;
    std::string role;
    int         years = 0;
    friend bool operator==(const Person&, const Person&) = default;
};
using ui::operator""_fr;
ui::Property<std::vector<Person>> people{{{"Ada Lovelace", "Engineer", 5}, {"Grace Hopper", "Architect", 12}}};
ui::Property<std::string>         status{};

window.set_content(ui::VerticalStack{.content = {
    ui::TableView<Person>{
        .items   = people,
        .columns = {
            {.header = "Name", .width = 140.0f, .text = [](const Person& p) { return p.name; }, .frozen = true},
            {.header = "Role", .width = 1_fr, .text = [](const Person& p) { return p.role; }},
            {.header  = "Years",
             .width   = 80.0f,
             .text    = [](const Person& p) { return std::to_string(p.years); },
             .compare   = [](const Person& a, const Person& b) { return a.years <=> b.years; },
             .read_only = true},
        },
        .alternating_rows     = true,
        .read_only            = false,
        .group_by             = [](const Person& p) { return p.role; },
        .height               = 300.0f,
        .on_selection_changed = [&](std::optional<Person> p) { status = p ? p->name : ""; },
        .on_cell_edited       = [](const ui::TableViewCellEditedArgs<Person>& args)
        {
            if (args.text.empty())
            {
                return ui::Cancel; // keeps the editor open
            }
            (args.column == 0 ? args.item.name : args.item.role) = args.text;
            return ui::Proceed;
        },
        .empty_content        = ui::Text{.text = "No people yet"},
    },
    ui::Text{.text = status},
}});

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

ui::TableView{
    .items, .key, .columns, .selected_row, .selected_indices, .row_height,
    .size_rows_to_content, .selection_mode, .can_user_sort_columns, .can_user_resize_columns,
    .grid_lines_visibility, .alternating_rows, .read_only, .group_by, .grouping_enabled,
    .sticky_group_headers, .alternating_row_background, .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_sorting, .on_sorted, .on_column_resized, .on_beginning_edit, .on_cell_edited,
    .on_pointer_entered, .on_pointer_exited, .on_got_focus, .on_lost_focus, .on_key_down,
    .on_wheel, .control, .empty_content, .group_header_template, .item_context_menu
}

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

Field Type Default
items ui::Binding<std::vector<T>> The collection, one row per item: a ui::ObservableVector, a ui::Property<std::vector<T>>, a plain vector or a braced list.
key ui::ItemKey<std::type_identity_t<T>> What identifies an item when .items is bound to a property: a member pointer or a callable returning the key (see ui::ItemKey). Keys must be unique. With a key, a new vector keeps the selection on the same items. Without one, the rows before and after an edit keep theirs. A change that arrives as a reset, such as an ObservableVector's assign() or clear() or a very large reorder, clears the selection.
columns std::vector<ui::TableColumn<std::type_identity_t<T>>> The columns, left to right: header, width in DIPs and the cell text for an item.
selected_row ui::TwoWayField<std::optional<std::size_t>> The selected row. Unset, the default, selects nothing. For a table that opens on its first row, set 0.
selected_indices ui::Binding<std::vector<std::size_t>> Every selected row, as ascending positions in .items. Bind a ui::Property<std::vector<std::size_t>> to read and set the selection under Multiple or Extended. Two-way with ui::bind(). Left empty and unbound, .selected_row decides.
row_height ui::Binding<float> {40.0f} Row height, DIPs. 30 and 48 are the compact and comfortable densities.
size_rows_to_content ui::Binding<bool> {false} Whether rows grow past row_height to fit their tallest cell, wrapping the text cells.
selection_mode ui::Binding<ui::SelectionMode> {ui::SelectionMode::Single} None, Single (the default), Multiple or Extended.
can_user_sort_columns ui::Binding<bool> {true} Whether header clicks sort. Columns opt out with can_sort.
can_user_resize_columns ui::Binding<bool> {true} Whether header dividers can be dragged. Columns opt out with can_resize.
grid_lines_visibility ui::Binding<ui::TableViewGridLinesVisibility> {ui::TableViewGridLinesVisibility::All} Which grid lines show: All (the default), Horizontal, Vertical or None.
alternating_rows ui::Binding<bool> {false} Whether rows are banded: every second row takes alternating_row_background.
read_only ui::Binding<bool> {true} False turns editing on, by F2 or a double-click on a cell. Columns opt out with their own read_only. Each commit reaches .on_cell_edited.
group_by ui::Callback<std::string(const T&)> The group an item is filed under, as the text its header shows. Groups follow the order their keys first appear in .items, and a sort orders the rows within each group.
grouping_enabled ui::Binding<bool> {true} Whether .group_by groups the rows. False shows them ungrouped, and true groups them again, with the collapsed groups still collapsed.
sticky_group_headers ui::Binding<bool> {true} Whether the current group's header stays pinned under the column headers while its rows scroll under it.
alternating_row_background ui::Binding<ui::Paint> The banding color. Empty, the default, uses the theme's banding color (palette.tabular_row_alternating under Fluent). Drawn only with alternating_rows.
on_selection_changed ui::Callback<void(ui::SelectedItem<std::type_identity_t<T>>)> Fires on every selection change, by click, keys or a bound write. Called with a copy of the selected row's item, the first in .items order under Multiple and Extended, or unset when nothing is selected (see ui::SelectedItem). The copy is safe to keep.
on_sorting ui::Callback<ui::Decision(const ui::TableViewSortingArgs&)> Called with the column and the direction a header click or key is about to sort it in. Return ui::Cancel to keep the current order.
on_sorted ui::Callback<void(const ui::TableViewSortedArgs&)> Called with the column and its new direction after a sort change. The column is unset when the sort was cleared.
on_column_resized ui::Callback<void(const ui::TableViewColumnResizedArgs&)> Called with the column and its new width in DIPs on every step of a user resize.
on_beginning_edit ui::Callback<ui::Decision(const ui::TableViewBeginningEditArgs<std::type_identity_t<T>>&)> Called with the item and the column before an editor opens. Return ui::Cancel to keep it closed.
on_cell_edited ui::Callback<ui::Decision(const ui::TableViewCellEditedArgs<std::type_identity_t<T>>&)> Called on a commit with the item, the column and the editor's text. Write the value into the item and return ui::Proceed or nothing. Return ui::Cancel to reject it and keep the editor open. For a column with an editing_cell, item is the edited copy and the text is its cell text, and proceeding copies it into the collection. A ui::Property collection is written back after an accepted edit. An ObservableVector's item is changed in place, without a notification. Without a handler a text edit only changes the cell as shown, until the rows are read again.
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<std::type_identity_t<ui::TableView>> Out: the handle that receives the built control. Its type uses std::type_identity_t, so passing a handle does not take part in deducing the type argument.
empty_content ui::Spec Shown, centered in the body, while there are no rows.
group_header_template ui::Callback<ui::Spec(const std::string& key)> A group header's content from its key, shown after the chevron in place of the key and the count. Needs .group_by.
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 row is selected first. An empty list opens no menu. An item of a ui::ObservableVector stays where it is while it is in the collection, so an entry's on_click may keep a reference to it. Any other item is valid during the call only, so capture it 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::TableViewControl; the caller owns it.

ui::TableViewControl

Derives from ui::Element, ui::SelectionProvider, ui::GridProvider, ui::TableProvider.

A table: a header strip over virtualized rows, with sorting, resizable columns, frozen leading and trailing columns, four selection modes, cell templates, cell editing and collapsible groups.

Only the rows in view are realized, so a grid of 100,000 rows costs what a screenful does. Columns are fixed, Auto or fraction-sized (TableViewColumnWidth). When they are wider than the viewport, the table scrolls sideways with Shift+wheel, a horizontal wheel, Left and Right, or the horizontal scroll bar. Leading and trailing frozen columns stay in place. A click on a header sorts by that column through its TableViewSortCycle and draws a chevron. Sorting reorders the table's own view of the rows, never rows(), and the selection stays on the same rows. Dragging a header's trailing divider resizes the column between its min and max width.

A cell shows its text in rows() unless set_cell_factory() builds an Element for it, such as a check box, a progress bar or an icon beside a caption. Those are built only for the rows in view and kept while their row stays in view or holds focus. They are children of the table, so they take pointer input and focus and screen readers see them. An Auto column measures them too, and the column still sorts by its text.

Editing is off until set_read_only(false). Then F2, or a double-click on a cell, opens an editor in the current cell. That is a TextBox over a text cell, or what set_cell_editor_factory() builds, which is the only way to edit a template cell. Enter commits and Escape cancels. A press elsewhere in the table, or focus leaving it, commits. on_cell_edited() sees every commit and can reject it, which keeps the editor open. While editing is possible, Left and Right move the current column instead of scrolling sideways, and scrolling follows it. The current column holds the cell F2 edits, ringed when focused.

set_group_keys() files the rows under collapsible group header rows, the groups in the order their keys first appear. A sort orders the rows within each group. A header row toggles on a click, Space or Enter. Left collapses it and Right expands it. Selection never lands on a header row, and it stays put while the columns scroll sideways.

The grid itself is the single tab stop. Up, Down, PageUp, PageDown, Home and End move through the rows and group headers in view order and scroll the row into view. Space selects in Single and Extended mode and toggles in Multiple mode. Shift extends a range in Multiple and Extended mode, and Ctrl moves without selecting there. Row indices everywhere in this API (selected_row(), selected_rows(), on_selection_changed(), on_cell_edited()) are indices into rows(), whatever the sort or grouping. Up from the first row moves onto the column headers. There Left and Right move between them, Enter or Space sorts, Ctrl+Left and Ctrl+Right narrow and widen the column, and Down goes back to the rows.

Example:

// Declarative: ui::TableView builds a ui::TableViewControl, one row per item.
using ui::operator""_fr;
struct Person { std::string name; std::string role; int years = 0; };
ui::Property<std::string> status{"Nothing selected"};
window.set_content(ui::VerticalStack{
    .spacing  = 8,
    .content  = {
        ui::TableView<Person>{
            .items   = {{"Ada Lovelace", "Engineer", 5},
                        {"Grace Hopper", "Architect", 12},
                        {"Alan Turing", "Researcher", 8}},
            .columns = {{.header = "Name", .width = 1_fr, .text = [](const Person& p) { return p.name; }},
                        {.header = "Role", .width = 160.0f, .text = [](const Person& p) { return p.role; }},
                        {.header  = "Years",
                         .width   = 80.0f,
                         .text    = [](const Person& p) { return std::to_string(p.years); },
                         .compare = [](const Person& a, const Person& b) { return a.years <=> b.years; }}},
            .height  = 240,
            .on_selection_changed = [&status](std::optional<Person> p)
            { status = p ? p->name + " selected" : "Nothing selected"; },
        },
        ui::Text{.text = status},
    },
});

// Imperative: the same control built by hand.
auto stack = std::make_unique<ui::StackControl>(ui::Orientation::Vertical);
auto grid  = std::make_unique<ui::TableViewControl>();
grid->set_columns({{.header = "Name", .width = 100.0f}, {.header = "Age", .width = 60.0f}});
grid->set_rows({{"Ada", "36"}, {"Grace", "85"}});
grid->on_selection_changed([&status](std::optional<std::size_t> row)
                           { status = row ? std::format("Row {}", *row) : ""; });
grid->set_selected_row(1); // fires the handler, scrolls row 1 into view
grid->sort_by_column(1, ui::TableViewSortDirection::Descending);
grid->set_read_only(false); // F2 or a double-click edits a cell
grid->on_cell_edited([](const ui::TableViewCellEditArgs& args)
                     { return args.text.empty() ? ui::Cancel : ui::Proceed; });
grid->set_group_keys({"Staff", "Guests"}); // one key per row
stack->add(std::move(grid));

SortComparer

using ui::TableViewControl::SortComparer = Callback<std::weak_ordering(std::size_t column, std::size_t row_a, std::size_t row_b)>

Orders two rows, by their indices into rows(), for the column being sorted.

CellFactory

using ui::TableViewControl::CellFactory = Callback<std::unique_ptr<Element>(std::size_t row, std::size_t column)>

Builds the content of one cell, by its row (an index into rows()) and column. Returns nullptr to leave that cell as text.

CellToolTip

using ui::TableViewControl::CellToolTip = Callback<std::string(std::size_t row, std::size_t column) const>

The tip for a body cell, by its row (an index into rows()) and column. Empty means none, and the table's own tool_tip() shows instead.

GroupHeaderFactory

using ui::TableViewControl::GroupHeaderFactory = Callback<std::unique_ptr<Element>(std::size_t group)>

Builds the content of group group's header row.

TableViewControl

TableViewControl()

An empty grid: no columns, no rows, nothing selected, 40 DIP rows.

~TableViewControl

~TableViewControl() override

TableViewControl

TableViewControl(const TableViewControl&) = delete

Not movable or copyable.

operator=

TableViewControl& operator=(const TableViewControl&) = delete

TableViewControl

TableViewControl(TableViewControl&&) = delete

operator=

TableViewControl& operator=(TableViewControl&&) = delete

columns

const std::vector<TableViewColumn>& columns() const

The columns, left to right. A row's cell i is drawn under column i.

set_columns

void set_columns(std::vector<TableViewColumn> columns)

Replaces every column, clears the sort, and lays the table out again.

rows

const std::vector<std::vector<std::string>>& rows() const

The rows in source order. Each inner vector is one row's cell text, indexed by column. A row with fewer cells than there are columns shows blank cells past its own size.

set_rows

void set_rows(std::vector<std::vector<std::string>> rows)

Replaces every row, sorts them again by the current sort, clears the selection, and lays the table out again. Nothing is selected afterwards, in every selection mode. Raises on_selection_changed() when something was selected before.

row_height

float row_height() const

Height of every body row in DIPs; 40 by default. The header strip has its own fixed height of 40.

set_row_height

void set_row_height(float height)

Changes the row height, lays the table out again and rebuilds the visible rows.

size_rows_to_content

bool size_rows_to_content() const

Whether each row grows past row_height() to fit its tallest cell. Text cells then wrap instead of being trimmed, and a template cell or an editor is measured at its own height. Off by default. A row keeps its height while it is out of view. Rows not yet seen count as row_height() until they scroll in.

set_size_rows_to_content

void set_size_rows_to_content(bool on)

See size_rows_to_content().

column_actual_width

float column_actual_width(std::size_t column) const

The width the last arrange gave a column, whether it is fixed, Auto or a fraction. 0 before the first arrange or past the last column.

set_column_width

void set_column_width(std::size_t column, TableViewColumnWidth width)

Sets one column's width. A user resize does this, then raises on_column_resized().

set_column_frozen

void set_column_frozen(std::size_t column, bool frozen)

Pins one column, or lets it scroll again (TableViewColumn::frozen). Keeps the sort.

set_column_frozen_trailing

void set_column_frozen_trailing(std::size_t column, bool frozen)

Pins one column against the right edge, or lets it scroll again (TableViewColumn::frozen_trailing). Keeps the sort.

can_user_resize_columns

bool can_user_resize_columns() const

The table-wide switch for dragging header dividers; on by default. A column opts out with TableViewColumn::can_resize.

set_can_user_resize_columns

void set_can_user_resize_columns(bool can)

See can_user_resize_columns().

on_column_resized

void on_column_resized(Callback<void(const TableViewColumnResizedArgs&)> handler)

Called with the column and its new width in DIPs on every step of a user resize drag.

horizontal_offset

float horizontal_offset() const

How far the columns are scrolled sideways, DIPs. 0 is the leading edge.

set_horizontal_offset

void set_horizontal_offset(float offset)

Scrolls sideways, clamped to how far the columns overflow the viewport, and redraws.

can_user_sort_columns

bool can_user_sort_columns() const

The table-wide switch for sorting by a header click; on by default. A column opts out with TableViewColumn::can_sort.

set_can_user_sort_columns

void set_can_user_sort_columns(bool can)

See can_user_sort_columns().

sort_column

std::optional<std::size_t> sort_column() const

The sorted column. Unset while the rows are in source order.

sort_direction

TableViewSortDirection sort_direction() const

The sorted column's direction. None while unsorted.

sort_by_column

bool sort_by_column(std::size_t column, TableViewSortDirection direction)

Sorts the view by column and clears any other column's sort. TableViewSortDirection::None restores the source order. The selected rows stay selected. Returns true when the sort changed, and then raises on_sorted().

toggle_sort_direction

bool toggle_sort_direction(std::size_t column)

Does what a header click does: steps column to the next direction in its sort_cycle, unless on_sorting() cancels. Returns true when the sort changed.

header_cursor

std::optional<std::size_t> header_cursor() const

The column header the keyboard is on. Unset while the keys move through the rows.

clear_sort

bool clear_sort()

Restores the source order. Returns true when there was a sort to clear.

set_sort_comparer

void set_sort_comparer(SortComparer comparer)

Sets how two rows compare for a sort. Without one, the default, the cell text is compared, as numbers when both cells read as numbers (compare_text()).

on_sorting

void on_sorting(Callback<Decision(const TableViewSortingArgs&)> handler)

Called before a header click or key sorts a column. Return ui::Cancel to keep the current order. sort_by_column() does not raise it.

on_sorted

void on_sorted(Callback<void(const TableViewSortedArgs&)> handler)

Called after every sort change, with the column and its new direction. When the sort was cleared, the direction is None and there is no column.

row_at_position

std::optional<std::size_t> row_at_position(std::size_t position) const

The index into rows() of the row shown at position in the sorted and grouped view. Unset for a group header and past the end.

position_of_row

std::optional<std::size_t> position_of_row(std::size_t row) const

Where row row, an index into rows(), is shown in the view. Unset while its group is collapsed.

position_count

std::size_t position_count() const

How many positions the view has: the rows shown plus the group header rows.

selection_mode

SelectionMode selection_mode() const

The selection mode; Single by default. Switching to None clears the selection. Switching to Single keeps at most one row, the first selected.

set_selection_mode

void set_selection_mode(SelectionMode mode)

See selection_mode().

selected_row

std::optional<std::size_t> selected_row() const

The selected row with the lowest index into rows(). Unset when nothing is selected.

set_selected_row

void set_selected_row(std::optional<std::size_t> index)

Selects index alone, clamped to the rows, and scrolls it into view. Does nothing, and fires no callback, if it is already the selection, if there are no rows, or under SelectionMode::None. Unset clears the selection.

selected_rows

const std::vector<std::size_t>& selected_rows() const

Every selected row, as ascending indices into rows().

set_selected_rows

void set_selected_rows(std::vector<std::size_t> rows)

Replaces the selection. Indices past the rows are dropped, Single keeps the lowest, None keeps nothing, and an empty list clears it. Raises on_selection_changed() when the selection changed.

is_row_selected

bool is_row_selected(std::size_t row) const

Whether row row, an index into rows(), is selected.

on_selection_changed

void on_selection_changed(Callback<void(std::optional<std::size_t>)> handler)

Called with selected_row() whenever the selection changes: by click, by key, by set_selected_row() or set_selected_rows(), or by set_rows() clearing a selection. Replaces any previous handler.

set_item_context_menu

void set_item_context_menu(Callback<std::vector<MenuItem>(std::size_t row)> menu)

Sets the context menu of row row, an index into rows(). It is asked for on a right-click on the row, or on Shift+F10 or the Menu key while the row is the current one. The row is selected first, unless it already is or the selection mode is None. An empty list opens no menu. Replaces any previous one.

raise_context_requested

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

Opens the row's context menu (set_item_context_menu()) when no on_context_requested() handler inside the row took the request.

set_cell_factory

void set_cell_factory(CellFactory factory)

Sets the cell factory. A cell it returns an Element for shows that Element instead of its text, stretched over the whole cell with no cell padding. Called once per cell as its row comes into view. The Element is kept while the row stays in view or holds focus, and built again after set_rows() or set_columns(). Replacing the factory rebuilds every cell.

set_cell_tool_tip

void set_cell_tool_tip(CellToolTip tip)

Sets the per-cell tips; see CellToolTip.

tool_tip_at

std::string tool_tip_at(Point p) const override

The tip of the cell under p (set_cell_tool_tip()), else tool_tip().

cell_element

Element* cell_element(std::size_t row, std::size_t column) const

The Element a template cell shows while its row is in view. nullptr for a text cell or a row out of view.

read_only

bool read_only() const

Whether editing is off for the whole table, the default. A column opts out on its own with TableViewColumn::read_only.

set_read_only

void set_read_only(bool read_only)

Turns editing off or on. Turning it off commits an open edit, or cancels it when the commit is rejected.

set_cell_editor_factory

void set_cell_editor_factory(CellFactory factory)

Sets the factory for a cell's editor, by its row and column. Returning nullptr gives the default, a TextBox over a text cell. A template cell without an editor is not editable. The editor is laid over the cell while the edit is open. The table stays the focused element and passes the keys and text on to the editor's first focusable control.

is_editing

bool is_editing() const

Whether a cell editor is open.

editing_row

std::optional<std::size_t> editing_row() const

The row of the open edit, an index into rows(). Unset when none is open.

editing_column

std::optional<std::size_t> editing_column() const

The column of the open edit. Unset when none is open.

editor

Element* editor() const

The open editor, either the TextBox or what the editor factory built, or nullptr.

commit_edit

bool commit_edit()

Closes the open edit and keeps its text. on_cell_edited() runs first and may reject it. Returns false when nothing was open or the commit was rejected.

cancel_edit

bool cancel_edit()

Closes the open edit and leaves the cell as it was. Returns false when nothing was open.

current_column

std::optional<std::size_t> current_column() const

The current column, where a click on a cell or Left and Right put it. F2 edits its cell on the keyboard's row. Unset until then, and F2 then edits the first editable column.

set_current_column

void set_current_column(std::size_t column)

Moves the current column, clamped to the columns, and scrolls it into view.

on_beginning_edit

void on_beginning_edit(Callback<Decision(const TableViewCellArgs&)> handler)

Called before an editor opens on a cell, with its row and column. Return ui::Cancel to keep it closed.

on_cell_edited

void on_cell_edited(Callback<Decision(const TableViewCellEditArgs&)> handler)

Called on a commit with the cell and the editor's text. For an editor from the factory, the text is the cell's own text. Return ui::Cancel to reject the value, which keeps the editor open. The handler may rewrite text to what the cell should show. Without a handler a commit stores the text in rows().

set_cell_text

void set_cell_text(std::size_t row, std::size_t column, std::string text)

Replaces one cell's text in rows() and redraws. The row keeps its place until the next sort.

set_group_keys

void set_group_keys(std::vector<std::string> keys)

Sets one key per row, by index into rows(), the text its group header shows. Rows with the same key form a group. Groups follow the order their keys first appear in rows(), and a sort orders the rows within each group. A collapsed group stays collapsed while its key is in use. An empty list, the default, shows the rows ungrouped. A row past the end of the list is filed under an empty key.

set_group_header_factory

void set_group_header_factory(GroupHeaderFactory factory)

Sets the group header content, shown after the chevron in place of the key and count. It is built for the headers in view only, and again when the groups change. A click on the row still toggles the group.

group_header_element

Element* group_header_element(std::size_t group) const

Group group's built header content while its header is in view, else nullptr.

group_keys

const std::vector<std::string>& group_keys() const

The keys set_group_keys() was given.

grouping_enabled

bool grouping_enabled() const

Whether the keys group the rows; on by default. Off shows the rows ungrouped and keeps the keys, so turning it back on groups them again.

set_grouping_enabled

void set_grouping_enabled(bool enabled)

See grouping_enabled().

sticky_group_headers

bool sticky_group_headers() const

Whether group headers are sticky; on by default. The current group's header stays pinned under the column headers until the next group's header pushes it up. A click on it still toggles the group.

set_sticky_group_headers

void set_sticky_group_headers(bool sticky)

See sticky_group_headers().

group_count

std::size_t group_count() const

How many groups there are. 0 while the rows are not grouped.

group_key

const std::string& group_key(std::size_t group) const

A group's key, the text on its header. Empty past the last group.

group_size

std::size_t group_size(std::size_t group) const

How many rows a group holds, in or out of view. 0 past the last group.

is_group_expanded

bool is_group_expanded(std::size_t group) const

Whether a group's rows are shown.

set_group_expanded

void set_group_expanded(std::size_t group, bool expanded)

Expands or collapses a group. A keyboard cursor on a row that this hides moves to the group's header. The selection stays.

expand_all_groups

void expand_all_groups()

Expands every group.

collapse_all_groups

void collapse_all_groups()

Collapses every group.

group_of_row

std::optional<std::size_t> group_of_row(std::size_t row) const

The group a row is filed under. Unset while the rows are not grouped.

is_group_header

bool is_group_header(std::size_t position) const

Whether view position position is a group header row.

group_at_position

std::optional<std::size_t> group_at_position(std::size_t position) const

The group of the row or header at view position position. Unset past the end or while the rows are not grouped.

grid_lines_visibility

TableViewGridLinesVisibility grid_lines_visibility() const

Row separators, column separators, both (the default) or neither.

set_grid_lines_visibility

void set_grid_lines_visibility(TableViewGridLinesVisibility lines)

See grid_lines_visibility().

alternating_rows

bool alternating_rows() const

Whether rows are banded: every second row in view order gets the alternating background. Off by default.

set_alternating_rows

void set_alternating_rows(bool on)

See alternating_rows().

set_alternating_row_background

void set_alternating_row_background(Paint paint)

Sets the banding color. Empty, the default, uses the theme's banding color (palette.tabular_row_alternating under Fluent).

alternating_row_background

const Paint& alternating_row_background() const

See set_alternating_row_background().

set_empty_content

void set_empty_content(std::unique_ptr<Element> content)

Sets what the body shows, centered, while there are no rows. nullptr, the default, shows nothing. The table owns it.

empty_content

Element* empty_content() const

The empty-state content, or nullptr.

scroll_offset

float scroll_offset() const

The body's current vertical scroll offset, DIPs.

handle_shortcut

bool handle_shortcut(const KeyEvent& e) override

Handles the table's shortcuts. An Escape that closes a popup the editor opened, such as a ComboBox's list, leaves the edit open.

cursor_at

std::optional<CursorShape> cursor_at(Point p) const override

The resize cursor over a resizable header divider.

focused

bool focused() const

Whether the grid holds keyboard focus and draws its focus ring.

focusable

bool focusable() const override

Focusable while enabled and holding at least one row.

collect_focusables

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

The table, then the focusable controls in its template cells and empty content. Never the editor, because the table keeps focus while one is open.

for_each_child

void for_each_child(ChildVisitor visit) const override

The table body, the empty-state content while it shows, the realized template cells and the open editor.

automation_role

AutomationRole automation_role() const override

Reported to screen readers as a data grid. It holds a header with one item per column, and invoking a sortable one sorts by it. Then come the rows in view, each selectable, and the group headers, which expand and collapse. A row holds its cells, one per column, each named by its text. A template cell's Element or an open editor sits under its cell. As in every virtualizing list, only the rows in view are reported.

automation_children

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

The empty-state content while it shows, the header, then the rows in view.

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_parent_of

Element* automation_parent_of(const Element& child) const override

The cell around a template cell or an open editor, and the header row around a group header's content, while their row is in view.

compare_text

static std::weak_ordering compare_text(const std::string& a, const std::string& b)

The default cell comparison. Compares as numbers when both texts parse entirely as numbers, else as text, ignoring ASCII case first.

clear_dirty_override

void clear_dirty_override(DirtyFlags flags) override

Clears the row context menu too, which is listed as a child only while it shows.

measure_override

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

Desired size: the sum of the column widths, with fraction columns at their content width, by the header height plus every row. Each is clamped to available where it is bounded.

arrange_override

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

Resolves the column widths against the final width, splits the bounds into the header strip and the body, and arranges the body and the empty-state content.

render_override

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

Draws the header strip, the realized rows, the grid lines, the scroll bars and the focus ring.

pointer_override

bool pointer_override(const PointerEvent& e) override

A press on a header sorts, or starts a resize on its trailing divider. A press on a body row selects it by the selection mode's rules.

handled_pointer_override

void handled_pointer_override(const PointerEvent& e) override

A press a template cell took still commits an open edit elsewhere in the table.

wheel_override

bool wheel_override(const PointerEvent& e) override

Vertical turns scroll the rows. Horizontal turns and Shift+wheel scroll the columns sideways when they overflow.

hit_test_children

void hit_test_children(Point p, std::vector<Element*>& out) const override

The children p (in this element's layout coordinates) is tested against, topmost first. Default: what for_each_child() lists, last first. A container that draws in another order (Panel's z-index), a control with parts for_each_child() does not list, or one whose parts cover others in places (a pane drawn over the content) lists them here.

covers_children_at

bool covers_children_at(Point p) const override

Whether a part of this element drawn over its children is at p (a scroll bar over the content), so the element is hit there instead of a child. Default: false.

tick_override

void tick_override(Duration delta) override

Ticks the children and the horizontal scroll bar's fade, and commits an edit whose focus left the table.

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.

key_override

bool key_override(const KeyEvent& e) override

The row keys described in the class comment. Left and Right scroll sideways, move the current column while editing is possible, or collapse and expand the group header the cursor is on. F2 opens an editor. While one is open, Enter commits, Escape cancels and every other key goes to the editor. Ignored while disabled or unfocused.

text_override

bool text_override(const TextInputEvent& text_event) override

Typed text goes to an open editor; without one it is ignored.

focus_changed

void focus_changed(bool focused) override

Called by Window as focus arrives or leaves. Redraws. An open editor follows the table's focus. Focus leaving commits the edit on the next tick unless it has come back by then.

focus_ring_rect

Rect focus_ring_rect(const Theme& theme) const override

Inside the header cell, group header, cell or row the keyboard is on, cut to the part in view. Around the whole table otherwise, and nowhere while an editor is open.

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 TableViewSortedArgs

What on_sorted gets after a sort change.

Field Type Default
column std::optional<std::size_t> The sorted column; unset when cleared.
direction TableViewSortDirection TableViewSortDirection::None Its new direction.

struct TableViewSortingArgs

What on_sorting gets before a user sort: the column and the direction it is about to take. Return ui::Cancel from the handler to keep the current order.

Field Type Default
column std::size_t 0 The column about to be sorted.
direction TableViewSortDirection TableViewSortDirection::None The direction it is about to take.

struct TableViewColumnResizedArgs

What on_column_resized gets on every step of a user resize.

Field Type Default
column std::size_t 0 The resized column.
width float 0.0f Its new width, DIPs.

struct TableViewCellArgs

A TableViewControl's on_beginning_edit: the cell an editor is about to open on.

Field Type Default
row std::size_t 0 An index into rows().
column std::size_t 0 The column.

struct TableViewCellEditArgs

A TableViewControl's on_cell_edited: the cell and the editor's text, which the handler may rewrite to what the cell should show.

Field Type Default
row std::size_t An index into rows().
column std::size_t The column.
text std::string& The editor's text.

struct TableViewBeginningEditArgs

template <typename T> struct TableViewBeginningEditArgs

A TableView spec's on_beginning_edit: the item and the column an editor is about to open on.

Field Type Default
item T& The row's item.
column std::size_t The column.

struct TableViewCellEditedArgs

template <typename T> struct TableViewCellEditedArgs

A TableView spec's on_cell_edited: the item, the column and the editor's text.

Field Type Default
item T& The row's item (the edited copy for an editing_cell column).
column std::size_t The column.
text const std::string& The editor's text.

struct TableViewColumnWidth

Derives from ui::GridLength.

A TableView column's width, in the same units as a ui::Grid track:

Converts implicitly from a number and from a ui::GridLength, so {"Name", 140.0f} and {"Name", 1_fr} both name a column.

TableViewColumnWidth

constexpr TableViewColumnWidth(float dips)

A fixed width of dips.

TableViewColumnWidth

constexpr TableViewColumnWidth(GridLength length)

Auto, a fraction or fixed DIPs, as length says.

struct TableViewColumn

One column of a TableView: its header, its width and how the user may size, sort and pin it.

Field Type Default
header std::string Caption drawn in the header strip above the rows. A caption too wide for the column is trimmed with an ellipsis, like the cells.
width TableViewColumnWidth 100.0f Fixed DIPs including the cell padding, Auto or a fraction; see TableViewColumnWidth. Cell text past the resolved width is trimmed with a trailing ellipsis. A user resize makes the column fixed at the width it was dragged to, and fixes the fraction columns before it at the widths they had, so only the columns after it give or take the room.
min_width float 20.0f The smallest and largest width, DIPs. The resolved width and a user resize stay inside them.
max_width float Unbounded See min_width.
can_resize bool true Whether the user can drag this column's header divider. The table's can_user_resize_columns() must be on too.
can_sort bool true Whether a click on this header sorts by it. The table's can_user_sort_columns() must be on too. sort_by_column() works either way.
sort_cycle TableViewSortCycle TableViewSortCycle::AscendingDescending The directions repeated header clicks step through.
frozen bool false Whether the column stays put while the others scroll sideways. Only a leading run of frozen columns is pinned. A frozen column after an unfrozen one scrolls.
frozen_trailing bool false Whether the column stays put against the right edge while the others scroll sideways. Only a trailing run of such columns is pinned, and frozen wins on a column that sets both.
read_only bool false Whether the column's cells are never edited, even in a table that allows editing (TableViewControl::set_read_only(false)). A read-only column can still be the current one.

struct TableColumn

template <typename T> struct TableColumn

One column of a ui::TableView: its header, its width, the text it shows for an item, and how the user may size, sort and pin it.

The callables are std::function because columns are written as a braced list, which copies its entries.

Field Type Default
header std::string Caption drawn in the header strip above the rows.
width TableViewColumnWidth 100.0f A number of DIPs including the cell padding, ui::AutoSize (as wide as the header and the cells in view) or a fraction (1_fr, 2_fr: a weighted share of what the other columns leave). Header and cell text past the width is trimmed with an ellipsis.
text std::function<std::string(const T&)> The cell text for an item.
min_width float 20.0f The column's minimum and maximum width, DIPs. The resolved width and a user resize stay inside them.
max_width float Unbounded See min_width.
frozen bool false Whether the column stays put while the other columns scroll sideways. Only a leading run of frozen columns is pinned.
frozen_trailing bool false Whether the column stays put against the right edge while the other columns scroll sideways. Only a trailing run of such columns is pinned.
can_resize bool true Whether the user can drag this header's divider. The table's .can_user_resize_columns must be on too.
can_sort bool true Whether a header click sorts by this column. The table's .can_user_sort_columns must be on too.
sort_cycle TableViewSortCycle TableViewSortCycle::AscendingDescending The directions repeated header clicks step through.
compare std::function<std::weak_ordering(const T&, const T&)> How two items order for this column, for example [](const T& a, const T& b) { return a.size <=> b.size; }. Left out, the cell text is compared, as numbers when both cells are numbers.
read_only bool false Whether this column is never edited, even in a table with .read_only = false.
cell std::function<Spec(T&)> The cell's content for an item, for example [](Task& t) -> ui::Spec { return ui::CheckBox{.checked = t.done}; }, stretched over the whole cell with no padding. Built for the rows in view only, and again when the collection changes. text still sorts the column and names the cell.
editing_cell std::function<Spec(T&)> The editor for an item, shown over the cell while it is edited. It works on a copy of the item. A commit copies it back after .on_cell_edited accepts it, and a cancel drops it, so the item must be copyable. Without an editor, a text column edits in a TextBox and a column with a cell template cannot be edited.
tool_tip std::function<std::string(const T&)> The tip shown while the pointer rests on the item's cell in this column, for example the full text of a trimmed cell. Empty for none.

Enumerations

TableViewGridLinesVisibility

enum class TableViewGridLinesVisibility

Which grid lines a TableView draws. Row banding is separate (set_alternating_rows()).

Value
All Row separators and column separators (the default).
Horizontal Row separators only.
None No grid lines.
Vertical Column separators only.

TableViewSortDirection

enum class TableViewSortDirection

The direction a TableView column is sorted in.

Value
None Not sorted; the rows keep their source order.
Ascending Smallest first; the header shows an up chevron.
Descending Largest first; the header shows a down chevron.

TableViewSortCycle

enum class TableViewSortCycle

The directions repeated clicks on a column header step through. An unsorted column starts at the first one named. The *None cycles end on unsorted, so a third click restores the source order.

Value
AscendingDescending Ascending, Descending, Ascending, and so on (the default).
AscendingDescendingNone Ascending, Descending, None.
DescendingAscending Descending, Ascending, Descending, and so on. Suits metrics, largest first.
DescendingAscendingNone Descending, Ascending, None.

Tests

  • tests/test_table_view.cpp