YOUSEFUL / DELPHI
Atlas Virtual Grid: FAQ, examples and tutorial
Learning center · Component reference
Atlas Virtual Grid is a data grid for Delphi 13 VCL applications that draws itself and holds no cells.
A TAtlasVirtualGrid asks a data source for the cells on screen, so the
same control shows ten rows held in memory or a million computed on demand. It has column bands,
frozen columns, sorting, a text filter, totals, multi-row selection, in-place editing with validation,
conditional colours, and CSV, HTML, SVG and PNG output. Add Atlas.VGrid (the grid and its columns)
and Atlas.VGridData (cells, TAtlasMemoryData and
TAtlasVirtualData) to uses, plus Atlas.VGridPlot for
TAtlasPlotData, the companion that shows and edits an Atlas Plot chart as a
table. The product requires Atlas Plot: the grid draws through its drawing surface, and the companion
reads TAtlasPlot. The components are on the Atlas Plot palette page. It is VCL only, for
Win32 and Win64. See Getting started for building and installing the packages.
FAQ
How is it different from a grid that stores its cells? It keeps no copy of the data. The grid
asks its source for the cells it is about to draw. When it is neither sorted nor filtered it
allocates nothing per row and reads only the visible cells: fewer than 200 per paint in the
product's million-row test. Sorting, filtering, totals, ToCSV and ToHTML read every row once,
and take as long as the source takes to supply them. It is a different control from Atlas VCL's TAtlasGrid, which wraps
the stock VCL draw grid around an in-memory table.
Which data source should I use? Use TAtlasMemoryData for rows you load into memory. Use
TAtlasVirtualData when the data lives elsewhere or is computed: you answer OnGetCell, and
OnSetCell if the data is editable. Use TAtlasPlotData to show a chart as a table. For
anything else, derive from TAtlasGridData and override five methods.
There is no TDataSet source; TAtlasGridData is the extension point for one. Several grids may
share one source, and either side may be destroyed first.
What can a cell hold? One of four kinds (TAtlasCellKind): text, a number (a finite Double no larger than 1E100 in magnitude), a date/time (years 1 to 9999) or yes/no. Any cell may be null. Cells are TAtlasCell records, not Variants. Each data column has one kind, which decides how typed text is parsed, how the cell is formatted and aligned, and how it sorts and totals.
How does sorting work? Click a header to sort ascending, click again for descending, and a
third time for source order. In code, call Sort(Column, Descending), where Column is an index
into Columns and -1 removes the sort. The sort is stable, so equal keys keep source order. Nulls
come first when ascending and last when descending. Text compares without regard to case, with case
as the tie-break. The current record and the selected records stay with their records wherever they
move. Only one column sorts at a time.
What does the filter match? FilterText keeps the rows where the formatted text of any visible
column contains it, ignoring case. The filter searches what the user sees, so 2026-01 matches a
date column formatted yyyy-mm-dd but not one formatted dd/mm/yyyy. Hidden columns are not
searched. There is one filter text per grid; per-column filters and filter expressions are not
implemented.
How do totals work? Set a column's Total to tkSum, tkAverage, tkMinimum, tkMaximum
or tkCount (TAtlasTotalKind), and a footer appears. Totals cover the
rows shown, so they follow the filter and every change in the data, and they use the column's
format. Sum and average apply to numbers; minimum and maximum to numbers and dates; count to any
kind, counting non-null cells. Totals that do not suit the column stay blank.
How do I validate what users type? In the data source. The grid parses the text for the column's
kind, and text that does not parse is refused before your code runs. It then hands the cell to
SetCell, or to OnSetCell for TAtlasVirtualData, where you raise
EAtlasGridError to refuse it. A refused value is never stored. The
editor stays open, LastError holds the message and OnEditError fires. Moving away, sorting and
filtering wait until the value is corrected or the edit is cancelled with Esc. If focus leaves the
grid while the value is refused, the edit is cancelled. The grid catches only EAtlasGridError.
Which locale is used where? TAtlasMemoryData.AddRow always reads invariant text: . decimals,
ISO dates, yes/no. SetText uses the settings you pass. The in-place editor shows and parses
values with the global FormatSettings. CellText, HTML and pictures are formatted with the global
FormatSettings and each column's Format mask. ToCSV is always invariant (1250.5,
2026-03-31, true), so it reads back the same on every machine.
Can I export without showing a window? Yes. A grid with no parent can be bound, sorted,
filtered and totalled, and it writes CSV, HTML, SVG and image files, as the
quick start does in a console program. CSV
and HTML carry every row shown. Pictures carry what fits in the requested size, starting at TopRow.
In-place editing is the exception: it needs the grid to be on a form.
How does the chart companion lay out a chart? Column 0 is the argument: the point labels when the
first visible series has any, dates for a date axis, and numbers otherwise. One number column follows
per series. There is one row per distinct argument, in ascending order, and a missing point or a
gap is a null cell. Editing a value moves the point, or adds one where the series had none.
Arguments are read-only, blanks are refused, and the chart's own rules still apply. Changes made on
the chart reach the table once per chart batch, and RowOfPoint finds the row of a clicked point.
Can users reorder, hide or group columns? Not with the mouse. Column reordering by drag, a column
chooser, and grouping with expand and collapse are not implemented. In code, hide a column with
Visible := False, and reorder by rebuilding Columns. Users can resize a column by dragging its
header border, and fit it by double-clicking the border.
What is not implemented? An FMX control; editors other than text and yes/no (combo box, date
picker, spin edit); row details and sub-grids; merged cells; multi-line rows; a TDataSet source;
printing with pagination (Render draws one viewport); rescaling column widths when the DPI
changes; VCL styles; and UI Automation. The clipboard, dragging the scroll bars and use in the IDE
designer have not been tested.
Examples
A memory source with four typed columns, shown in a grid with a frozen first column, a format mask,
a sum and a descending sort. Call FillOrders(Orders) and then ShowOrders(Self, Orders) from a
form.
uses Vcl.Forms, Vcl.Controls, Atlas.VGridData, Atlas.VGrid;
procedure FillOrders(Orders: TAtlasMemoryData);
begin
Orders.AddColumn('Customer');
Orders.AddColumn('Amount', ckNumber);
Orders.AddColumn('Due', ckDateTime);
Orders.AddColumn('Paid', ckBoolean);
Orders.BeginUpdate; // one notification for the whole batch
try
// One text per column, read with invariant settings: '.' decimals, ISO dates.
Orders.AddRow(['Contoso', '1250.50', '2026-03-31', 'no']);
Orders.AddRow(['Fabrikam', '980', '2026-02-28', 'yes']);
Orders.AddRow(['Northwind', '', '', '']); // empty text is a null cell
finally
Orders.EndUpdate;
end;
end;
procedure ShowOrders(Form: TForm; Orders: TAtlasMemoryData);
var Grid: TAtlasVirtualGrid;
begin
Grid := TAtlasVirtualGrid.Create(Form);
Grid.Parent := Form;
Grid.Align := alClient;
Grid.Data := Orders; // no columns yet: one per data column
Grid.FrozenColumns := 1; // Customer stays put when scrolling sideways
Grid.Columns[1].Format := '#,##0.00';
Grid.Columns[1].Total := tkSum;
Grid.Columns[2].Format := 'yyyy-mm-dd';
Grid.Sort(1, True); // largest amount first
end;The grid lists Contoso (1,250.50), Fabrikam (980.00) and Northwind, whose null amount sorts last. The
footer shows 2,230.50. With invariant settings, CellText(1, 0) returns 1,250.50.
An on-demand source that stores nothing. The grid asks GetCell only for the cells it draws.
uses System.Classes, Atlas.VGridData;
type
TSquares = class(TComponent)
private
FData: TAtlasVirtualData;
procedure GetCell(Sender: TObject; Column, Row: Integer; var Cell: TAtlasCell);
public
constructor Create(AOwner: TComponent); override;
property Data: TAtlasVirtualData read FData;
end;
constructor TSquares.Create(AOwner: TComponent);
begin
inherited;
FData := TAtlasVirtualData.Create(Self);
FData.AddColumn('N', ckNumber);
FData.AddColumn('N squared', ckNumber);
FData.OnGetCell := GetCell; // asked only for the cells being shown
FData.RowCount := 1000000;
end;
procedure TSquares.GetCell(Sender: TObject; Column, Row: Integer; var Cell: TAtlasCell);
begin
if Column = 0 then Cell := TAtlasCell.OfNumber(Row + 1)
else Cell := TAtlasCell.OfNumber(Sqr(Int64(Row) + 1));
end;With Grid.Data := Squares.Data, the grid has 1,000,000 rows, and the last one reads 1,000,000 and
1,000,000,000,000. The source is read-only because it has no OnSetCell.
Accepting edits and refusing bad values. Assigning OnSetCell makes the source writable, and
raising EAtlasGridError refuses a value.
uses System.Classes, Atlas.VGridData;
type
TPrices = class(TComponent)
private
FPrices: TArray<Double>;
FData: TAtlasVirtualData;
procedure GetPrice(Sender: TObject; Column, Row: Integer; var Cell: TAtlasCell);
procedure SetPrice(Sender: TObject; Column, Row: Integer; const Cell: TAtlasCell);
public
constructor Create(AOwner: TComponent); override;
property Data: TAtlasVirtualData read FData;
end;
constructor TPrices.Create(AOwner: TComponent);
begin
inherited;
FPrices := [9.5, 12, 30];
FData := TAtlasVirtualData.Create(Self);
FData.AddColumn('Price', ckNumber);
FData.OnGetCell := GetPrice;
FData.OnSetCell := SetPrice; // without a handler every column is read-only
FData.RowCount := Length(FPrices);
end;
procedure TPrices.GetPrice(Sender: TObject; Column, Row: Integer; var Cell: TAtlasCell);
begin
Cell := TAtlasCell.OfNumber(FPrices[Row]);
end;
procedure TPrices.SetPrice(Sender: TObject; Column, Row: Integer; const Cell: TAtlasCell);
begin
// The text has already been parsed as a number. Raising EAtlasGridError
// refuses the value; a grid keeps its editor open and reports the message.
if Cell.IsNull or (Cell.Number <= 0) then
raise EAtlasGridError.Create('A price must be above zero');
FPrices[Row] := Cell.Number;
end;Here is what happens when a user edits the first price in a grid bound to this source. Typing 0
keeps the editor open with A price must be above zero in LastError, and the price stays 9.5.
Typing cheap is refused before SetPrice runs, with "cheap" is not a number. Typing 11.25
stores the value.
Conditional colours. OnCellStyle receives the grid column, the data row and the cell, and may
change the background and text colour.
uses Vcl.Graphics, Atlas.VGridData, Atlas.VGrid;
type
TOrderStyler = class
procedure Style(Sender: TObject; Column: TAtlasGridColumn; DataRow: Integer;
const Cell: TAtlasCell; var Background, TextColor: TColor);
end;
procedure TOrderStyler.Style(Sender: TObject; Column: TAtlasGridColumn; DataRow: Integer;
const Cell: TAtlasCell; var Background, TextColor: TColor);
begin
// Data column 3 is Paid: unpaid orders get a pale red cell.
if (Column.DataIndex = 3) and not Cell.IsNull and (Cell.Number = 0) then Background := $00D8D8FF;
// Data column 1 is Amount: 1,000 or more in dark green.
if (Column.DataIndex = 1) and not Cell.IsNull and (Cell.Number >= 1000) then TextColor := $00207A20;
end;After Grid.OnCellStyle := Styler.Style on the orders grid, Contoso's Paid cell is pale red and its
amount dark green, on screen and in exported pictures. Selected rows keep the selection colour.
The chart companion. The table below the chart edits the chart.
uses Vcl.Forms, Vcl.Controls, Atlas.Plot, Atlas.VGridPlot, Atlas.VGrid;
function LinkChart(Form: TForm; Chart: TAtlasPlot): TAtlasVirtualGrid;
var Link: TAtlasPlotData;
begin
Link := TAtlasPlotData.Create(Form);
Link.Plot := Chart; // column 0: the argument, then one column per series
Result := TAtlasVirtualGrid.Create(Form);
Result.Parent := Form;
Result.Align := alBottom;
Result.Data := Link;
Result.Columns[1].Total := tkSum;
end;Consider a chart with one bar series, Sales, holding Jan 10 and Feb 20. The grid shows two rows and a total of 30. Typing 25 over Feb raises the bar, and the total becomes 35. Adding a point on the chart adds a row.
Your own choice of columns. Columns that exist before the source is bound are kept.
uses Atlas.VGridData, Atlas.VGrid;
procedure ChooseColumns(Grid: TAtlasVirtualGrid; Orders: TAtlasMemoryData);
var Column: TAtlasGridColumn;
begin
Grid.Columns.BeginUpdate;
try
Grid.Columns.Clear;
Column := Grid.Columns.Add;
Column.DataIndex := 0; // Customer
Column.Width := 160;
Column := Grid.Columns.Add;
Column.DataIndex := 1; // Amount
Column.Title := 'Amount (EUR)';
Column.Format := '#,##0.00';
Column.Alignment := caRight;
Column.Total := tkSum;
finally
Grid.Columns.EndUpdate;
end;
Grid.Data := Orders; // existing columns are kept: Due and Paid stay hidden
end;The grid shows two columns, and ToCSV starts with Customer,Amount (EUR).
Export: CSV and HTML carry every row shown, while a picture carries what fits.
uses System.IOUtils, Atlas.VGrid;
procedure ExportOrders(Grid: TAtlasVirtualGrid; const Folder: string);
begin
Grid.SaveToFile(TPath.Combine(Folder, 'orders.csv')); // every row, raw values
Grid.SaveToFile(TPath.Combine(Folder, 'orders.html')); // every row, formatted, with totals
Grid.TopRow := 0; // pictures start at TopRow
Grid.SaveToFile(TPath.Combine(Folder, 'orders.png'), 900, 400); // what fits in 900 x 400
end;For the orders above, orders.csv reads:
Customer,Amount,Due,Paid
Contoso,1250.5,2026-03-31,false
Fabrikam,980,2026-02-28,true
Northwind,,,Your own data source. Override GetRowCount, GetColumnCount, ColumnTitle, ColumnKind and
GetCell, and call Changed when the data changes.
uses System.SysUtils, Atlas.VGridData;
type
// A read-only multiplication table. A source must provide GetRowCount,
// GetColumnCount, ColumnTitle, ColumnKind and GetCell, and call Changed.
TTimesTable = class(TAtlasGridData)
private
FSize: Integer;
procedure SetSize(Value: Integer);
protected
function GetRowCount: Integer; override;
function GetColumnCount: Integer; override;
public
function ColumnTitle(Column: Integer): string; override;
function ColumnKind(Column: Integer): TAtlasCellKind; override;
function GetCell(Column, Row: Integer): TAtlasCell; override;
property Size: Integer read FSize write SetSize;
end;
procedure TTimesTable.SetSize(Value: Integer);
begin
if Value < 0 then raise EAtlasGridError.Create('Size cannot be negative');
FSize := Value;
Changed; // tells every watching grid
end;
function TTimesTable.GetRowCount: Integer;
begin
Result := FSize;
end;
function TTimesTable.GetColumnCount: Integer;
begin
Result := FSize;
end;
function TTimesTable.ColumnTitle(Column: Integer): string;
begin
Result := 'x ' + IntToStr(Column + 1);
end;
function TTimesTable.ColumnKind(Column: Integer): TAtlasCellKind;
begin
Result := ckNumber;
end;
function TTimesTable.GetCell(Column, Row: Integer): TAtlasCell;
begin
CheckCell(Column, Row); // EAtlasGridError outside the table
Result := TAtlasCell.OfNumber((Row + 1) * (Column + 1));
end;With Size := 12, a grid shows twelve columns titled x 1 to x 12, and the bottom-right cell reads
144. Set Size before binding, or call the grid's ResetColumns afterwards (see Troubleshooting).
Every column is read-only because the class keeps the default ColumnReadOnly and SetCell.
The VirtualAndPlotGrid sample puts these pieces in one window. It has a million on-demand rows with bands, two frozen columns, validated editing and conditional colours, and a chart edited through a table.
Tutorial
Sample project: Memory grid export.
Goal: build an in-memory table of orders, show it through a grid with bands and totals, sort, filter and edit it, and export it to CSV, HTML, SVG and PNG without opening a window.
Create a console application named
MemoryGridExport(in the IDE: File > New > Console Application). Add the Atlas Virtual Gridsrcfolder and the Atlas Plotsrcfolder to the project's search path, or install the runtime packages as described in Getting started. Replace the generated code with the program header and two helpers.Verifystops the program when a result is not what this tutorial promises, andRejectedtells whether an action was refused withEAtlasGridError.program MemoryGridExport; // Atlas Virtual Grid quick start: an in-memory data source, a grid bound to it, // sorting, filtering, totals, programmatic edits and CSV/HTML/SVG/PNG output. // No window is shown; the grid works without a parent. Every result is checked. {$APPTYPE CONSOLE} uses Winapi.Windows, System.SysUtils, System.Classes, System.IOUtils, Atlas.VGridData, Atlas.VGrid; procedure Verify(Condition: Boolean; const What: string); begin if not Condition then raise Exception.Create('Check failed: ' + What); Writeln(' ok: ', What); end; function Rejected(const Action: TProc): Boolean; begin Result := False; try Action(); except on EAtlasGridError do Result := True; end; end;Winapi.WindowsandSystem.Classesare listed so the compiler can inlineTDirectory.GetCurrentDirectoryandColumns.Countwithout a hint.Add the
Runprocedure, which owns the source and the grid, and the main block. The grid is created without an owner or a parent: nothing is shown. The main block fixes the globalFormatSettings, so the formatted text checked below is the same on every machine.procedure Run; var Orders: TAtlasMemoryData; Grid: TAtlasVirtualGrid; Csv, Html, Svg: string; Line: string; begin Orders := TAtlasMemoryData.Create(nil); Grid := TAtlasVirtualGrid.Create(nil); try // steps 3 to 9 go here finally Grid.Free; Orders.Free; end; end; begin // Formatting follows the global FormatSettings; fix them so the checks hold on every machine. FormatSettings := TFormatSettings.Invariant; try Run; Writeln('MemoryGridExport: all checks passed'); except on E: Exception do begin Writeln('MemoryGridExport failed: ', E.ClassName, ': ', E.Message); ExitCode := 1; end; end; end.Add the columns and rows. Each column has a kind, and the order number is read-only.
AddRowtakes one invariant text per column, and empty text is null. A row with a value that does not fit its column is refused whole, so the source still has six rows afterwards.Writeln('1. Columns and rows'); Orders.AddColumn('Order', ckNumber, True); // read-only key Orders.AddColumn('Customer'); // ckText by default Orders.AddColumn('Shipped', ckDateTime); Orders.AddColumn('Quantity', ckNumber); Orders.AddColumn('Unit price', ckNumber); Orders.AddColumn('Paid', ckBoolean); Orders.BeginUpdate; // one change notification for the batch try // One invariant text per column: '.' decimals, ISO dates, yes/no. Empty text is null. Orders.AddRow(['1001', 'Contoso', '2026-01-05', '4', '125.50', 'yes']); Orders.AddRow(['1002', 'Fabrikam', '2026-01-06', '12', '40', 'no']); Orders.AddRow(['1003', 'Northwind', '2026-01-09', '1', '980', 'yes']); Orders.AddRow(['1004', 'Contoso', '2026-01-12', '', '125.50', 'no']); Orders.AddRow(['1005', 'Tailspin, Ltd', '2026-01-15', '7', '64.25', 'yes']); Orders.AddRow(['1006', 'Fabrikam', '', '3', '40', 'yes']); finally Orders.EndUpdate; end; Verify((Orders.RowCount = 6) and (Orders.ColumnCount = 6), '6 rows, 6 columns'); Verify(Rejected(procedure begin Orders.AddRow(['1007', 'Adatum', '2026-02-01', 'twelve', '10', 'no']); end) and (Orders.RowCount = 6), 'a row with a value that does not fit its column is rejected whole');Bind the grid. It has no columns yet, so it creates one per data column. Then give the columns format masks, two bands and three totals.
CellTextreturns what a user would see, andTotalTextreturns the footer.Writeln('2. Bind the grid and describe the columns'); Grid.Data := Orders; // one grid column per data column Grid.Columns[0].Format := '0'; Grid.Columns[0].Total := tkCount; Grid.Columns[1].Band := 'Shipment'; Grid.Columns[2].Band := 'Shipment'; Grid.Columns[2].Format := 'yyyy-mm-dd'; Grid.Columns[3].Band := 'Amount'; Grid.Columns[3].Total := tkSum; Grid.Columns[4].Band := 'Amount'; Grid.Columns[4].Format := '#,##0.00'; Grid.Columns[4].Total := tkAverage; Verify(Grid.Columns.Count = 6, 'columns were created from the source'); Verify((Grid.CellText(1, 4) = 'Tailspin, Ltd') and (Grid.CellText(4, 0) = '125.50') and (Grid.CellText(5, 1) = 'No') and (Grid.CellText(3, 3) = ''), 'cells are formatted per column'); Verify((Grid.TotalText(0) = '6') and (Grid.TotalText(3) = '27') and (Grid.TotalText(4) = '229.21'), 'totals: count 6, quantity sum 27, average price 229.21');The count skips nothing (six orders). The quantity sum skips the null quantity (4 + 12 + 1 + 7 + 3 = 27). The average price is 1375.25 / 6 = 229.21.
Sort. Row numbers in the grid are rows as shown;
DataRowOfgives the data row. The two Contoso orders keep their source order (data rows 0 and 3), and the null quantity sorts last when descending.Writeln('3. Sort'); Grid.Sort(1); // by customer, ascending, ignoring case Verify((Grid.CellText(1, 0) = 'Contoso') and (Grid.DataRowOf(0) = 0) and (Grid.DataRowOf(1) = 3) and (Grid.CellText(1, 5) = 'Tailspin, Ltd'), 'ascending by customer; equal keys keep source order'); Grid.Sort(3, True); // by quantity, descending Verify((Grid.CellText(3, 0) = '12') and (Grid.DataRowOf(5) = 3), 'descending by quantity; the null quantity comes last');Filter. The filter keeps rows where any visible column's formatted text contains the text, and the totals follow it. Because it reads the formatted text,
2026-01-0finds the three orders shipped between the 1st and the 9th of January.Writeln('4. Filter'); Grid.FilterText := 'fab'; // any visible column, formatted text, ignoring case Verify((Grid.RowCount = 2) and (Grid.DataRowOf(0) = 1) and (Grid.DataRowOf(1) = 5), '2 Fabrikam rows, still sorted'); Verify((Grid.TotalText(0) = '2') and (Grid.TotalText(3) = '15') and (Grid.TotalText(4) = '40.00'), 'totals follow the filter'); Grid.FilterText := '2026-01-0'; Verify(Grid.RowCount = 3, 'the filter reads the formatted date text: 3 rows shipped before 10 January'); Grid.FilterText := ''; Grid.Sort(-1); // back to source order Verify((Grid.RowCount = 6) and (Grid.SortColumn = -1), 'filter and sort removed');Edit through the data source. The grid listens to the source, so the cell and the total change at once. Text that does not parse is refused and the value is kept. The read-only column refuses writes from code too.
Writeln('5. Edit through the data source'); Orders.SetText(3, 3, '5', TFormatSettings.Invariant); Verify((Grid.CellText(3, 3) = '5') and (Grid.TotalText(3) = '32'), 'the grid follows the source: sum is now 32'); Verify(Rejected(procedure begin Orders.SetText(3, 3, 'five', TFormatSettings.Invariant); end) and (Grid.CellText(3, 3) = '5'), 'text that is not a number is refused and the value kept'); Verify(Rejected(procedure begin Orders.SetCell(0, 0, TAtlasCell.OfNumber(1)); end), 'the read-only key column refuses writes');Copy and export text.
SelectionTextis what Ctrl+C copies: formatted and tab-separated.ToCSVcarries raw values, quotes the comma inTailspin, Ltdand leaves hidden columns out.ToHTMLcarries the bands and totals.Writeln('6. Selection text'); Grid.SelectAll; Line := Grid.SelectionText; Verify((Grid.SelectedCount = 6) and Line.StartsWith('1001'#9'Contoso'#9'2026-01-05'#9'4'#9'125.50'#9'Yes'#13#10), 'tab-separated formatted rows, ready for the clipboard'); Writeln('7. Text output'); Csv := Grid.ToCSV; Writeln(Csv); Verify(Csv.StartsWith('Order,Customer,Shipped,Quantity,Unit price,Paid'#13#10'1001,Contoso,2026-01-05,4,125.5,true'), 'CSV carries raw values: invariant numbers, ISO dates, true/false'); Verify((Pos('"Tailspin, Ltd"', Csv) > 0) and (Pos('1006,Fabrikam,,3,40,true', Csv) > 0), 'commas are quoted; nulls are empty'); Grid.Columns[5].Visible := False; Verify(Grid.ToCSV.StartsWith('Order,Customer,Shipped,Quantity,Unit price'#13#10), 'hidden columns are left out'); Grid.Columns[5].Visible := True; Html := Grid.ToHTML('Orders'); Verify((Pos('<caption>Orders</caption>', Html) > 0) and (Pos('<th colspan="2">Amount</th>', Html) > 0) and (Pos('<tfoot>', Html) > 0) and (Pos('>229.21<', Html) > 0), 'HTML has the caption, bands and totals');Write the files. CSV and HTML carry every row. SVG and PNG carry what fits in 800 x 300 pixels, starting at
TopRow, and they show the selection, so it is cleared first. An unknown extension is refused.Writeln('8. Pictures and files'); Grid.ClearSelection; // pictures show the selection too Svg := Grid.ToSVG(800, 300); Verify((Pos('<svg', Svg) > 0) and (Pos('Northwind', Svg) > 0), 'SVG of the visible part of the grid'); Grid.SaveToFile('orders.csv'); // everything Grid.SaveToFile('orders.html'); // everything, formatted, with totals Grid.SaveToFile('orders.svg', 800, 300); // what is on screen Grid.SaveToFile('orders.png', 800, 300); for Line in ['orders.csv', 'orders.html', 'orders.svg', 'orders.png'] do Verify(TFile.Exists(Line) and (TFile.GetSize(Line) > 100), Line + ' written to ' + TDirectory.GetCurrentDirectory); Verify(Rejected(procedure begin Grid.SaveToFile('orders.xlsx'); end), 'unknown extensions are refused');Build and run it: press F9 in the IDE. Every step prints its checks, the CSV appears in the middle, and the last line is
MemoryGridExport: all checks passed, with exit code 0. The CSV reads:Order,Customer,Shipped,Quantity,Unit price,Paid 1001,Contoso,2026-01-05,4,125.5,true 1002,Fabrikam,2026-01-06,12,40,false 1003,Northwind,2026-01-09,1,980,true 1004,Contoso,2026-01-12,5,125.5,false 1005,"Tailspin, Ltd",2026-01-15,7,64.25,true 1006,Fabrikam,,3,40,trueorders.pngshows the Shipment and Amount bands, check boxes in the Paid column and a footer with 6, 32 and 229.21. To adapt it, replace theAddRowcalls with a loop over your own records. Then put the grid on a form (Grid.Parent := Self; Grid.Align := alClient;), where users can click headers to sort and press F2 to edit.
Troubleshooting
The grid shows no columns, or not the source's new columns. A grid creates columns only when a
source is bound while Columns is empty, and not while a form is loading from a .dfm. Binding before
the source has columns gives a grid with none, and columns added to the source later are not added
to the grid. This is the usual case when both components are placed at design time and the columns
are added in FormCreate. Call Grid.ResetColumns after adding the source's columns.
EAtlasGridError: "1250,50" does not fit column "Amount" from AddRow. AddRow reads invariant
text: . as the decimal separator and ISO dates such as 2026-03-31. Format values with
FloatToStr(Value, TFormatSettings.Invariant), or store typed values with SetCell and
TAtlasCell.OfNumber.
Sort, FilterText or SelectCell seem to do nothing. An in-place edit is open with a value
the source refused. These calls commit first and do nothing while the commit fails. Check Editing
and LastError, then correct the text with SetEditorText or call CancelEdit.
Edits are refused with "This data source is read-only", or columns you wanted locked are
editable. A TAtlasVirtualData without OnSetCell is read-only in every column, and with
OnSetCell it is writable in every column. Assign the handler, and set ReadOnly on the grid
columns that must stay locked. A column created with AddColumn(..., True) on TAtlasMemoryData
refuses writes from code as well.
EInvalidOperation: Control ... has no parent window from BeginEdit. The in-place editor
needs a window. Put the grid on a form before editing, or change values through the source
(SetText, SetCell) in code that has no window.
An exported picture starts in the middle of the data, or shows highlighted rows. Pictures draw
from the grid's own TopRow and LeftOffset, include the selection, and hold only what fits in the
requested size. A grid that was never shown is small, so selecting a row may have scrolled it. Set
TopRow := 0 and call ClearSelection before SaveToFile, or export CSV or HTML for every row.
A total stays blank. Sum and average apply only to number columns; minimum and maximum to number
and date columns. Count works for every kind, and shows 0 when every cell is null; the other totals
stay blank then. Every total stays blank when the column's DataIndex is beyond the source's
columns.
Filtering or sorting a large on-demand source is slow. Both read every row once through
OnGetCell, and the filter formats every visible cell. In the
VirtualAndPlotGrid sample, a filter over a
million computed rows took 1.6 to 1.7 s and a sort 1.0 to 1.4 s on Win32. Apply the filter on a button or after a
pause in typing, keep OnGetCell cheap, and remember that every change to the source recomputes an
active sort or filter.
The program asks for AtlasPlotRuntime370.bpl at start-up. When built with runtime packages,
AtlasVGridRuntime370.bpl needs AtlasPlotRuntime370.bpl beside it or on the PATH. Deploy both,
or link the units statically.
Columns look too narrow on a high-DPI monitor. Column widths are pixels and are not rescaled when
the DPI changes. Scale them yourself, for example with MulDiv(100, Screen.PixelsPerInch, 96), or
call AutoFitColumn once the grid shows data.