DataGrid
DataGrid in UIToolbox shows a list of items in a table with typed cell editors. It wraps the native MultiColumnListView, so the rows are virtual: only the visible rows have elements.
Video tutorial: Data grid. All videos: Video tutorials.
<tb:DataGrid name="units" selection-mode="Multiple" style="height: 250px;">
<tb:DataGridColumn header="Name" binding-path="Name" width="130" />
<tb:DataGridColumn header="Level" binding-path="Level" width="70" />
<tb:DataGridColumn header="Class" binding-path="Class" width="100" />
<tb:DataGridColumn header="Action" cell-type="Button" button-text="Heal" width="70" sortable="false" />
</tb:DataGrid>
var grid = root.Q<DataGrid>("units");
grid.ItemsSource = units; // a List<Unit>; the edits write to the items
grid.CellValueChanged += (i, column, old, now) => Debug.Log($"{units[i].Name}: {column.Header} {old} to {now}");
grid.CellClicked += (i, column) => grid.SetValue(i, grid.GetColumn("Health")!, 100f);
grid.SetFilter("Level", level => (int)level! >= 10);
grid.SearchText = "ash";
GUIUtility.systemCopyBuffer = grid.ToCsv(selectedOnly: true);
A grid with a check box column, pages of 25 rows with page buttons, and a footer row:
<tb:DataGrid name="units" selection-column="true" page-size="25" show-pagination="true" show-footer="true">
<tb:DataGridColumn header="Name" binding-path="Name" width="130" can-hide="false" footer-text="Total" />
<tb:DataGridColumn header="Level" binding-path="Level" width="70" footer-text="Avg " />
<tb:DataGridColumn header="Notes" binding-path="Notes" width="200" visible="false" />
</tb:DataGrid>
grid.SetFooter("Level", rows => rows.Count == 0 ? "" : rows.Average(i => units[i].Level).ToString("F1"));
columnsButton.clicked += () => grid.ShowColumnMenu(new Vector2(columnsButton.worldBound.xMin, columnsButton.worldBound.yMax));
grid.VisibleColumnsChanged += () => PlayerPrefs.SetString("columns", string.Join(",", grid.VisibleColumns));
grid.SetVisibleColumns(PlayerPrefs.GetString("columns", "Name,Level").Split(','));
| Member | Description |
|---|---|
DataGridColumn.Header (header), BindingPath (binding-path), Width (width, 100), Sortable (sortable, true) |
The header text, the public field or property of the items (empty for a column without a value), the start width, and the header click sort. |
DataGridColumn.CellType (cell-type) |
Auto (the default) picks the editor from the member type: Toggle for bool, Integer for int and long, Float for float, double and decimal, Dropdown for an enum or a string with Choices, Text for a string, and Label for other types. Button sends CellClicked. |
DataGridColumn.ReadOnly (read-only), Format (format), Choices (choices), ButtonText (button-text) |
A label instead of an editor; the number or date format of a label; the comma list of a string dropdown; the button text (empty shows the value). |
DataGridColumn.Visible (visible, true), CanHide (can-hide, true) |
The column shows. A column with CanHide off is not in the column menu. |
DataGridColumn.FooterText (footer-text) |
The text of the footer cell of the column. Set it before ShowFooter, or call Refresh(). |
DataGridColumn.Name, Member, ValueType, CanWrite, ResolvedCellType, FormatValue |
The binding path or the header, the member after the grid has an item type, and the editor in use. A member without a public setter gets a label. |
ItemsSource, ItemType, Refresh |
The items (an IList). A struct item is set back in the list. Call Refresh() after code adds, removes or changes items. |
Columns, AddColumn, RemoveColumn, GetColumn, AutoColumns (auto-columns, true) |
The columns in the declared order. A grid without columns adds one per public field and property. Items of another type replace these, but not columns that code or UXML added. |
SetCellRenderer(column, (index, item) => element) |
Builds the cells of a column with your code. The grid calls it each time it binds a row. |
GetValue, SetValue |
Read and write a cell by source index. SetValue converts text and numbers to the member type and returns false when it cannot, or when the setter throws (the grid logs that exception). |
ReadOnly (read-only) |
All cells show labels. Toggles are disabled. Buttons still work. |
AllowSorting (allow-sorting), SortByColumn, ClearSort |
A header click sorts; Shift+click adds a column to the sort. The sort is stable. Nulls come first, numbers and dates sort by value, and text sorts A to Z in any case. |
SetFilter, ClearFilter, ClearFilters, SearchText, ShownIndices |
One filter per column, and a search that shows the rows with a cell text that contains the text in any case. The search reads only the columns that show. ShownIndices gives the source indices of the shown rows, in the shown order. |
SelectionMode (selection-mode, Multiple), SelectedIndices, SelectedItems, SetSelection, ClearSelection, SelectionChanged |
The selection by source index. It stays on the same items after a sort. A filter that hides a selected row removes it from the selection. |
ItemsChosen |
A double click or Enter on rows, with the selected items. |
CellValueChanged, CellClicked |
An edit or SetValue that changed a value; a button click. Both give the source index and the column. |
AllowColumnResize (allow-column-resize), AllowColumnReorder (allow-column-reorder), RowHeight (row-height, 24) |
Header edge drags, header drags, and the fixed row height. |
GroupBy (group-by), Groups, SetGroupExpanded, IsGroupExpanded, SetAllGroupsExpanded |
Groups the shown rows by the value of one column, with a header row per group ("Class: Mage (3)"). The groups sort by value, and follow the direction of a sort on that column. The rows keep the sort inside a group. Groups gives the header texts of the values in the shown order. |
AllowColumnHide (allow-column-hide, true), ShowColumnMenu(position) |
A right-click on the header shows the column menu: one check item per column. ShowColumnMenu shows it at a panel position, for a button of your own. |
VisibleColumns, SetVisibleColumns(names), VisibleColumnsChanged |
The names of the columns that show, in the shown order. SetVisibleColumns shows these columns and hides the others. The event comes after each column that shows or hides. |
SelectionColumn (selection-column) |
Adds a first column with a check box per row, and a check box in the header for all shown rows. |
PageSize (page-size, 0), Page, PageCount, PageChanged |
The rows on one page; 0 shows all rows. Page starts at 1 and is clamped to PageCount. PageChanged gives the new page. |
ShowPagination (show-pagination), Pagination |
Shows page buttons under the grid while PageSize is above 0. Pagination is that Pagination control, for its own settings such as SiblingCount. |
FilteredIndices |
The source indices of the rows of all pages, in the shown order. Without pages it is the same as ShownIndices. |
ShowFooter (show-footer), SetFooter(column, rows => text) |
Shows a footer row with one cell per column. The cell shows FooterText, then the text of the callback. The callback gets FilteredIndices. Null removes it. |
ToCsv(selectedOnly) |
RFC 4180 CSV of the shown or the selected rows, in the shown row and column order, with invariant numbers. With pages, it holds the rows of all pages. Columns without a member, hidden columns and group headers are left out. |
View |
The native MultiColumnListView, for the settings that the grid does not wrap. |
- An edit that the setter refuses logs the exception, and the cell shows the item value again.
- In the UI Builder, a grid without items shows three sample rows in a dashed outline. A column of the UXML file shows its header and the row number, such as "Name 1". The sample does not show in Play mode or in a build, and the UI Builder does not save it to the UXML file.
- Text that does not convert, such as "five" for a number, returns false from
SetValuewithout a log. - A click on a group header collapses or expands the group. Header rows are never in the selection, and
ShownIndicesleaves them out. - An edit of the group column moves the row to its new group.
Column menu
- A right-click on the header shows a menu with one item per column. A check mark shows that the column is visible. A click on an item shows or hides the column and closes the menu.
- The last column that shows has a disabled item, so the grid always has one column.
- A hidden column keeps its filter and its sort. The search and the CSV leave it out.
VisibleColumnshas the names in the shown order.SetVisibleColumnschanges which columns show, not their order.- The header has no keyboard focus. For keyboard and gamepad users, add a button that calls
ShowColumnMenu; the arrow keys and Submit then work in the menu.
Selection column
- The check boxes show the selection of the grid and change it.
SelectedIndices,SetSelectionandSelectionChangedwork as before; a row click still selects. - The header box selects all shown rows, or none. It shows a mixed state when some of the shown rows are selected.
- With
selection-mode="Single", a row box selects only its row and the header box is disabled. WithNone, the boxes do nothing. - The column is not in
ColumnsorVisibleColumns, and not in the CSV.
Pages
- The filters and the sort come first; then the grid cuts the rows in pages. A sort or a filter keeps the page number when that page still exists, or goes to the last page.
ShownIndiceshas the rows of the current page.FilteredIndicesandToCsv()have the rows of all pages.ToCsv(selectedOnly: true)has the selected rows.- The selection holds rows of the current page only. A page change removes the rows of the old page from the selection and sends
SelectionChanged. The header box of the selection column selects the rows of the current page. - With groups, a page holds
PageSizeitems, counted in group order; the header rows do not count. A group that goes over a page end shows its header on both pages, with the count of the whole group. - A collapsed group keeps its place in the pages, so the page count does not change with a collapse. A page can then show only headers.
Groupshas the groups of all pages.
Footer
- The footer row stays under the rows; it does not scroll up or down. Its cells follow the column widths, the column order and the horizontal scroll.
- The grid calls the callbacks of
SetFooterafter a filter, a sort, a group collapse, a page change and an edit. CallRefresh()after your code changes the items. - The callback gets the rows of all pages, without the rows of collapsed groups. For the rows of the current page, read
ShownIndicesin the callback.
Tips:
- For a sum, a count or an average, write one line:
grid.SetFooter("Health", rows => rows.Sum(i => units[i].Health).ToString("F0")). - Set
can-hide="false"on the column that names the row, so the user cannot hide it. - To keep the user's columns, save
VisibleColumnsin theVisibleColumnsChangedhandler and give the names toSetVisibleColumnsat the start. - Use pages for a list that the user reads page by page. The rows are virtual, so a long list without pages is also fast.
Styling
| Class | Element |
|---|---|
tb-data-grid |
The grid (grows; surface background, border). tb-data-grid--read-only is added with ReadOnly. |
tb-data-grid__view |
The native MultiColumnListView. The grid colors its header (surface-alt, bold text), its rows (surface, surface-alt on odd rows, surface-hover under the pointer, accent when selected) and its sort arrow. |
tb-data-grid__cell |
A cell. It holds one editor, label or button, without the field margins. The editors are flat until the pointer or the focus is on them. |
tb-data-grid__cell-label |
The text of a label cell (ellipsis). |
tb-data-grid__group |
The label of a group header row. It fills the row: --tb-color-surface-alt background, bold text, --tb-color-surface-hover on hover. |
tb-data-grid__select, tb-data-grid__select-all |
The check box of a row and of the header in the selection column: native Toggle elements without margins. |
tb-data-grid__footer |
The footer: --tb-color-surface-alt background and a top border. It clips its row. |
tb-data-grid__footer-row |
The row of the footer cells (24 px high at least). |
tb-data-grid__footer-cell |
A footer cell, a Label: bold text, ellipsis. The grid sets its width. |
tb-data-grid__pagination |
The built-in Pagination: centered, with a top border. |
To put the page buttons on the right and make the footer text normal:
.tb-data-grid__pagination { justify-content: flex-end; }
.tb-data-grid__footer-cell { -unity-font-style: normal; }