Skip to main content
Table of Contents

Cells, columns & coordinates

How the grid identifies cells, what fixed and freeze columns mean, and the difference between physical and visible row indices.

Cell coordinates

Every cell has a (Column, Row) pair, both 0-based:

Inline example 1PascalC++
Grid.Cells[0, 0] := 'ID';        // top-left
Grid.Cells[1, 0] := 'Name';
Grid.Cells[2, 0] := 'Country';

Grid.Cells[0, 1] := 1;           // first data row
Grid.Cells[1, 1] := 'John Doe';
Grid.Cells[2, 1] := 'USA';
Grid->Cells[0][0] = L"ID";        // top-left
Grid->Cells[1][0] = L"Name";
Grid->Cells[2][0] = L"Country";

Grid->Cells[0][1] = 1;           // first data row
Grid->Cells[1][1] = L"John Doe";
Grid->Cells[2][1] = L"USA";

The grid records use the helper record TTMSFNCDataGridCellCoord:

Building and reading a cell coordinatePascalC++
procedure TForm1.ShowCellCoordinate;
var
  Cell: TTMSFNCDataGridCellCoord;
begin
  Cell := MakeCell(2, 5);          // column 2, row 5
  ShowMessage(Format('%d, %d', [Cell.Column, Cell.Row]));
end;
void __fastcall TForm1::ShowCellCoordinate()
{
  TTMSFNCDataGridCellCoord Cell = Fmx::Tmsfncdatagridcore::MakeCell(2, 5);          // column 2, row 5
  ShowMessage(Format("%d, %d", ARRAYOFCONST((Cell.Column, Cell.Row))));
}

Cell ranges use TTMSFNCDataGridCellCoordRange:

Inline example 2PascalC++
Grid.Selection := MakeCellRange(0, 1, 3, 4);    // L=0,T=1,R=3,B=4
Grid->Selection = MakeCellRange(0, 1, 3, 4);    // L=0,T=1,R=3,B=4

Fixed columns and rows

Fixed columns and rows are always visible — they don't scroll:

Inline example 3PascalC++
Grid.FixedColumnCount      := 1;     // leftmost column always visible
Grid.FixedRightColumnCount := 1;     // rightmost column always visible
Grid.FixedRowCount         := 1;     // top header row always visible
Grid.FixedBottomRowCount   := 1;     // bottom footer row always visible
Grid->FixedColumnCount      = 1;     // leftmost column always visible
Grid->FixedRightColumnCount = 1;     // rightmost column always visible
Grid->FixedRowCount         = 1;     // top header row always visible
Grid->FixedBottomRowCount   = 1;     // bottom footer row always visible

Use them for headers, row numbers, and totals.

Freeze columns and rows

Freeze columns and rows behave like fixed ones at first, but they scroll past when the user moves far enough:

Inline example 4PascalC++
Grid.FreezeColumnCount := 2;         // sticky for the first scroll, then scrolls away
Grid.FreezeRowCount    := 1;
Grid->FreezeColumnCount = 2;         // sticky for the first scroll, then scrolls away
Grid->FreezeRowCount    = 1;

Common case: keep the ID and Name columns visible while exploring distant columns; once the user scrolls past them they get out of the way.

Physical vs visible rows

When rows are filtered or hidden, the grid maintains two row numbering systems:

Index What it means
Physical The storage slot (0..RowCount-1). Stable across filtering and hiding.
Visual The sequential rendered position after filter and hidden state are applied. Use RealToVisualRow to obtain it; returns -1 if the row is not rendered.

To find the visual position of a physical row, or to convert back:

Inline example 5PascalC++
var VisualPos := Grid.RealToVisualRow(PhysicalRow);
// VisualPos = -1 → row is filtered or hidden
// VisualPos = n  → it is the n-th rendered row (0-based)

var PhysRow := Grid.VisualToRealRow(VisualPos);
var VisualPos = Grid->RealToVisualRow(PhysicalRow);
// VisualPos = -1 → row is filtered or hidden
// VisualPos = n  → it is the n-th rendered row (0-based)

var PhysRow = Grid->VisualToRealRow(VisualPos);

For alternating-row banding use RealToVisualRow inside OnGetCellLayout to determine whether the row occupies an odd or even rendered slot — see Layout priority for a full example.

Hidden vs filtered rows

Both states make a row invisible, but they have different sources:

State API When it's set
Hidden Grid.HiddenRows[ARow] := True Programmatic — you decide.
Filtered Grid.Filter.Add + Grid.ApplyFilter Automatic — based on filter conditions.

Use Grid.IsRowHidden(ARow) to check either case in one call. See the Filtering guide for details.

Inline example 6PascalC++
// Programmatically hide a row
Grid.HiddenRows[5] := True;

// Programmatically show it again
Grid.HiddenRows[5] := False;

// Walk only the rows that are actually displayed (neither hidden nor filtered out)
for var R := Grid.FixedRowCount to Grid.RowCount - 1 do
  if not Grid.IsRowHidden(R) then
    ProcessRow(R);
// Programmatically hide a row
Grid->HiddenRows[5] = true;

// Programmatically show it again
Grid->HiddenRows[5] = false;

// Walk only the rows that are actually displayed (neither hidden nor filtered out)
for var R = Grid->FixedRowCount to Grid->RowCount - 1 do
  if (!Grid->IsRowHidden(R))
    ProcessRow(R);

Hidden vs invisible columns

Same idea on the column axis:

Inline example 7PascalC++
Grid.HideColumn(4);                  // also: Grid.Columns[4].Visible := False
Grid.UnhideColumn(4);
Grid.UnhideAllColumns;
Grid->HideColumn(4);                  // also: Grid->Columns[4].Visible = false
Grid->UnhideColumn(4);
Grid->UnhideAllColumns;

See Column management.

Selection coordinates

Grid.Selection is a TTMSFNCDataGridCellCoordRange covering the user's current selection. Grid.FocusedCell is the single coordinate with keyboard focus inside that range.

When the selection mode allows disjoint selection (e.g. gsmDisjunctRow, gsmDisjunctCellRange), use the indexed properties to enumerate:

Inline example 8PascalC++
for var i := 0 to Grid.RowCount - 1 do
  if Grid.SelectedRows[i] then
    ProcessRow(i);
for var i = 0 to Grid->RowCount - 1 do
  if (Grid->SelectedRows[i])
    ProcessRow(i);

See Selection for the full picture.

The three coordinate spaces

Every row and column lives in one of three coordinate spaces at any given time:

Space What it reflects Changes when…
Physical Storage index (0..RowCount-1, 0..ColumnCount-1). Rows or columns are added or deleted.
Display Order after sort (rows) and reorder (columns). Fixed rows/columns are excluded and always map 1:1. The user sorts a column or drags a column header.
Visual Display order with filter and hidden state applied. Returns -1 for any row or column that is not rendered. The user filters, hides rows/columns, or scrolls.

The grid always stores and events data in physical coordinates. The conversion functions let you translate between spaces when you need to build display-order lists, respond to visual positions, or iterate only what the user can see.

Physical vs display indices under sort and filter

When sorting or filtering is active the order in which rows appear to the user differs from the physical storage order. Two families of conversion functions handle this:

Function Direction Notes
RealToDisplayRow(ARow) Physical → display slot Returns -1 if the row is filtered out.
DisplayToRealRow(ARow) Display slot → physical The inverse.
RealToDisplayColumn(ACol) Physical → display slot Accounts for column reordering.
DisplayToRealColumn(ACol) Display slot → physical The inverse.
RealToDisplayCell(ACell) Physical → display Delegates to the row and column variants.
DisplayToRealCell(ACell) Display → physical The inverse.
Inline example 9PascalC++
// Convert a physical row index to its current display slot
// (returns -1 when the row is filtered out or hidden)
var DisplaySlot := Grid.RealToDisplayRow(PhysRow);

// Convert a display-order slot back to the physical row index
var PhysRow := Grid.DisplayToRealRow(DisplaySlot);

// Column reordering uses the same pattern
var PhysCol   := Grid.DisplayToRealColumn(VisualColIndex);
var VisualCol := Grid.RealToDisplayColumn(PhysCol);
// Convert a physical row index to its current display slot
// (returns -1 when the row is filtered out or hidden)
var DisplaySlot = Grid->RealToDisplayRow(PhysRow);

// Convert a display-order slot back to the physical row index
var PhysRow = Grid->DisplayToRealRow(DisplaySlot);

// Column reordering uses the same pattern
var PhysCol   = Grid->DisplayToRealColumn(VisualColIndex);
var VisualCol = Grid->RealToDisplayColumn(PhysCol);

When you need these

Iterating over visible rows in display order — after sorting, physical row 0 may not be the first visible row. Iterate display slots and convert back to physical when accessing data:

Inline example 10PascalC++
var i: Integer;
begin
  for i := Grid.FixedRowCount to Grid.RowCount - 1 do
  begin
    if Grid.IsRowFiltered(i) then Continue;
    var DisplaySlot := Grid.RealToDisplayRow(i);
    // DisplaySlot is the visual rank (0-based after the header)
  end;
end;
int i;
{
  for (i = Grid->FixedRowCount; i <= Grid->RowCount - 1; ++i)
  {
    if (Grid->IsRowFiltered(i)) continue;
    int DisplaySlot = Grid->RealToDisplayRow(i);
    // DisplaySlot is the visual rank (0-based after the header)
  }
}

Responding to a cell click and looking up the physical row — the cell coordinate passed to click events is always the physical coordinate, so no conversion is needed for those. Conversion is needed when you build a display-order list yourself:

Inline example 11PascalC++
// Build a list of visible rows in the order the user sees them
var DisplayList: TArray<Integer>;
SetLength(DisplayList, Grid.VisibleRowCount);
for var i := 0 to Grid.VisibleRowCount - 1 do
  DisplayList[i] := Grid.DisplayToRealRow(i + Grid.FixedRowCount);
// Build a list of visible rows in the order the user sees them
System::DynamicArray<int> DisplayList;
DisplayList.Length = Grid->VisibleRowCount;
for (int i = 0; i <= Grid->VisibleRowCount - 1; ++i)
  DisplayList[i] = Grid->DisplayToRealRow(i + Grid->FixedRowCount);

After filtering: check whether a row is still visible

Inline example 12PascalC++
if Grid.RealToDisplayRow(PhysicalRow) = -1 then
  ShowMessage('Row is filtered out');
if (Grid->RealToDisplayRow(PhysicalRow) = -1)
  ShowMessage(L"Row is filtered out");

These functions were fixed in v1.8.1.1 to return correct indices when a filter is active. Filtered rows consistently return -1 from RealToDisplayRow.

Visual coordinates — the rendered index

Display indices account for sort and reorder but not for filter or hidden state. Use the Visual family when you need the actual rendered index — for instance, to build a zero-based list of only what is drawn on screen, in order:

Function Direction Notes
RealToVisualRow(ARow) Physical → rendered index Returns -1 if the row is filtered or hidden.
VisualToRealRow(AVisualRow) Rendered index → physical Returns -1 if out of range.
RealToVisualColumn(ACol) Physical → rendered index Returns -1 if the column is hidden.
VisualToRealColumn(AVisualCol) Rendered index → physical Returns -1 if out of range.

Display vs Visual — choosing the right one

Inline example 13PascalC++
// Display order: respects sort, but includes filtered-out rows
// Use when iterating for data processing (filter state may change)
for var i := Grid.FixedRowCount to Grid.RowCount - 1 do
begin
  var Phys := Grid.DisplayToRealRow(i);
  ProcessRow(Phys);
end;

// Visual order: only rows actually rendered, in render order
// Use when building an export of exactly what the user sees
for var v := Grid.FixedRowCount to Grid.VisibleRowCount - 1 do
begin
  var Phys := Grid.VisualToRealRow(v);
  if Phys >= 0 then
    ExportRow(Phys);
end;
// Display order: respects sort, but includes filtered-out rows
// Use when iterating for data processing (filter state may change)
for var i = Grid->FixedRowCount to Grid->RowCount - 1 do
{
  var Phys = Grid->DisplayToRealRow(i);
  ProcessRow(Phys);
}

// Visual order: only rows actually rendered, in render order
// Use when building an export of exactly what the user sees
for var v = Grid->FixedRowCount to Grid->VisibleRowCount - 1 do
{
  var Phys = Grid->VisualToRealRow(v);
  if (Phys >= 0)
    ExportRow(Phys);
}

Checking whether a column is rendered

Inline example 14PascalC++
// Is physical column 4 currently visible to the user?
if Grid.RealToVisualColumn(4) = -1 then
  ShowMessage('Column 4 is hidden or filtered out');
// Is physical column 4 currently visible to the user?
if (Grid->RealToVisualColumn(4) = -1)
  ShowMessage(L"Column 4 is hidden or filtered out");

IsRowDisplayed vs IsRowVisible

Two similarly-named predicates test different things:

Method Returns True when…
IsRowDisplayed(ARow) The row passes the current filter and is not hidden. It may be off-screen (scrolled past).
IsRowVisible(ARow) The row is displayed (above) and currently within the visible scroll viewport — i.e., actually rendered on screen right now.

Use IsRowDisplayed to iterate all filterable/visible rows regardless of scroll position (the common case for data processing). Use IsRowVisible to react only to what the user can actually see — for example, refreshing a status bar that summarises only the on-screen rows.

Inline example 15PascalC++
// All rows that survived the filter (anywhere in the dataset)
for var R := Grid.FixedRowCount to Grid.RowCount - 1 do
  if Grid.IsRowDisplayed(R) then
    ProcessRow(R);

// Only the rows currently rendered on screen
for var R := Grid.FixedRowCount to Grid.RowCount - 1 do
  if Grid.IsRowVisible(R) then
    UpdateStatusForRow(R);
// All rows that survived the filter (anywhere in the dataset)
for var R = Grid->FixedRowCount to Grid->RowCount - 1 do
  if (Grid->IsRowDisplayed(R))
    ProcessRow(R);

// Only the rows currently rendered on screen
for var R = Grid->FixedRowCount to Grid->RowCount - 1 do
  if (Grid->IsRowVisible(R))
    UpdateStatusForRow(R);

Column and cell visibility predicates

The same displayed/visible distinction applies to columns and individual cells:

Method Axis Returns True when…
IsColumnDisplayed(ACol) Column Column is not hidden. May be scrolled off-screen.
IsColumnVisible(ACol) Column Column is displayed and in the scroll viewport.
IsColumnHidden(ACol) Column Column is explicitly hidden.
IsCellDisplayed(ACell) Cell Both the cell's column and row pass IsColumnDisplayed / IsRowDisplayed.
IsCellVisible(ACell) Cell Both the cell's column and row pass IsColumnVisible / IsRowVisible.
IsCellHidden(ACell) Cell Either the cell's column or its row is hidden.
Inline example 16PascalC++
// Iterate all rendered cells in the current column
var C := MakeCell(2, 0);
for var R := Grid.FixedRowCount to Grid.RowCount - 1 do
begin
  C.Row := R;
  if Grid.IsCellDisplayed(C) then
    ProcessCell(C);
end;
// Iterate all rendered cells in the current column
var C = MakeCell(2, 0);
for var R = Grid->FixedRowCount to Grid->RowCount - 1 do
{
  C->Row = R;
  if (Grid->IsCellDisplayed(C))
    ProcessCell(C);
}

HiddenColumns and HiddenRows let you get or set hidden state in bulk:

Inline example 17PascalC++
Grid.HiddenColumns[3] := True;   // hide column 3
Grid.HiddenRows[10]   := True;   // hide row 10
Grid->HiddenColumns[3] = true;   // hide column 3
Grid->HiddenRows[10]   = true;   // hide row 10

HiddenColumnCount and HiddenRowCount report how many are currently hidden.

Pixel coordinates — XY to cell

All XY… methods accept pixel coordinates relative to the control's client area and return logical cell or area information. They are useful in custom mouse-event handlers.

Method Returns Notes
XYToCell(X, Y) TTMSFNCDataGridCellCoord The cell under the pointer. Column = -1 or Row = -1 if outside the grid.
XYToColumn(X, Y) Integer Column index under X, or -1. Equivalent to XYToCell(X, Y).Column.
XYToRow(X, Y) Integer Row index under Y, or -1. Equivalent to XYToCell(X, Y).Row.
XYToHeader(X, Y) Boolean True if the point is inside the header area.
XYToFooter(X, Y) Boolean True if the point is inside the footer area.
XYToHeaderGroup(X, Y) TTMSFNCDataGridHeaderGroup The header-group element under the pointer.
XYToRenderer(X, Y) TTMSFNCDataGridRenderer The nested renderer (sub-grid) under the pointer, or Self.
Resolving a pixel position to a cell in OnMouseDownPascalC++
procedure TForm1.TMSFNCDataGrid1MouseDown(Sender: TObject; Button: TMouseButton;
  Shift: TShiftState; X, Y: Single);
var
  Cell: TTMSFNCDataGridCellCoord;
begin
  Cell := TMSFNCDataGrid1.XYToCell(X, Y);
  if (Cell.Column >= 0) and (Cell.Row >= 0) then
    ShowMessage(Format('Clicked cell (%d, %d)', [Cell.Column, Cell.Row]));
end;
void __fastcall TForm1::TMSFNCDataGrid1MouseDown(TObject *Sender, TMouseButton Button,
  TShiftState Shift, float X, float Y)
{
  TTMSFNCDataGridCellCoord Cell = TMSFNCDataGrid1->XYToCell(X, Y);
  if ((Cell.Column >= 0) && (Cell.Row >= 0))
    ShowMessage(Format("Clicked cell (%d, %d)", ARRAYOFCONST((Cell.Column, Cell.Row))));
}

XYToCell returns physical coordinates — the same space that Cells[], events, and all other APIs use. No conversion is needed after calling it.

Cell and column sizing

GetCellSize returns the pixel dimensions of a cell, including any merged span:

Inline example 18PascalC++
var S := Grid.GetCellSize(MakeCell(2, 3));
// S.cx = pixel width  (covers merged columns if the cell spans multiple)
// S.cy = pixel height (covers merged rows)
var S = Grid->GetCellSize(MakeCell(2, 3));
// S->cx = pixel width  (covers merged columns if the cell spans multiple)
// S->cy = pixel height (covers merged rows)

For individual column widths and row heights, use the indexed properties directly:

Inline example 19PascalC++
Grid.ColumnWidths[2] := 120;   // set column 2 to 120 px
Grid.RowHeights[0]   := 32;    // set header row height
Grid->ColumnWidths[2] = 120;   // set column 2 to 120 px
Grid->RowHeights[0]   = 32;    // set header row height

These are the same values the renderer uses when laying out cells. Summing them from column 0 gives you the left edge of any column, and from row 0 gives you the top edge of any row.

GoToCell scrolls the grid so the target cell is visible, then optionally selects it:

Inline example 20PascalC++
Grid.GoToCell(MakeCell(5, 20));             // scroll to cell, keep selection
Grid.GoToCell(MakeCell(5, 20), True);       // scroll and select
Grid->GoToCell(MakeCell(5, 20));             // scroll to cell, keep selection
Grid->GoToCell(MakeCell(5, 20), true);       // scroll and select

When you need to move relative to the current position — skipping hidden or filtered rows and columns — use the NextVisible* / PreviousVisible* helpers. They each return a TTMSFNCDataGridCellCoord:

Inline example 21PascalC++
// Step forward one visible row from the focused cell
var Next := Grid.NextVisibleRow(
  Grid.FocusedCell.Column,
  Grid.FocusedCell.Row
);
if Next.Row >= 0 then
  Grid.GoToCell(Next);
// Step forward one visible row from the focused cell
var Next = Grid->NextVisibleRow(
  Grid->FocusedCell->Column,
  Grid->FocusedCell->Row
);
if (Next->Row >= 0)
  Grid->GoToCell(Next);
Method Moves
NextVisibleRow(ACol, ARow) Down — next non-hidden, non-filtered row.
PreviousVisibleRow(ACol, ARow) Up — previous non-hidden, non-filtered row.
NextVisibleColumn(ACol, ARow) Right — next non-hidden column.
PreviousVisibleColumn(ACol, ARow) Left — previous non-hidden column.

All four return a cell with Row = -1 or Column = -1 when no further visible row or column exists.

Combining fixed regions, hidden rows and pixel hit-testing

Coordinate concepts rarely appear alone. A grid with a fixed header and frozen columns, a few hidden rows, and mouse interaction has to translate between pixel space, physical rows and visual rows all at once. Use this pattern when you handle clicks or hover feedback and must report which row the user actually sees, as opposed to which storage slot it occupies. The example configures the fixed and freeze regions, hides a row, and then converts a pixel position with XYToCell, maps the physical row to its visual position with RealToVisualRow, selects the cell with a MakeCellRange and checks IsRowHidden. A visual position of -1 tells you the row is not rendered.

Fixed regions, hidden rows and pixel hit-testing togetherPascalC++
procedure TForm1.SetupCoordinateRegions;
begin
  Grid.FixedColumnCount := 1;      // row numbers always visible
  Grid.FixedRowCount := 1;         // header row always visible
  Grid.FreezeColumnCount := 2;     // sticky until the user scrolls far enough
  Grid.HiddenRows[5] := True;      // physical row 5 no longer has a visual slot
end;

procedure TForm1.DescribeCellAt(X, Y: Single);
var
  Cell: TTMSFNCDataGridCellCoord;
  VisualRow: Integer;
begin
  // Pixel space -> physical coordinate.
  Cell := Grid.XYToCell(X, Y);
  if (Cell.Column < 0) or (Cell.Row < 0) then
    Exit;

  // Physical -> visual: -1 would mean the row is filtered or hidden.
  VisualRow := Grid.RealToVisualRow(Cell.Row);

  // Select the clicked cell with a coordinate range.
  Grid.Selection := MakeCellRange(Cell.Column, Cell.Row, Cell.Column, Cell.Row);

  Caption := Format('Physical row %d, visual row %d, hidden: %s',
    [Cell.Row, VisualRow, BoolToStr(Grid.IsRowHidden(Cell.Row), True)]);
end;
void __fastcall TForm1::SetupCoordinateRegions()
{
  Grid->FixedColumnCount = 1;      // row numbers always visible
  Grid->FixedRowCount = 1;         // header row always visible
  Grid->FreezeColumnCount = 2;     // sticky until the user scrolls far enough
  Grid->HiddenRows[5] = true;      // physical row 5 no longer has a visual slot
}

void __fastcall TForm1::DescribeCellAt(float X, float Y)
{
  // Pixel space -> physical coordinate.
  TTMSFNCDataGridCellCoord Cell = Grid->XYToCell(X, Y);
  if ((Cell.Column < 0) || (Cell.Row < 0))
    return;

  // Physical -> visual: -1 would mean the row is filtered or hidden.
  int VisualRow = Grid->RealToVisualRow(Cell.Row);

  // Select the clicked cell with a coordinate range.
  Grid->Selection = Fmx::Tmsfncdatagridcore::MakeCellRange(Cell.Column, Cell.Row, Cell.Column, Cell.Row);

  Caption = Format("Physical row %d, visual row %d, hidden: %s",
    ARRAYOFCONST((Cell.Row, VisualRow, BoolToStr(Grid->IsRowHidden(Cell.Row), true))));
}

See also