Skip to main content
Table of Contents

Drawing & grid events

Customize empty space, draw overlays after the grid, and initialize rows or columns as they are inserted or appended.

Overview

Most visual customization belongs in Appearance and theming, Custom cells, or Cell controls. Use the drawing and structural events in this chapter when you need to customize areas outside normal cells or react immediately to row/column structure changes.

Empty space drawing

When StretchScrollBars is False (the default), the scroll bars run only alongside the scrollable cells, which leaves small regions next to them beside the fixed rows and columns. The grid paints these regions after the cells, with the fixed-cell (header) fill by default, and raises the empty-space events for each one. TTMSFNCDataGridEmptySpaceKind tells you which region is being drawn. A region exists only while its scroll bar is visible and the fixed rows or columns it sits beside are present; with StretchScrollBars set to True none of them is painted. The area below the last row or to the right of the last column is the grid background, not an empty-space region.

Value Area
gekColumnTop Strip above the vertical scroll bar, beside the fixed header rows. Needs a visible vertical scroll bar and FixedRowCount above 0.
gekColumnBottom Strip under the vertical scroll bar, beside the fixed bottom rows. Painted only while the vertical scroll bar is visible, StretchScrollBars is False and FixedBottomRowCount is above 0.
gekRowLeft Strip left of the horizontal scroll bar, below the fixed left columns. Needs a visible horizontal scroll bar and FixedColumnCount above 0.
gekRowRight Strip right of the horizontal scroll bar, below the fixed right columns. Needs a visible horizontal scroll bar and FixedRightColumnCount above 0.
gekColumnBottomRowRight Corner square where the vertical and horizontal scroll bars meet. Needs both scroll bars visible.

Use OnBeforeDrawEmptySpace to replace the default fill. The example gives the grid a header row and more rows than fit, so the vertical scroll bar and the gekColumnTop strip above it appear, then paints that strip in an accent colour instead of the header fill:

Replace the default empty-space fillPascalC++
procedure TForm1.SetupEmptySpace;
begin
  // gekColumnTop is the strip above the vertical scroll bar, beside the fixed
  // header rows. It exists only while the vertical scroll bar is visible and
  // StretchScrollBars is False (the default), so give the grid a header row
  // and more rows than fit in the control.
  Grid.BeginUpdate;
  try
    Grid.StretchScrollBars := False;
    Grid.FixedRowCount := 1;
    Grid.RowCount := 100;
  finally
    Grid.EndUpdate;
  end;
  Grid.OnBeforeDrawEmptySpace := GridBeforeDrawEmptySpace;
end;

procedure TForm1.GridBeforeDrawEmptySpace(Sender: TObject;
  AGraphics: TTMSFNCGraphics; ARect: TRectF;
  AKind: TTMSFNCDataGridEmptySpaceKind; var ACanDraw: Boolean);
begin
  // By default the strip gets the header fill; paint an accent instead.
  if AKind = gekColumnTop then
  begin
    AGraphics.Fill.Kind := gfkSolid;
    AGraphics.Fill.Color := gcDarkorange;
    AGraphics.Stroke.Kind := gskNone;
    AGraphics.DrawRectangle(ARect);
    ACanDraw := False;
  end;
end;
void __fastcall TForm1::SetupEmptySpace()
{
  // gekColumnTop is the strip above the vertical scroll bar, beside the fixed
  // header rows. It exists only while the vertical scroll bar is visible and
  // StretchScrollBars is false (the default), so give the grid a header row
  // and more rows than fit in the control.
  Grid->BeginUpdate();
  try
  {
    Grid->StretchScrollBars = false;
    Grid->FixedRowCount = 1;
    Grid->RowCount = 100;
  }
  __finally
  {
    Grid->EndUpdate();
  }
  Grid->OnBeforeDrawEmptySpace = GridBeforeDrawEmptySpace;
}

void __fastcall TForm1::GridBeforeDrawEmptySpace(TObject *Sender,
  TTMSFNCGraphics *AGraphics, const TRectF &ARect,
  TTMSFNCDataGridEmptySpaceKind AKind, bool &ACanDraw)
{
  // By default the strip gets the header fill; paint an accent instead.
  if (AKind == gekColumnTop)
  {
    AGraphics->Fill->Kind = gfkSolid;
    AGraphics->Fill->Color = gcDarkorange;
    AGraphics->Stroke->Kind = gskNone;
    AGraphics->DrawRectangle(ARect);
    ACanDraw = false;
  }
}
DataGrid with a vertical scroll bar; OnBeforeDrawEmptySpace paints the gekColumnTop strip above the scroll bar orange instead of the header fill DataGrid with a vertical scroll bar; OnBeforeDrawEmptySpace paints the gekColumnTop strip above the scroll bar orange instead of the header fill, dark theme

Use OnAfterDrawEmptySpace to draw on top of the default background. It fires only when the default fill was painted, that is when OnBeforeDrawEmptySpace left ACanDraw set to True. The example adds enough rows and columns for both scroll bars, then draws a small size-grip mark in the gekColumnBottomRowRight corner. That corner is only about one scroll bar wide, so keep anything drawn there to a glyph or a few lines rather than text:

Draw on top of the empty-space backgroundPascalC++
procedure TForm1.SetupCornerGrip;
begin
  // gekColumnBottomRowRight is the square where the vertical and horizontal
  // scroll bars meet. It exists only while both scroll bars are visible and
  // StretchScrollBars is False (the default), so give the grid more rows and
  // columns than fit in the control.
  Grid.BeginUpdate;
  try
    Grid.StretchScrollBars := False;
    Grid.FixedRowCount := 1;
    Grid.ColumnCount := 30;
    Grid.RowCount := 100;
  finally
    Grid.EndUpdate;
  end;
  Grid.OnAfterDrawEmptySpace := GridAfterDrawEmptySpace;
end;

procedure TForm1.GridAfterDrawEmptySpace(Sender: TObject;
  AGraphics: TTMSFNCGraphics; ARect: TRectF;
  AKind: TTMSFNCDataGridEmptySpaceKind);
var
  I: Integer;
  D: Single;
begin
  // Runs after the default fill, so only draw the mark on top of it. The corner
  // is about one scroll bar wide: three short diagonal lines fit.
  if AKind = gekColumnBottomRowRight then
  begin
    AGraphics.Stroke.Kind := gskSolid;
    AGraphics.Stroke.Color := gcGray;
    for I := 1 to 3 do
    begin
      D := I * 4;
      AGraphics.DrawLine(PointF(ARect.Right - 3 - D, ARect.Bottom - 3),
        PointF(ARect.Right - 3, ARect.Bottom - 3 - D));
    end;
  end;
end;
void __fastcall TForm1::SetupCornerGrip()
{
  // gekColumnBottomRowRight is the square where the vertical and horizontal
  // scroll bars meet. It exists only while both scroll bars are visible and
  // StretchScrollBars is false (the default), so give the grid more rows and
  // columns than fit in the control.
  Grid->BeginUpdate();
  try
  {
    Grid->StretchScrollBars = false;
    Grid->FixedRowCount = 1;
    Grid->ColumnCount = 30;
    Grid->RowCount = 100;
  }
  __finally
  {
    Grid->EndUpdate();
  }
  Grid->OnAfterDrawEmptySpace = GridAfterDrawEmptySpace;
}

void __fastcall TForm1::GridAfterDrawEmptySpace(TObject *Sender,
  TTMSFNCGraphics *AGraphics, const TRectF &ARect,
  TTMSFNCDataGridEmptySpaceKind AKind)
{
  // Runs after the default fill, so only draw the mark on top of it. The corner
  // is about one scroll bar wide: three short diagonal lines fit.
  if (AKind == gekColumnBottomRowRight)
  {
    AGraphics->Stroke->Kind = gskSolid;
    AGraphics->Stroke->Color = gcGray;
    for (int I = 1; I <= 3; I++)
    {
      float D = I * 4;
      AGraphics->DrawLine(PointF(ARect.Right - 3 - D, ARect.Bottom - 3),
        PointF(ARect.Right - 3, ARect.Bottom - 3 - D));
    }
  }
}

Post-paint overlays

Use OnAfterDrawGrid for overlays that are not tied to one cell: watermarks, drag feedback, selection summaries, diagnostics, or empty-state messages. Keep this handler light; it runs every time the grid repaints.

A watermark is the canonical example — large faded text drawn diagonally across the whole grid rectangle after the cells have painted:

Draw a diagonal watermark over the gridPascalC++
procedure TForm1.GridAfterDrawGrid(Sender: TObject;
  AGraphics: TTMSFNCGraphics; ARect: TRectF);
begin
  // Drawn after the cells paint, so it reads as a watermark over the data.
  AGraphics.SetFontSize(44);
  AGraphics.SetFontColor($55C0392B);                 // ~33% opacity brick red
  AGraphics.DrawText(ARect, 'CONFIDENTIAL', False,
    gtaCenter, gtaCenter, gttNone, -28);             // -28 degree diagonal
end;
void __fastcall TForm1::GridAfterDrawGrid(TObject *Sender,
  TTMSFNCGraphics *AGraphics, const TRectF &ARect)
{
  // Drawn after the cells paint, so it reads as a watermark over the data.
  AGraphics->SetFontSize(44);
  AGraphics->SetFontColor((TTMSFNCGraphicsColor)0x55C0392B);   // ~33% opacity brick red
  AGraphics->DrawText(ARect, "CONFIDENTIAL", false,
    gtaCenter, gtaCenter, gttNone, -28);                       // -28 degree diagonal
}
DataGrid with a faded diagonal CONFIDENTIAL watermark drawn over the populated rows via OnAfterDrawGrid DataGrid with a faded diagonal CONFIDENTIAL watermark drawn over the populated rows in dark theme

The same event also handles empty-state messages — draw centered text only when the grid has no data rows:

Draw a post-paint empty-state overlayPascalC++
procedure TForm1.GridAfterDrawGrid(Sender: TObject;
  AGraphics: TTMSFNCGraphics; ARect: TRectF);
begin
  if Grid.RowCount = Grid.FixedRowCount then
  begin
    AGraphics.Font.Color := gcSilver;
    AGraphics.DrawText(ARect, 'No rows', False, gtaCenter, gtaCenter);
  end;
end;
void __fastcall TForm1::GridAfterDrawGrid(TObject *Sender,
  TTMSFNCGraphics *AGraphics, const TRectF &ARect)
{
  if (Grid->RowCount == Grid->FixedRowCount)
  {
    AGraphics->Font->Color = gcSilver;
    AGraphics->DrawText(ARect, "No rows", false, gtaCenter, gtaCenter);
  }
}

Append row and column events

Append events fire after new rows or columns are added at the end of the grid.

Inline example 1PascalC++
procedure TForm1.GridAppendRow(Sender: TObject; ARow: Integer);
begin
  Grid.Cells[0, ARow] := DateToStr(Now);
  Grid.Cells[1, ARow] := 'New entry';
end;
void __fastcall TForm1::GridAppendRow(TObject *Sender; int ARow)
{
  Grid->Cells[0][ARow] = DateToStr(Now);
  Grid->Cells[1][ARow] = L"New entry";
}
Inline example 2PascalC++
procedure TForm1.GridAppendColumn(Sender: TObject; AColumn: Integer);
begin
  Grid.Columns[AColumn].Header.Caption := Format('Column %d', [AColumn]);
end;
void __fastcall TForm1::GridAppendColumn(TObject *Sender; int AColumn)
{
  Grid->Columns[AColumn].Header->Caption = Format(L"Column %d", [AColumn]);
}

Use insert events when rows or columns are inserted at a specific position. Use append events when the new row or column is always added after the current end.

Insert row and column events

Use the OnCanInsertRow / OnInsertRow and OnCanInsertColumn / OnInsertColumn pairs to validate or initialize insertions at arbitrary positions. The "can" variant fires before insertion and exposes a var ACanInsert guard; the plain variant fires after the row or column is added.

Inline example 3PascalC++
procedure TForm1.GridCanInsertRow(Sender: TObject;
  ARow: Integer; var ACanInsert: Boolean);
begin
  ACanInsert := not ReadOnlyMode;
end;
void __fastcall TForm1::GridCanInsertRow(TObject *Sender;
  int ARow; bool &ACanInsert)
{
  ACanInsert = !ReadOnlyMode;
}

Cell control support callbacks

For advanced cell-control scenarios, the grid can ask whether a real control should be used and which extra properties should be applied:

Event Use it for
OnGetCellSupportsRealControl Decide whether a cell can host a live framework control.
OnGetCellControlExtraProperties Supply extra cache keys or control-specific properties.
OnGetCellProperties Customize cell elements before rendering.

These events are useful when a column contains controls with different visual state, owner data, or behavior that cannot be derived from the cell value alone.

Combining empty-space, overlay and append events

The drawing and structural events work best as a set. A list-style grid with a pinned totals footer starts empty, shows a message until the first row arrives, and grows as the user appends rows from the keyboard. The example sets RowViewMode to grvmControl so the content area spans the full control height: the footer sits at the bottom edge, and the ARect passed to OnAfterDrawGrid (the content clip rectangle) covers the whole body. With the default grvmTotalRows, ARect ends after the last painted row, so in an empty grid it covers only the header row and centred text lands on top of the column captions. OnAfterDrawGrid draws "No entries yet" while there are no data rows, and OnAppendRow seeds each row added by the Down arrow key (Options.Keyboard.AppendRowOnArrowKeyDown) and updates the entry count in the footer. Once the rows overflow and the vertical scroll bar appears, OnBeforeDrawEmptySpace replaces the gekColumnBottom strip under the scroll bar with a marker for the totals row. Because both paint handlers run on every repaint, keep them cheap and free of allocations.

Empty DataGrid with a header row and a pinned totals footer; OnAfterDrawGrid draws the No entries yet message centred in the body Empty DataGrid with a header row and a pinned totals footer; OnAfterDrawGrid draws the No entries yet message centred in the body, dark theme
Populated DataGrid with a vertical scroll bar; OnBeforeDrawEmptySpace paints an orange Sigma marker in the gekColumnBottom strip beside the totals footer, which reports 12 entries after two appended rows Populated DataGrid with a vertical scroll bar; OnBeforeDrawEmptySpace paints an orange Sigma marker in the gekColumnBottom strip beside the totals footer, dark theme
Empty space, overlay and append events togetherPascalC++
procedure TForm1.SetupDrawingEvents;
begin
  Grid.BeginUpdate;
  try
    Grid.FixedRowCount := 1;
    Grid.FixedBottomRowCount := 1;   // totals footer
    Grid.RowCount := 2;              // header + footer, no data rows yet
    Grid.Cells[0, 1] := 'Total';
    Grid.Cells[1, 1] := '0 entries';
    // Span the full control height: the footer is pinned to the bottom edge
    // and OnAfterDrawGrid receives the whole body, not just the painted rows.
    Grid.RowViewMode := grvmControl;
    // Down arrow on the last data row appends a row and fires OnAppendRow.
    Grid.Options.Keyboard.AppendRowOnArrowKeyDown := True;
  finally
    Grid.EndUpdate;
  end;

  Grid.OnBeforeDrawEmptySpace := GridBeforeDrawEmptySpace;
  Grid.OnAfterDrawGrid := GridAfterDrawGrid;
  Grid.OnAppendRow := GridAppendRow;
end;

procedure TForm1.GridBeforeDrawEmptySpace(Sender: TObject;
  AGraphics: TTMSFNCGraphics; ARect: TRectF;
  AKind: TTMSFNCDataGridEmptySpaceKind; var ACanDraw: Boolean);
begin
  // gekColumnBottom is the strip under the vertical scroll bar, beside the
  // fixed bottom rows. It is painted only while the scroll bar is visible.
  if AKind = gekColumnBottom then
  begin
    AGraphics.Fill.Kind := gfkSolid;
    AGraphics.Fill.Color := gcDarkorange;
    AGraphics.Stroke.Kind := gskNone;
    AGraphics.DrawRectangle(ARect);
    AGraphics.Font.Color := gcWhite;
    AGraphics.DrawText(ARect, #$03A3, False, gtaCenter, gtaCenter);  // Sigma
    ACanDraw := False;
  end;
end;

procedure TForm1.GridAfterDrawGrid(Sender: TObject;
  AGraphics: TTMSFNCGraphics; ARect: TRectF);
begin
  // ARect is the content clip rectangle. With RowViewMode = grvmControl it
  // covers the full body, so the message lands between header and footer.
  if Grid.RowCount = Grid.FixedRowCount + Grid.FixedBottomRowCount then
  begin
    AGraphics.Font.Size := 14;
    AGraphics.Font.Color := gcGray;
    AGraphics.DrawText(ARect, 'No entries yet', False, gtaCenter, gtaCenter);
  end;
end;

procedure TForm1.GridAppendRow(Sender: TObject; ARow: Integer);
var
  DataRows: Integer;
begin
  // The new row is inserted above the footer, which stays the last row.
  Grid.Cells[0, ARow] := DateToStr(Date);
  Grid.Cells[1, ARow] := 'New entry';
  DataRows := Grid.RowCount - Grid.FixedRowCount - Grid.FixedBottomRowCount;
  Grid.Cells[1, Grid.RowCount - 1] := Format('%d entries', [DataRows]);
end;
void __fastcall TForm1::SetupDrawingEvents()
{
  Grid->BeginUpdate();
  try
  {
    Grid->FixedRowCount = 1;
    Grid->FixedBottomRowCount = 1;   // totals footer
    Grid->RowCount = 2;              // header + footer, no data rows yet
    Grid->Strings[0][1] = "Total";
    Grid->Strings[1][1] = "0 entries";
    // Span the full control height: the footer is pinned to the bottom edge
    // and OnAfterDrawGrid receives the whole body, not just the painted rows.
    Grid->RowViewMode = grvmControl;
    // Down arrow on the last data row appends a row and fires OnAppendRow.
    Grid->Options->Keyboard->AppendRowOnArrowKeyDown = true;
  }
  __finally
  {
    Grid->EndUpdate();
  }

  Grid->OnBeforeDrawEmptySpace = GridBeforeDrawEmptySpace;
  Grid->OnAfterDrawGrid = GridAfterDrawGrid;
  Grid->OnAppendRow = GridAppendRow;
}

void __fastcall TForm1::GridBeforeDrawEmptySpace(TObject *Sender,
  TTMSFNCGraphics *AGraphics, const TRectF &ARect,
  TTMSFNCDataGridEmptySpaceKind AKind, bool &ACanDraw)
{
  // gekColumnBottom is the strip under the vertical scroll bar, beside the
  // fixed bottom rows. It is painted only while the scroll bar is visible.
  if (AKind == gekColumnBottom)
  {
    AGraphics->Fill->Kind = gfkSolid;
    AGraphics->Fill->Color = gcDarkorange;
    AGraphics->Stroke->Kind = gskNone;
    AGraphics->DrawRectangle(ARect);
    AGraphics->Font->Color = gcWhite;
    AGraphics->DrawText(ARect, System::UnicodeString(System::WideChar(0x03A3)),
      false, gtaCenter, gtaCenter);  // Sigma
    ACanDraw = false;
  }
}

void __fastcall TForm1::GridAfterDrawGrid(TObject *Sender,
  TTMSFNCGraphics *AGraphics, const TRectF &ARect)
{
  // ARect is the content clip rectangle. With RowViewMode = grvmControl it
  // covers the full body, so the message lands between header and footer.
  if (Grid->RowCount == Grid->FixedRowCount + Grid->FixedBottomRowCount)
  {
    AGraphics->Font->Size = 14;
    AGraphics->Font->Color = gcGray;
    AGraphics->DrawText(ARect, "No entries yet", false, gtaCenter, gtaCenter);
  }
}

void __fastcall TForm1::GridAppendRow(TObject *Sender, int ARow)
{
  // The new row is inserted above the footer, which stays the last row.
  Grid->Strings[0][ARow] = DateToStr(Date());
  Grid->Strings[1][ARow] = "New entry";
  int DataRows = Grid->RowCount - Grid->FixedRowCount - Grid->FixedBottomRowCount;
  Grid->Strings[1][Grid->RowCount - 1] = IntToStr(DataRows) + " entries";
}
  • OnBeforeDrawEmptySpace
  • OnAfterDrawEmptySpace
  • OnAfterDrawGrid
  • OnAppendRow / OnAppendColumn
  • OnCanInsertRow / OnInsertRow
  • OnCanInsertColumn / OnInsertColumn
  • OnGetCellSupportsRealControl
  • OnGetCellControlExtraProperties
  • OnGetCellProperties
  • TTMSFNCDataGridEmptySpaceKind

See also