Skip to main content
Table of Contents

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.

DataGrid in tree mode with one group expanded and one collapsed DataGrid in tree mode with one group expanded and one collapsed

Quick example

Build a two-level treePascalC++
// 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

Inline example 1PascalC++
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:

Inline example 2PascalC++
// 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 > 0 that 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

Inline example 3PascalC++
// 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

Inline example 4PascalC++
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

Inline example 5PascalC++
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.

Inline example 6PascalC++
// 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:

Inline example 7PascalC++
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.

Inline example 8PascalC++
// 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:

Indent child rows by node levelPascalC++
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.

Combining tree nodes and bulk expansionPascalC++
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();
}
  • Grid.RowNodeLevel[ARow] — nesting level (0-based)
  • Grid.RowNodeState[ARow] — grnsExpanded / grnsCollapsed
  • Grid.RowNodeChildCount[ARow] — direct child count
  • Grid.Options.Column.NodeIndex — column that hosts the expand/collapse button
  • Grid.IsRowNode(ARow) — True if the row has an expand button
  • Grid.GetParentRow(ARow) — direct parent row index
  • Grid.GetRootRow(ARow) — root ancestor row index
  • Grid.GetMaxLevel — deepest nesting level in the grid
  • Grid.CollapseAllNodes / Grid.ExpandAllNodes
  • Grid.AddNodeRow(ARow, AChildCount, ALevel, AMerge) — convert a plain row into a collapsible node header
  • OnCellBeforeNodeExpand / OnCellAfterNodeExpand
  • OnCellBeforeNodeCollapse / OnCellAfterNodeCollapse
  • OnRowExpand / OnRowCollapse

See also