Tree mode
Display hierarchical data as an expandable/collapsible tree without any additional components. Rows carry a level and a node state; the grid handles collapse, expand, indentation, and icons automatically.
Overview
Tree mode is row-level metadata: each row can be a node (expandable parent) or a leaf (plain row with a level ≥ 1). There is no separate tree model class — you set RowNodeLevel and RowNodeState directly on the grid rows.
| Property | Purpose |
|---|---|
RowNodeLevel[row] |
0 = top-level, 1 = child, 2 = grandchild, … |
RowNodeState[row] |
grnsExpanded or grnsCollapsed (only relevant for node rows that have children) |
Options.Column.NodeIndex |
Which column shows the expand/collapse button (default: 0) |
The grid determines which rows are children of a node automatically: a node at level n owns all consecutive rows with level n+1 that follow it until the next level-n (or lower) row.
Quick example
// Tree mode basic — build a two-level hierarchy and handle expand/collapse.
// Drop a TTMSFNCDataGrid on a form named Grid.
procedure TForm1.BuildTree;
var
Row: Integer;
begin
Grid.BeginUpdate;
try
Grid.ColumnCount := 3;
Grid.FixedRowCount := 1;
Grid.RowCount := 1; // will grow as we add
// Header row
Grid.Cells[0, 0] := 'Name';
Grid.Cells[1, 0] := 'Type';
Grid.Cells[2, 0] := 'Value';
// The expand/collapse button appears in column 0
Grid.Options.Column.NodeIndex := 0;
// --- Parent A (level 0, starts collapsed) ---
Row := Grid.RowCount;
Grid.RowCount := Row + 1;
Grid.RowNodeLevel[Row] := 0;
Grid.RowNodeState[Row] := grnsCollapsed;
Grid.Cells[0, Row] := 'Group A';
Grid.Cells[1, Row] := 'Parent';
// Child A1 (level 1)
Inc(Row); Grid.RowCount := Row + 1;
Grid.RowNodeLevel[Row] := 1;
Grid.Cells[0, Row] := 'Item A1';
Grid.Cells[1, Row] := 'Child';
Grid.Cells[2, Row] := 10;
// Child A2 (level 1)
Inc(Row); Grid.RowCount := Row + 1;
Grid.RowNodeLevel[Row] := 1;
Grid.Cells[0, Row] := 'Item A2';
Grid.Cells[1, Row] := 'Child';
Grid.Cells[2, Row] := 20;
// --- Parent B (level 0, starts expanded) ---
Inc(Row); Grid.RowCount := Row + 1;
Grid.RowNodeLevel[Row] := 0;
Grid.RowNodeState[Row] := grnsExpanded;
Grid.Cells[0, Row] := 'Group B';
Grid.Cells[1, Row] := 'Parent';
// Child B1 (level 1)
Inc(Row); Grid.RowCount := Row + 1;
Grid.RowNodeLevel[Row] := 1;
Grid.Cells[0, Row] := 'Item B1';
Grid.Cells[1, Row] := 'Child';
Grid.Cells[2, Row] := 30;
finally
Grid.EndUpdate;
end;
end;
// React to expand/collapse
procedure TForm1.GridCellBeforeNodeExpand(Sender: TObject;
AColumn, ARow: Integer; var ACanExpand: Boolean);
begin
// Optionally prevent expanding certain rows
// ACanExpand := Grid.RowNodeChildCount[ARow] > 0;
end;
procedure TForm1.GridCellAfterNodeExpand(Sender: TObject;
AColumn, ARow: Integer);
begin
Caption := Format('Row %d expanded', [ARow]);
end;
procedure TForm1.GridCellAfterNodeCollapse(Sender: TObject;
AColumn, ARow: Integer);
begin
Caption := Format('Row %d collapsed', [ARow]);
end;
void __fastcall TForm1::BuildTree()
{
Grid->BeginUpdate();
try
{
Grid->ColumnCount = 3;
Grid->FixedRowCount = 1;
Grid->RowCount = 1; // will grow as we add
// Header row
Grid->Strings[0][0] = "Name";
Grid->Strings[1][0] = "Type";
Grid->Strings[2][0] = "Value";
// The expand/collapse button appears in column 0
Grid->Options->Column->NodeIndex = 0;
// --- Parent A (level 0, starts collapsed) ---
int Row = Grid->RowCount;
Grid->RowCount = Row + 1;
Grid->RowNodeLevel[Row] = 0;
Grid->RowNodeState[Row] = grnsCollapsed;
Grid->Strings[0][Row] = "Group A";
Grid->Strings[1][Row] = "Parent";
// Child A1 (level 1)
Row++; Grid->RowCount = Row + 1;
Grid->RowNodeLevel[Row] = 1;
Grid->Strings[0][Row] = "Item A1";
Grid->Strings[1][Row] = "Child";
Grid->Ints[2][Row] = 10;
// Child A2 (level 1)
Row++; Grid->RowCount = Row + 1;
Grid->RowNodeLevel[Row] = 1;
Grid->Strings[0][Row] = "Item A2";
Grid->Strings[1][Row] = "Child";
Grid->Ints[2][Row] = 20;
// --- Parent B (level 0, starts expanded) ---
Row++; Grid->RowCount = Row + 1;
Grid->RowNodeLevel[Row] = 0;
Grid->RowNodeState[Row] = grnsExpanded;
Grid->Strings[0][Row] = "Group B";
Grid->Strings[1][Row] = "Parent";
// Child B1 (level 1)
Row++; Grid->RowCount = Row + 1;
Grid->RowNodeLevel[Row] = 1;
Grid->Strings[0][Row] = "Item B1";
Grid->Strings[1][Row] = "Child";
Grid->Ints[2][Row] = 30;
}
__finally
{
Grid->EndUpdate();
}
}
// React to expand/collapse
void __fastcall TForm1::GridCellBeforeNodeExpand(TObject *Sender,
const TTMSFNCDataGridCellCoord &ACell, bool &ACanExpand)
{
// Optionally prevent expanding certain rows
// ACanExpand = Grid->RowNodeChildCount[ARow] > 0;
}
void __fastcall TForm1::GridCellAfterNodeExpand(TObject *Sender,
const TTMSFNCDataGridCellCoord &ACell)
{
TTMSFNCDataGridCellCoord Cell = ACell;
Caption = Format("Row %d expanded", ARRAYOFCONST((Cell.Row)));
}
void __fastcall TForm1::GridCellAfterNodeCollapse(TObject *Sender,
const TTMSFNCDataGridCellCoord &ACell)
{
TTMSFNCDataGridCellCoord Cell = ACell;
Caption = Format("Row %d collapsed", ARRAYOFCONST((Cell.Row)));
}
Step by step
1. Configure the grid
Grid.ColumnCount := 3;
Grid.FixedRowCount := 1;
// Column 0 shows the expand/collapse button
Grid.Options.Column.NodeIndex := 0;
Grid->ColumnCount = 3;
Grid->FixedRowCount = 1;
// Column 0 shows the expand/collapse button
Grid->Options->Column->NodeIndex = 0;
2. Add node rows
Set RowNodeLevel and RowNodeState for every row:
// Top-level parent — starts collapsed
Grid.RowNodeLevel[1] := 0;
Grid.RowNodeState[1] := grnsCollapsed;
Grid.Cells[0, 1] := 'Europe';
// Children
Grid.RowNodeLevel[2] := 1;
Grid.Cells[0, 2] := 'Germany';
Grid.RowNodeLevel[3] := 1;
Grid.Cells[0, 3] := 'France';
// Second top-level node
Grid.RowNodeLevel[4] := 0;
Grid.RowNodeState[4] := grnsExpanded;
Grid.Cells[0, 4] := 'Americas';
Grid.RowNodeLevel[5] := 1;
Grid.Cells[0, 5] := 'USA';
// Top-level parent — starts collapsed
Grid->RowNodeLevel[1] = 0;
Grid->RowNodeState[1] = grnsCollapsed;
Grid->Cells[0][1] = L"Europe";
// Children
Grid->RowNodeLevel[2] = 1;
Grid->Cells[0][2] = L"Germany";
Grid->RowNodeLevel[3] = 1;
Grid->Cells[0][3] = L"France";
// Second top-level node
Grid->RowNodeLevel[4] = 0;
Grid->RowNodeState[4] = grnsExpanded;
Grid->Cells[0][4] = L"Americas";
Grid->RowNodeLevel[5] = 1;
Grid->Cells[0][5] = L"USA";
Note: rows with
RowNodeLevel > 0that have no children are leaves — they don't display an expand button. Only a row that is followed by rows at a higher level is treated as a node.
3. Expand and collapse programmatically
// Collapse or expand a specific row
Grid.RowNodeState[4] := grnsCollapsed;
Grid.RowNodeState[4] := grnsExpanded;
// Collapse or expand all nodes at once
Grid.CollapseAllNodes;
Grid.ExpandAllNodes;
// Collapse or expand a specific row
Grid->RowNodeState[4] = grnsCollapsed;
Grid->RowNodeState[4] = grnsExpanded;
// Collapse or expand all nodes at once
Grid->CollapseAllNodes;
Grid->ExpandAllNodes;
4. Inspect tree structure at runtime
var Level := Grid.RowNodeLevel[ARow];
var State := Grid.RowNodeState[ARow];
var IsNode := Grid.IsRowNode(ARow); // True if row has an expand button
var Parent := Grid.GetParentRow(ARow); // row index of direct parent (-1 for roots)
var Root := Grid.GetRootRow(ARow); // row index of the root ancestor
var Depth := Grid.GetMaxLevel; // deepest level in the whole grid
var Level = Grid->RowNodeLevel[ARow];
var State = Grid->RowNodeState[ARow];
var IsNode = Grid->IsRowNode(ARow); // true if row has an expand button
var Parent = Grid->GetParentRow(ARow); // row index of direct parent (-1 for roots)
var Root = Grid->GetRootRow(ARow); // row index of the root ancestor
var Depth = Grid->GetMaxLevel; // deepest level in the whole grid
5. React to expand and collapse
procedure TForm1.GridCellBeforeNodeExpand(Sender: TObject;
AColumn, ARow: Integer; var ACanExpand: Boolean);
begin
// Load children on demand here, then allow expansion
LoadChildren(ARow);
ACanExpand := True;
end;
procedure TForm1.GridCellAfterNodeExpand(Sender: TObject;
AColumn, ARow: Integer);
begin
Caption := Format('%d node expanded', [ARow]);
end;
void __fastcall TForm1::GridCellBeforeNodeExpand(TObject *Sender;
AColumn, int ARow; bool &ACanExpand)
{
// Load children on demand here, then allow expansion
LoadChildren(ARow);
ACanExpand = true;
}
void __fastcall TForm1::GridCellAfterNodeExpand(TObject *Sender;
AColumn, int ARow)
{
Caption = Format(L"%d node expanded", [ARow]);
}
| Event | Fires |
|---|---|
OnCellBeforeNodeExpand |
Before the expand animation. Set ACanExpand := False to block it. |
OnCellAfterNodeExpand |
After expansion is complete. |
OnCellBeforeNodeCollapse |
Before collapse. Set ACanCollapse := False to block it. |
OnCellAfterNodeCollapse |
After collapse is complete. |
OnRowExpand |
Data-layer event: fires when a row's node state changes to expanded. |
OnRowCollapse |
Data-layer event: fires when a row's node state changes to collapsed. |
6. Lazy (on-demand) child loading
A common pattern is to load children only when a node is first expanded. Use AddNodeRow to convert an existing empty row into a node header, then set RowNodeLevel on its children.
// Mark nodes that still need loading (using the Objects[] slot for a flag)
Grid.Objects[0, ParentRow] := Pointer(1); // 1 = needs load
procedure TForm1.GridCellBeforeNodeExpand(Sender: TObject;
AColumn, ARow: Integer; var ACanExpand: Boolean);
begin
if Grid.Objects[0, ARow] = Pointer(1) then
begin
LoadChildrenFromDatabase(ARow);
Grid.Objects[0, ARow] := nil; // mark as loaded
end;
ACanExpand := True;
end;
procedure TForm1.LoadChildrenFromDatabase(AParentRow: Integer);
var
Children: TStringList;
R, i: Integer;
begin
Children := FetchChildRecords(Grid.Cells[0, AParentRow]);
try
// Insert rows for the children
Grid.InsertRows(AParentRow + 1, Children.Count);
// Convert the parent into a node header that owns <Children.Count> child rows
Grid.AddNodeRow(AParentRow, Children.Count, Grid.RowNodeLevel[AParentRow], True);
// Populate children
for i := 0 to Children.Count - 1 do
begin
R := AParentRow + 1 + i;
Grid.RowNodeLevel[R] := Grid.RowNodeLevel[AParentRow] + 1;
Grid.Cells[0, R] := Children[i];
end;
finally
Children.Free;
end;
end;
// Mark nodes that still need loading (using the Objects[] slot for a flag)
Grid->Objects[0][ParentRow] = reinterpret_cast<TObject *>(1); // 1 = needs load
void __fastcall TForm1::GridCellBeforeNodeExpand(TObject *Sender,
int AColumn, int ARow, bool &ACanExpand)
{
if (Grid->Objects[0][ARow] == reinterpret_cast<TObject *>(1))
{
LoadChildrenFromDatabase(ARow);
Grid->Objects[0][ARow] = nullptr; // mark as loaded
}
ACanExpand = true;
}
void __fastcall TForm1::LoadChildrenFromDatabase(int AParentRow)
{
TStringList *Children;
int R, i;
Children = FetchChildRecords(Grid->Cells[0][AParentRow]);
try
// Insert rows for the children
Grid->InsertRows(AParentRow + 1, Children->Count);
// Convert the parent into a node header that owns <Children->Count> child rows
Grid->AddNodeRow(AParentRow, Children->Count, Grid->RowNodeLevel[AParentRow], true);
// Populate children
for (i = 0; i <= Children->Count - 1; ++i)
{
R = AParentRow + 1 + i;
Grid->RowNodeLevel[R] = Grid->RowNodeLevel[AParentRow] + 1;
Grid->Cells[0][R] = Children[i];
}
}
__finally
{
Children->Free();
}
}
AddNodeRow(ARow, AChildCount, ALevel, AMerge) parameters:
| Parameter | Description |
|---|---|
ARow |
The row index to convert into a node header. |
AChildCount |
Number of direct child rows it owns. |
ALevel |
Nesting depth (0 = top level). |
AMerge |
When True, merges the header cell across all columns. |
7. Three-level hierarchy
Levels can go as deep as needed:
Grid.RowNodeLevel[1] := 0; Grid.RowNodeState[1] := grnsExpanded; Grid.Cells[0, 1] := 'Continent';
Grid.RowNodeLevel[2] := 1; Grid.RowNodeState[2] := grnsExpanded; Grid.Cells[0, 2] := 'Country';
Grid.RowNodeLevel[3] := 2; Grid.Cells[0, 3] := 'City';
Grid.RowNodeLevel[4] := 2; Grid.Cells[0, 4] := 'City';
Grid.RowNodeLevel[5] := 1; Grid.RowNodeState[5] := grnsCollapsed; Grid.Cells[0, 5] := 'Country';
Grid.RowNodeLevel[6] := 2; Grid.Cells[0, 6] := 'City';
Grid->RowNodeLevel[1] = 0; Grid->RowNodeState[1] = grnsExpanded; Grid->Cells[0][1] = L"Continent";
Grid->RowNodeLevel[2] = 1; Grid->RowNodeState[2] = grnsExpanded; Grid->Cells[0][2] = L"Country";
Grid->RowNodeLevel[3] = 2; Grid->Cells[0][3] = L"City";
Grid->RowNodeLevel[4] = 2; Grid->Cells[0][4] = L"City";
Grid->RowNodeLevel[5] = 1; Grid->RowNodeState[5] = grnsCollapsed; Grid->Cells[0][5] = L"Country";
Grid->RowNodeLevel[6] = 2; Grid->Cells[0][6] = L"City";
8. Combine tree with grouping
Tree mode and data-layer grouping are independent features. If you use the grid's built-in grouping, the grouping engine may insert node rows automatically. For hand-built hierarchies use RowNodeLevel directly, not grouping.
// Use EITHER tree mode OR built-in grouping — not both.
// Tree mode: assign node levels manually (do NOT call Grid.Group)
Grid.RowNodeLevel[2] := 1; // parent node
Grid.RowNodeLevel[3] := 2; // child
Grid.RowNodeLevel[4] := 2; // child
// Grouping: let the data layer insert group rows automatically (do NOT set RowNodeLevel)
// Grid.Group([CountryColumn]);
// Use EITHER tree mode OR built-in grouping — !both.
// Tree mode: assign node levels manually (do !call Grid->Group)
Grid->RowNodeLevel[2] = 1; // parent node
Grid->RowNodeLevel[3] = 2; // child
Grid->RowNodeLevel[4] = 2; // child
// Grouping: let the data layer insert group rows automatically (do !set RowNodeLevel)
// Grid->Group([CountryColumn]);
Appearance
The expand/collapse icons can be replaced with custom bitmaps. See Custom icons for ExpandIcon and CollapseIcon.
Row indentation and node-button size are controlled by the grid's cell layout at each level. Use OnGetCellLayout to indent child rows:
procedure TForm1.GridGetCellLayout(Sender: TObject; ACell: TTMSFNCDataGridCell);
var
R: TRectF;
begin
if ACell.Row > 0 then
begin
R := ACell.Rect;
R.Left := R.Left + Grid.RowNodeLevel[ACell.Row] * 20;
ACell.Rect := R;
end;
end;
void __fastcall TForm1::GridGetCellLayout(TObject *Sender, TTMSFNCDataGridCell *ACell)
{
if (ACell->Row > 0)
{
TRectF R = ACell->Rect;
R.Left = R.Left + Grid->RowNodeLevel[ACell->Row] * 20;
ACell->Rect = R;
}
}
Combining tree nodes and bulk expansion
A tree is most useful when the hierarchy, the node button column, and bulk expand or collapse controls work together: levels define the structure, the node column shows the buttons, and expand-all or collapse-all commands let users scan or drill down quickly. Combine them for category lists, folder views, or grouped reports. The example below sets the node button column, builds a small two-level hierarchy with one collapsed and one expanded parent, indents child rows through the cell layout event, and offers methods that expand or collapse every node.
procedure TForm1.BuildCombinedTree;
begin
Grid.BeginUpdate;
try
Grid.ColumnCount := 2;
Grid.FixedRowCount := 1;
Grid.RowCount := 6;
Grid.Options.Column.NodeIndex := 0;
Grid.Cells[0, 0] := 'Name';
Grid.Cells[1, 0] := 'Value';
Grid.RowNodeLevel[1] := 0;
Grid.RowNodeState[1] := grnsExpanded;
Grid.Cells[0, 1] := 'Europe';
Grid.RowNodeLevel[2] := 1;
Grid.Cells[0, 2] := 'Germany';
Grid.Cells[1, 2] := 83;
Grid.RowNodeLevel[3] := 1;
Grid.Cells[0, 3] := 'France';
Grid.Cells[1, 3] := 68;
Grid.RowNodeLevel[4] := 0;
Grid.RowNodeState[4] := grnsCollapsed;
Grid.Cells[0, 4] := 'Americas';
Grid.RowNodeLevel[5] := 1;
Grid.Cells[0, 5] := 'Canada';
Grid.Cells[1, 5] := 40;
finally
Grid.EndUpdate;
end;
// Indent child rows
Grid.OnGetCellLayout := GridGetCellLayout;
end;
procedure TForm1.GridGetCellLayout(Sender: TObject; ACell: TTMSFNCDataGridCell);
var
R: TRectF;
begin
if ACell.Row > 0 then
begin
R := ACell.Rect;
R.Left := R.Left + Grid.RowNodeLevel[ACell.Row] * 20;
ACell.Rect := R;
end;
end;
procedure TForm1.ExpandAllButtonClick(Sender: TObject);
begin
Grid.ExpandAllNodes;
end;
procedure TForm1.CollapseAllButtonClick(Sender: TObject);
begin
Grid.CollapseAllNodes;
end;
void __fastcall TForm1::BuildCombinedTree()
{
Grid->BeginUpdate();
try
{
Grid->ColumnCount = 2;
Grid->FixedRowCount = 1;
Grid->RowCount = 6;
Grid->Options->Column->NodeIndex = 0;
Grid->Strings[0][0] = "Name";
Grid->Strings[1][0] = "Value";
Grid->RowNodeLevel[1] = 0;
Grid->RowNodeState[1] = grnsExpanded;
Grid->Strings[0][1] = "Europe";
Grid->RowNodeLevel[2] = 1;
Grid->Strings[0][2] = "Germany";
Grid->Ints[1][2] = 83;
Grid->RowNodeLevel[3] = 1;
Grid->Strings[0][3] = "France";
Grid->Ints[1][3] = 68;
Grid->RowNodeLevel[4] = 0;
Grid->RowNodeState[4] = grnsCollapsed;
Grid->Strings[0][4] = "Americas";
Grid->RowNodeLevel[5] = 1;
Grid->Strings[0][5] = "Canada";
Grid->Ints[1][5] = 40;
}
__finally
{
Grid->EndUpdate();
}
// Indent child rows
Grid->OnGetCellLayout = GridGetCellLayout;
}
void __fastcall TForm1::GridGetCellLayout(TObject *Sender, TTMSFNCDataGridCell *ACell)
{
if (ACell->Row > 0)
{
TRectF R = ACell->Rect;
R.Left = R.Left + Grid->RowNodeLevel[ACell->Row] * 20;
ACell->Rect = R;
}
}
void __fastcall TForm1::ExpandAllButtonClick(TObject *Sender)
{
Grid->ExpandAllNodes();
}
void __fastcall TForm1::CollapseAllButtonClick(TObject *Sender)
{
Grid->CollapseAllNodes();
}
Related API
Grid.RowNodeLevel[ARow]— nesting level (0-based)Grid.RowNodeState[ARow]—grnsExpanded/grnsCollapsedGrid.RowNodeChildCount[ARow]— direct child countGrid.Options.Column.NodeIndex— column that hosts the expand/collapse buttonGrid.IsRowNode(ARow)— True if the row has an expand buttonGrid.GetParentRow(ARow)— direct parent row indexGrid.GetRootRow(ARow)— root ancestor row indexGrid.GetMaxLevel— deepest nesting level in the gridGrid.CollapseAllNodes/Grid.ExpandAllNodesGrid.AddNodeRow(ARow, AChildCount, ALevel, AMerge)— convert a plain row into a collapsible node headerOnCellBeforeNodeExpand/OnCellAfterNodeExpandOnCellBeforeNodeCollapse/OnCellAfterNodeCollapseOnRowExpand/OnRowCollapse
See also
- Grouping — data-driven grouping that creates node rows automatically.
- Custom icons — replace expand/collapse icons.
- Cell controls — embed controls in tree rows.
- Data, formatting & conversion —
Objects[]for tagging tree rows. - Demo:
Demo/FMX/DataGrid/Advanced/Tree