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.Columns.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

  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
  StatusBar1.SimpleText := Format('Row %d expanded', [ARow]);
end;

procedure TForm1.GridCellAfterNodeCollapse(Sender: TObject;
  AColumn, ARow: Integer);
begin
  StatusBar1.SimpleText := Format('Row %d collapsed', [ARow]);
end;

Step by step

1. Configure the grid

Grid.ColumnCount   := 3;
Grid.FixedRowCount := 1;

// Column 0 shows the expand/collapse button
Grid.Options.Columns.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';

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

// 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

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;
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;

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';

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]);

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;
  • Grid.RowNodeLevel[ARow] — nesting level (0-based)
  • Grid.RowNodeState[ARow]grnsExpanded / grnsCollapsed
  • Grid.RowNodeChildCount[ARow] — direct child count
  • Grid.Options.Columns.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