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:
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;
}
}
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:
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:
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
}
The same event also handles empty-state messages — draw centered text only when the grid has no data rows:
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.
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";
}
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.
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.
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";
}
Related API
OnBeforeDrawEmptySpaceOnAfterDrawEmptySpaceOnAfterDrawGridOnAppendRow/OnAppendColumnOnCanInsertRow/OnInsertRowOnCanInsertColumn/OnInsertColumnOnGetCellSupportsRealControlOnGetCellControlExtraPropertiesOnGetCellPropertiesTTMSFNCDataGridEmptySpaceKind
See also
- Appearance and theming - normal cell layout customization.
- Custom cells - cell subclasses and custom rendering.
- Cell controls - live controls inside cells.