YOUSEFUL / DELPHI
Atlas VCL: FAQ, examples and tutorial
Learning center · Component reference
Atlas VCL is a suite of original VCL components for Delphi 13 on Windows (Win32 and Win64) that registers 29 classes on the Atlas palette page and depends only on the Delphi RTL and VCL; Atlas Analytics, Atlas UI and Atlas Info are built on top of it. Its classes fall into three areas. This page covers the data classes: TAtlasTable (unit Atlas.Data), an in-memory table with typed columns, sorting, filtering and CSV; TAtlasGrid (Atlas.Grid) and TAtlasValueEdit (Atlas.Editors) to show and edit it; TAtlasTree (Atlas.Tree); TAtlasWorkbook (Atlas.Spreadsheet), a small formula engine, with its grid TAtlasSheetGrid (Atlas.SheetGrid); TAtlasPivot (Atlas.Pivot); TAtlasInspector (Atlas.Inspector); TAtlasRichEdit (Atlas.RichEdit) and TAtlasSpellChecker (Atlas.Spell). Charts, gauges, themes, the wizard, workspaces, command bars, tiles, navigation, maps and diagrams have their own page, the interface guide; printing, scheduling, Gantt plans and PDF viewing are in the planning guide. Every class lives in its own unit: add the units you use to uses and the Atlas VCL src folder to the search path, or build with the AtlasVclRuntime runtime package. The components are meant for the VCL main thread, report bad input with exceptions and leave their data unchanged when they do. There is no database connection, no undo for the data models (TAtlasRichEdit keeps the RichEdit control's own undo and redo), no compatibility layer for other component suites and no cross-platform support.
FAQ
Which units do I add to uses? One per class: Atlas.Data (TAtlasTable, TAtlasColumnKind, EAtlasDataError and the CSV functions), Atlas.Grid, Atlas.Editors, Atlas.Tree, Atlas.Spreadsheet (TAtlasWorkbook, TAtlasCellFormat, EAtlasFormulaError, AtlasCellAddress), Atlas.SheetGrid, Atlas.Pivot, Atlas.Inspector, Atlas.RichEdit and Atlas.Spell. A control unit does not re-export its model, so a form that creates a TAtlasTable for a TAtlasGrid needs both Atlas.Data and Atlas.Grid. For runtime packages, add AtlasVclRuntime and deploy AtlasVclRuntime370.bpl built for the same platform as the application.
Is TAtlasTable a dataset? Can I connect it to a database? No. It is an in-memory model of string values with typed validation. It is not a TDataSet, has no data source link, no transactions, and no persistence apart from CSV text. Load rows from your database in your own code (AddRow per record, or one ImportCSV for bulk loading) and write changes back from OnChange or a listener.
Why do row numbers change, and how do I identify a row? Cell, SetCell, DeleteRow, RowCount and SourceIndex use zero-based indices into the current view, which changes with every sort, filter, insert and delete. SourceIndex(ViewRow) returns the current position in the underlying list, but that position also shifts when earlier rows are deleted or the table is reloaded. Keep a business key (an invoice number, a product code) in a column of its own, usually read-only (AddColumn('Number', ackText, 100, False)), and search for it with a loop over Cell.
What values does each column kind accept? ackText accepts anything. ackNumber accepts finite numbers in invariant format (12.50, -3, 1e3) and stores the shortest form (12.5, 1000); 12,5, 1,000, NaN and INF raise EAtlasDataError. ackBoolean accepts true/false in any case and 1/0, stored as true/false. ackDate accepts real dates written exactly as YYYY-MM-DD. Every kind accepts an empty value. AtlasNormalizeValue(Kind, Value) applies the same rules to a single value.
Can I add columns after loading data, or define them at design time? Neither. AddColumn raises Define columns before adding rows once the table holds rows (after Clear you may add more), and columns can never be removed or renamed. TAtlasTable publishes only OnChange, so define the schema in code, for example in the form's constructor, before loading rows.
How do sorting and filtering work? Sort(Column, Ascending) orders the view by one column: numbers numerically, all other kinds as text ignoring ASCII case (ISO dates therefore sort by date). Empty values come first when ascending, rows with equal keys keep their source order, and Sort(-1) restores source order. SetFilter(Text, Column) keeps the rows whose stored value contains Text, ignoring case, in one column or in all columns (-1). Both settings are remembered and reapplied after every change. There is one sort column and one filter text; there are no filter expressions.
When does an edit in TAtlasGrid reach the table? When the cursor moves to another cell, a header is clicked, the grid loses focus, Enter is pressed with the editor closed, or your code calls CommitEdit; the value is validated then, and an invalid value raises EAtlasDataError and keeps the cursor where it is. In this baseline the first Enter only closes the editor, and Escape does not discard text typed in an open editor (a second Escape does). The tutorial's form-level key handler makes both keys act at once. Call CommitEdit before your own code sorts, filters or changes the table.
Which formulas does TAtlasWorkbook understand? Can it open spreadsheet files? Numbers, cell references (A1 to XFD1048576, any case), + - * / ^, parentheses and five functions, SUM, AVERAGE, MIN, MAX and COUNT, whose arguments may be ranges such as B2:C4. Errors show as #REF!, #CYCLE!, #DIV/0!, #NUM!, #NAME?, #VALUE! or #LIMIT!. There are no comparisons, strings, IF, absolute references or further sheets, and no file import or export: persist a workbook with ToJSON and FormatsJSON.
Can users type formulas into TAtlasSheetGrid? Not reliably in this baseline. The grid stores the editor text in the workbook on every keystroke, and the refresh that follows selects the whole editor text, so the next keystroke replaces it: typing =A1*2 leaves 2. Typing one character or pasting a whole value works. Remove goEditing from Options and edit through a separate edit box that calls SetCell; the TAtlasSheetGrid reference has a formula bar example.
What does TAtlasPivot return, and who frees it? A new TAtlasTable with no owner: one row per value of the row dimension plus Grand total, and the columns Rows: <name>, 1: <value>, 2: <value>, ... and Grand total. All its columns are read-only. Free it yourself, or pass it to an owner with InsertComponent before showing it in a grid. It summarizes the source's current view, so apply a filter first to summarize a subset.
Does TAtlasSpellChecker include a dictionary, and does it underline words? Neither. Load your own word list with LoadDictionary (one word per line, # for comments) or AddWord. Check returns the unknown words with one-based offsets and Suggest ranks corrections; showing them is up to the application. Case is ignored for ASCII letters only, so CAFÉ does not match café.
Can I use these classes without a window or from a background thread? The models TAtlasTable, TAtlasWorkbook, TAtlasPivot and TAtlasSpellChecker need no window, and the WorkbookReport sample runs them in a console program. The controls need a parent window before use: TAtlasTree nodes and TAtlasRichEdit RTF streaming raise EInvalidOperation without one. Nothing is thread-safe; use every instance from the thread that created it, normally the VCL main thread.
Examples
A table with a read-only key, a filter on the boolean column and a descending numeric sort:
uses System.SysUtils, Atlas.Data;
procedure ListUnpaidInvoices;
var
Invoices: TAtlasTable;
I: Integer;
begin
Invoices := TAtlasTable.Create(nil);
try
Invoices.AddColumn('Number', ackText, 100, False); // read-only business key
Invoices.AddColumn('Customer');
Invoices.AddColumn('Amount', ackNumber);
Invoices.AddColumn('Paid', ackBoolean);
Invoices.AddColumn('Due', ackDate);
Invoices.AddRow(['INV-001', 'Harbor Books', '125.50', 'false', '2026-10-01']);
Invoices.AddRow(['INV-002', 'Blue Mill', '980', '1', '2026-09-15']);
Invoices.AddRow(['INV-003', 'Harbor Books', '42', '0', '']);
Invoices.SetFilter('false', 3); // unpaid rows only
Invoices.Sort(2, False); // largest amount first
for I := 0 to Invoices.RowCount - 1 do
Writeln(Invoices.Cell(0, I), ' ', Invoices.Cell(1, I), ' ', Invoices.Cell(2, I));
Writeln(Invoices.RowCount, ' of ', Invoices.SourceRowCount, ' rows shown');
finally
Invoices.Free;
end;
end;Output: INV-001 Harbor Books 125.5, INV-003 Harbor Books 42 and 2 of 3 rows shown. The amount 125.50 is stored as 125.5 and 1 as true, so the filter on false hides INV-002.
Saving every row to a file and loading it back; ImportCSV checks the whole file before it replaces anything:
uses System.SysUtils, System.IOUtils, Atlas.Data;
procedure SaveToCsv(Table: TAtlasTable; const FileName: string);
begin
// False: every row in source order, whatever the filter and sort.
TFile.WriteAllText(FileName, Table.ExportCSV(False), TEncoding.UTF8);
end;
procedure ReloadFromCsv(Table: TAtlasTable; const FileName: string);
begin
try
Table.ImportCSV(TFile.ReadAllText(FileName, TEncoding.UTF8));
except
on E: EAtlasDataError do
Writeln('Not loaded, previous rows kept: ', E.Message);
end;
end;With a table holding Ink, "archival" / 1.50, the reload gives the same row back (1.5). A file containing Paper,lots prints Not loaded, previous rows kept: Invalid finite number: lots and the table still holds the old row.
A summary of sales by region and quarter with TAtlasPivot:
uses System.SysUtils, Atlas.Data, Atlas.Pivot;
procedure PrintQuarterlySales;
var
Sales, Summary: TAtlasTable;
Pivot: TAtlasPivot;
Row, Col: Integer;
Line: string;
begin
Sales := TAtlasTable.Create(nil);
Pivot := TAtlasPivot.Create(nil);
try
Sales.AddColumn('Region');
Sales.AddColumn('Quarter');
Sales.AddColumn('Sales', ackNumber);
Sales.AddRow(['East', 'Q1', '100']);
Sales.AddRow(['East', 'Q2', '150']);
Sales.AddRow(['West', 'Q1', '80']);
Sales.AddRow(['East', 'Q1', '20']);
Summary := Pivot.Summarize(Sales, 0, 1, 2, aaSum);
try
for Row := -1 to Summary.RowCount - 1 do
begin
Line := '';
for Col := 0 to Summary.ColumnCount - 1 do
if Row < 0 then
Line := Line + Summary.Columns[Col].Name.PadRight(14)
else
Line := Line + Summary.Cell(Col, Row).PadRight(14);
Writeln(Line.TrimRight);
end;
finally
Summary.Free; // the caller owns the result
end;
finally
Pivot.Free;
Sales.Free;
end;
end;Output (columns padded to 14 characters):
Rows: Region 1: Q1 2: Q2 Grand total
East 120 150 270
West 80 0 80
Grand total 200 150 350An invoice computed by TAtlasWorkbook formulas and shown with currency formats:
uses System.SysUtils, Atlas.Spreadsheet;
procedure BuildInvoice(Book: TAtlasWorkbook);
var
Money: TAtlasCellFormat;
begin
Book.Clear;
Book.SetCell('A1', 'Item');
Book.SetCell('B1', 'Hours');
Book.SetCell('C1', 'Rate');
Book.SetCell('D1', 'Amount');
Book.SetCell('A2', 'Design');
Book.SetCell('B2', '12');
Book.SetCell('C2', '125');
Book.SetCell('D2', '=B2*C2');
Book.SetCell('A3', 'Development');
Book.SetCell('B3', '40');
Book.SetCell('C3', '150');
Book.SetCell('D3', '=B3*C3');
Book.SetCell('C4', 'Total');
Book.SetCell('D4', '=SUM(D2:D3)');
Book.SetCell('C5', 'Tax');
Book.SetCell('D5', '=D4*0.2');
Money := TAtlasCellFormat.Default;
Money.NumberFormat := anfCurrency;
Book.SetCellFormat('D2', Money);
Book.SetCellFormat('D3', Money);
Money.Bold := True;
Book.SetCellFormat('D4', Money);
Book.SetCellFormat('D5', Money);
end;
procedure PrintInvoice(Book: TAtlasWorkbook);
var
Row: Integer;
begin
for Row := 2 to 5 do
Writeln(Book.Display(AtlasCellAddress(1, Row)).PadRight(12),
Book.Display(AtlasCellAddress(3, Row)).PadRight(6),
Book.Display(AtlasCellAddress(4, Row)));
Writeln('Total as a number: ', Book.Value('D4'):0:2);
end;Output: Design 125 $1500.00, Development 150 $6000.00, Total $7500.00, Tax $1500.00 and Total as a number: 7500.00.
What the error codes look like, and how Value reports them:
uses System.SysUtils, Atlas.Spreadsheet;
procedure ShowFormulaErrors;
var
Book: TAtlasWorkbook;
begin
Book := TAtlasWorkbook.Create(nil);
try
Book.SetCell('A1', 'text');
Book.SetCell('B1', '=A1+1'); // arithmetic on text
Book.SetCell('B2', '=1/0');
Book.SetCell('B3', '=B4');
Book.SetCell('B4', '=B3'); // B3 and B4 refer to each other
Book.SetCell('B5', '=ROUND(2.5)'); // not one of the five functions
Book.SetCell('B6', '=SUM(A1:A3)'); // text and blank cells in a range are skipped
Writeln(Book.Display('B1'), ' ', Book.Display('B2'), ' ', Book.Display('B3'), ' ',
Book.Display('B5'), ' ', Book.Display('B6'));
try
Book.Value('B2');
except
on E: EAtlasFormulaError do
Writeln(E.Code, ' | ', E.Message);
end;
finally
Book.Free;
end;
end;Output: #VALUE! #DIV/0! #CYCLE! #NAME? 0 and #DIV/0! | #DIV/0! Division by zero at position 4. B6 is 0 because a range skips text and blank cells.
A folder browser whose nodes load their children when first expanded:
uses System.SysUtils, System.IOUtils, Vcl.Forms, Vcl.Controls, Vcl.ComCtrls, Atlas.Tree;
type
TFolderBrowser = class
private
FTree: TAtlasTree;
function PathOf(Node: TTreeNode): string;
procedure LoadFolders(Sender: TObject; Node: TTreeNode);
public
constructor Create(Form: TForm; const RootFolder: string);
property Tree: TAtlasTree read FTree;
end;
constructor TFolderBrowser.Create(Form: TForm; const RootFolder: string);
begin
inherited Create;
FTree := TAtlasTree.Create(Form);
FTree.Parent := Form; // the tree needs its window before nodes are added
FTree.Align := alLeft;
FTree.ReadOnly := True; // folder names are not edited in place
FTree.OnLoadChildren := LoadFolders;
FTree.AddNode(nil, RootFolder, True);
end;
function TFolderBrowser.PathOf(Node: TTreeNode): string;
begin
Result := Node.Text;
while Node.Parent <> nil do
begin
Node := Node.Parent;
Result := TPath.Combine(Node.Text, Result);
end;
end;
procedure TFolderBrowser.LoadFolders(Sender: TObject; Node: TTreeNode);
var
Folder: string;
begin
// Runs on the first expand only. If it raises (no access, for example), the
// tree removes what was added and the node stays expandable.
for Folder in TDirectory.GetDirectories(PathOf(Node)) do
FTree.AddNode(Node, ExtractFileName(Folder), True);
end;Create it with TFolderBrowser.Create(Form, 'C:\Projects'): the tree shows one expandable node, and expanding a folder lists its subfolders, each expandable again. A folder that cannot be read raises from the expand, and the node stays expandable.
Setting published properties by name with TAtlasInspector:
uses System.SysUtils, Vcl.Forms, Vcl.Controls, Atlas.Inspector;
function InspectControl(Form: TForm; Target: TControl): TAtlasInspector;
begin
Result := TAtlasInspector.Create(Form);
Result.Parent := Form;
Result.Align := alRight;
Result.Width := 320;
Result.Instance := Target; // lists Target's published properties, sorted by name
end;
procedure SetFromText(Inspector: TAtlasInspector; const Name, Value: string);
begin
try
Inspector.SetProperty(Name, Value);
except
on E: EAtlasPropertyError do
Writeln(Name, ' = ', Value, ': ', E.Message);
end;
end;SetFromText(Inspector, 'Caption', 'Inspected') changes the caption; 'Tag', 'seven' prints Tag = seven: Expected integer; 'Font', 'Arial' prints Font = Arial: Unsupported property: Font, because class properties are not listed.
Checking a sentence with TAtlasSpellChecker:
uses System.SysUtils, Atlas.Spell;
procedure ProofRead(const Text: string);
var
Spell: TAtlasSpellChecker;
Issue: TAtlasSpellingIssue;
begin
Spell := TAtlasSpellChecker.Create(nil);
try
Spell.LoadDictionary('the'#13#10'quarterly'#13#10'report'#13#10'is'#13#10 +
'ready'#13#10'isn''t'#13#10'it'#13#10'late');
for Issue in Spell.Check(Text) do
Writeln(Issue.Word, ' at ', Issue.StartIndex, ': ', string.Join(', ', Spell.Suggest(Issue.Word)));
finally
Spell.Free;
end;
end;ProofRead('The quartely reprot is ready, isn''t it?') prints quartely at 5: quarterly and reprot at 14: report. isn't is one word, and The matches the.
Tutorial
Sample project: GridQuickStart. The deeper samples are WorkbookReport (table, pivot, workbook and spelling in a console program) and TreeAndEditors (tree, sheet grid, inspector, rich edit and value edit, checked on a hidden form).
Goal: build a window that shows a typed product table in a grid, filters and sorts it, edits it in place, adds validated rows and exports the visible rows as CSV.
Create a new VCL application in Delphi 13, save it as
InventoryGrid, and remove the form unit the wizard created (the form here is built in code, so no.dfmis needed). Add the Atlas VCLsrcfolder to the project's search path for both Win32 and Win64. Replace the whole.dprwith the code of the following steps, in order. It compiles once every method from steps 3 to 10 is in place.Start the program with its
usesclause and the form declaration.Atlas.DataprovidesTAtlasTableand the column kinds,Atlas.Gridthe grid,Atlas.Editorsthe value edits;Winapi.Windowsis needed for the key codes in step 6.program InventoryGrid; // Atlas VCL quick start: a typed TAtlasTable shown in a TAtlasGrid, with a // filter box, header sorting, in-place editing, validated entry through // TAtlasValueEdit, row deletion and CSV export. The form is built in code; no // .dfm is needed. uses Winapi.Windows, System.SysUtils, System.Classes, System.IOUtils, Vcl.Forms, Vcl.Controls, Vcl.StdCtrls, Vcl.ExtCtrls, Atlas.Data, Atlas.Grid, Atlas.Editors; type TMainForm = class(TForm) private FTable: TAtlasTable; FGrid: TAtlasGrid; FFilter: TEdit; FProduct: TEdit; FQuantity: TAtlasValueEdit; FPrice: TAtlasValueEdit; FRestock: TAtlasValueEdit; FAvailable: TCheckBox; FStatus: TLabel; procedure AddLabel(Parent: TWinControl; Left, Top: Integer; const Caption: string); procedure AddButton(Parent: TWinControl; Left, Top: Integer; const Caption: string; OnClick: TNotifyEvent); function AddValueEdit(Parent: TWinControl; Left, Width: Integer; Kind: TAtlasColumnKind; Required: Boolean): TAtlasValueEdit; function Invalid(Edit: TAtlasValueEdit; const Name: string): Boolean; procedure FormKeyDown(Sender: TObject; var Key: Word; Shift: TShiftState); procedure FilterChanged(Sender: TObject); procedure AddClicked(Sender: TObject); procedure DeleteClicked(Sender: TObject); procedure ExportClicked(Sender: TObject); procedure ClearSortClicked(Sender: TObject); procedure TableChanged(Sender: TObject); public constructor Create(AOwner: TComponent); override; end;Write the first half of the constructor.
CreateNewbuilds a form without a.dfm.KeyPreviewlets the form see Enter and Escape before the grid's in-place editor does (step 6). The top panel holds the filter box and three buttons, then labelled entry boxes: a plainTEditfor the product name, threeTAtlasValueEditcontrols that validate a number, a number and a date, and an Available check box. The bottom panel holds the status line.constructor TMainForm.Create(AOwner: TComponent); var Toolbar, Footer: TPanel; begin inherited CreateNew(AOwner); Caption := 'Atlas VCL: grid quick start'; Width := 900; Height := 560; Position := poScreenCenter; KeyPreview := True; // FormKeyDown sees Enter and Escape before the grid editor OnKeyDown := FormKeyDown; Toolbar := TPanel.Create(Self); Toolbar.Parent := Self; Toolbar.Align := alTop; Toolbar.Height := 108; Toolbar.BevelOuter := bvNone; AddLabel(Toolbar, 16, 17, 'Filter'); FFilter := TEdit.Create(Self); FFilter.Parent := Toolbar; FFilter.SetBounds(56, 14, 260, 24); FFilter.OnChange := FilterChanged; AddButton(Toolbar, 332, 11, 'Clear sort', ClearSortClicked); AddButton(Toolbar, 448, 11, 'Delete row', DeleteClicked); AddButton(Toolbar, 564, 11, 'Export CSV', ExportClicked); AddLabel(Toolbar, 16, 50, 'Product'); AddLabel(Toolbar, 232, 50, 'Quantity'); AddLabel(Toolbar, 338, 50, 'Unit price'); AddLabel(Toolbar, 444, 50, 'Restock (YYYY-MM-DD)'); FProduct := TEdit.Create(Self); FProduct.Parent := Toolbar; FProduct.SetBounds(16, 68, 200, 24); FQuantity := AddValueEdit(Toolbar, 232, 90, ackNumber, True); FPrice := AddValueEdit(Toolbar, 338, 90, ackNumber, True); FRestock := AddValueEdit(Toolbar, 444, 110, ackDate, False); FAvailable := TCheckBox.Create(Self); FAvailable.Parent := Toolbar; FAvailable.SetBounds(570, 70, 90, 20); FAvailable.Caption := 'Available'; FAvailable.Checked := True; AddButton(Toolbar, 676, 65, 'Add row', AddClicked); Footer := TPanel.Create(Self); Footer.Parent := Self; Footer.Align := alBottom; Footer.Height := 32; Footer.BevelOuter := bvNone; FStatus := TLabel.Create(Self); FStatus.Parent := Footer; FStatus.SetBounds(16, 8, 860, 20);Finish the constructor with the model. All columns are defined before the first row, which
TAtlasTablerequires. AssigningFGrid.Tableconnects the grid, which follows every later change of the table by itself.ImportCSVloads the four starting rows in one step; its header must name the columns exactly. The quoted field"Ink, archival"contains a comma, and the empty last field of the pencil row is a blank date, which every column kind allows.// The schema is fixed before the first row is added. FTable := TAtlasTable.Create(Self); FTable.AddColumn('Product', ackText, 260); FTable.AddColumn('Quantity', ackNumber, 110); FTable.AddColumn('Unit price', ackNumber, 110); FTable.AddColumn('Available', ackBoolean, 100); FTable.AddColumn('Restock', ackDate, 120); FTable.OnChange := TableChanged; FGrid := TAtlasGrid.Create(Self); FGrid.Parent := Self; FGrid.Align := alClient; FGrid.AlignWithMargins := True; FGrid.Margins.SetBounds(16, 8, 16, 8); FGrid.Table := FTable; FTable.ImportCSV( 'Product,Quantity,Unit price,Available,Restock' + sLineBreak + 'Field notebook,120,4.75,true,2026-10-03' + sLineBreak + 'Drafting pencil,300,1.2,true,' + sLineBreak + 'Steel ruler,45,6.5,false,2026-11-15' + sLineBreak + '"Ink, archival",80,12,true,2026-10-20' + sLineBreak); end;Add the three small helpers that the constructor uses to create labels, buttons and value edits.
procedure TMainForm.AddLabel(Parent: TWinControl; Left, Top: Integer; const Caption: string); var Text: TLabel; begin Text := TLabel.Create(Self); Text.Parent := Parent; Text.SetBounds(Left, Top, 180, 16); Text.Caption := Caption; end; procedure TMainForm.AddButton(Parent: TWinControl; Left, Top: Integer; const Caption: string; OnClick: TNotifyEvent); var Button: TButton; begin Button := TButton.Create(Self); Button.Parent := Parent; Button.SetBounds(Left, Top, 100, 30); Button.Caption := Caption; Button.OnClick := OnClick; end; function TMainForm.AddValueEdit(Parent: TWinControl; Left, Width: Integer; Kind: TAtlasColumnKind; Required: Boolean): TAtlasValueEdit; begin Result := TAtlasValueEdit.Create(Self); Result.Parent := Parent; Result.SetBounds(Left, 68, Width, 24); Result.ValueKind := Kind; Result.Required := Required; end;Add the key handler. In this baseline the grid's first Enter only closes the in-place editor, and Escape closes it without discarding the typed text. With the editor open, the handler hands the text to the grid (
EditorMode := False) and stores it withCommitEditon Enter; on Escape it callsCancelEdittwice, because closing the editor turns its text back into a pending edit. Moving to another cell with the arrow keys, Tab or the mouse stores the value without any help.procedure TMainForm.FormKeyDown(Sender: TObject; var Key: Word; Shift: TShiftState); begin // In this baseline the grid's first Enter only closes the in-place editor, // and Escape closes it without discarding the text. Make both keys act at once. if (ActiveControl <> FGrid) or not FGrid.EditorMode then Exit; if Key = VK_RETURN then begin FGrid.EditorMode := False; // hand the editor text to the grid FGrid.CommitEdit; // validate and store it, or raise EAtlasDataError end else if Key = VK_ESCAPE then begin FGrid.CancelEdit; // closing the editor keeps its text pending... FGrid.CancelEdit; // ...so a second call discards it Key := 0; end; end;Add filtering and sorting. The filter box calls
SetFilteron every change, across all columns and ignoring case. Sorting needs no code: clicking a header sorts by that column and a second click reverses it. Clear sort goes back to the original order withSort(-1). Both handlers callCommitEditfirst, so a pending edit is stored before the rows move.procedure TMainForm.FilterChanged(Sender: TObject); begin FGrid.CommitEdit; // a pending edit must land before the view changes FTable.SetFilter(FFilter.Text); end; procedure TMainForm.ClearSortClicked(Sender: TObject); begin FGrid.CommitEdit; FTable.Sort(-1); end;Add new rows. A
TAtlasValueEditchecks its value when focus leaves it: an invalid value raisesEAtlasDataErrorfrom the focus change, and the application reports it.AddClickedcovers what that check cannot see, such as a required box the user never entered:Validatereturns False and puts the reason inValidationMessage, which goes to the status line.NormalizedValuereturns the stored form (12.50becomes12.5).AddRowtakes one value per column, in column order, and validates them again.function TMainForm.Invalid(Edit: TAtlasValueEdit; const Name: string): Boolean; begin Result := not Edit.Validate; if Result then FStatus.Caption := Name + ': ' + Edit.ValidationMessage; end; procedure TMainForm.AddClicked(Sender: TObject); const Flags: array[Boolean] of string = ('false', 'true'); begin if Trim(FProduct.Text) = '' then begin FStatus.Caption := 'Product: a name is required'; Exit; end; if Invalid(FQuantity, 'Quantity') or Invalid(FPrice, 'Unit price') or Invalid(FRestock, 'Restock') then Exit; FGrid.CommitEdit; FTable.AddRow([Trim(FProduct.Text), FQuantity.NormalizedValue, FPrice.NormalizedValue, Flags[FAvailable.Checked], FRestock.NormalizedValue]); FProduct.Clear; FQuantity.Clear; FPrice.Clear; FRestock.Clear; end;Add deletion and export. Grid row 0 is the header, so the table row under the cursor is
FGrid.Row - 1, and it is a view index, which is whatDeleteRowexpects.ExportCSVwithout an argument writes the visible rows in their current order;TFile.WriteAllTextwithTEncoding.UTF8adds a byte order mark.procedure TMainForm.DeleteClicked(Sender: TObject); begin // Grid row 0 is the header; table rows are zero-based view indices. if (FGrid.Row >= 1) and (FGrid.Row <= FTable.RowCount) then begin FGrid.CancelEdit; FTable.DeleteRow(FGrid.Row - 1); end; end; procedure TMainForm.ExportClicked(Sender: TObject); var FileName: string; begin FGrid.CommitEdit; FileName := TPath.Combine(TPath.GetDirectoryName(ParamStr(0)), 'inventory.csv'); TFile.WriteAllText(FileName, FTable.ExportCSV, TEncoding.UTF8); FStatus.Caption := 'Wrote the visible rows to ' + FileName; end;Add the status line.
OnChangefires after every change of the table, including sorting and filtering, soRowCount(visible rows) andSourceRowCount(all rows) are always current.procedure TMainForm.TableChanged(Sender: TObject); var SortText: string; begin if FTable.SortColumn < 0 then SortText := 'unsorted' else if FTable.Ascending then SortText := 'sorted by ' + FTable.Columns[FTable.SortColumn].Name + ' ascending' else SortText := 'sorted by ' + FTable.Columns[FTable.SortColumn].Name + ' descending'; FStatus.Caption := Format('%d of %d rows shown, %s. Click a header to sort; ' + 'Enter stores an edit, Escape discards it.', [FTable.RowCount, FTable.SourceRowCount, SortText]); end;End the program with the main block, then build and run it (F9).
var Form: TMainForm; begin Application.Initialize; Application.MainFormOnTaskbar := True; Application.CreateForm(TMainForm, Form); Application.Run; end.
The window shows four products in a grid with a dark header and alternating row tint; quantities and prices are right-aligned. The status line reads 4 of 4 rows shown, unsorted. Click a header to sort; Enter stores an edit, Escape discards it. Type ink into the filter box and one row remains (1 of 4 rows shown). Click Quantity twice to sort by quantity, descending; the header shows v and 45 comes after 120 because the column is numeric. Type lots into a quantity cell and press Enter: the application reports Invalid finite number: lots, and Escape brings the old value back. Type a product name and click Add row with the quantity box empty: the status line shows Quantity: A value is required and nothing is added. Type 2026-02-30 into the restock box and press Tab: the application reports Expected a valid date in YYYY-MM-DD format. Export CSV writes inventory.csv next to the executable.
To adapt it, replace the columns and the ImportCSV text with your own data, keep a unique code in a read-only column (AddColumn('Code', ackText, 100, False)), and write the rows back to your storage from TableChanged.
Troubleshooting
EAtlasDataError: Define columns before adding rows. AddColumn was called on a table that already holds rows. Define every column first, or call Clear before extending the schema. Columns cannot be removed or renamed.
CSV header does not match the table schema, CSV column does not match: <name> or CSV row <n> has an incorrect field count. ImportCSV needs a first record naming every column exactly, in order and with the same case, and exactly one field per column in every later record. A blank line counts as a record with one empty field, so remove blank lines, including a second line break at the end of the file. Nothing is imported until the whole text is valid.
Invalid finite number: 12,5, or dates such as 21.09.2026 are rejected. Table values use the invariant format, not the user's locale: . as the decimal point, no thousands separators, dates as YYYY-MM-DD. Convert user input before AddRow or SetCell, or validate it with a TAtlasValueEdit.
After Enter a grid cell shows its old value, or a value comes back after Escape. The first Enter only closes the in-place editor; the value is stored at the next move, header click or focus change. Escape in an open editor keeps the text pending, and the next move stores it. Press Escape twice, or add the form-level key handler from the tutorial (step 6).
A value typed into the grid ended up in a different row. Code sorted, filtered or changed the table while the in-place editor was still open, for example from a timer or a shortcut that does not move the focus. The typed text stays pending and is stored into the row that is under the cursor after the change. Call Grid.CommitEdit before such changes; the tutorial's handlers do.
Typing =A1*2 into a TAtlasSheetGrid cell leaves only 2. In this baseline every keystroke is stored in the workbook, and the refresh that follows selects the whole editor text, so the next keystroke replaces it. Remove goEditing from Options and edit through a formula bar (an edit box that calls Workbook.SetCell), as in the TAtlasSheetGrid reference.
EInvalidOperation: Control '...' has no parent window from TAtlasTree or TAtlasRichEdit. Tree nodes and RTF streaming need a window handle. Set Parent to a form or panel before calling AddNode, LoadJSON or LoadRTF; the form itself does not have to be visible.
Leaving a TAtlasValueEdit shows an error message and OnExit does not run. When focus leaves, the edit replaces its text with NormalizedValue, which raises for an invalid value or for a blank value while Required is True. The check cannot be switched off. To warn users before they leave the box, show ValidationMessage from the edit's OnChange whenever Validate returns False; to report a missing value only when the user saves, leave Required False and check for blanks in your own code.
Column widths snap back after an edit, sort or filter. TAtlasGrid resets every column to TAtlasColumn.Width (scaled to the monitor DPI) after each table change. Choose the widths in AddColumn; the grid does not keep widths the user dragged.
Pivot aggregation requires finite numeric values. Summarize with aaSum, aaMinimum, aaMaximum or aaAverage found a non-numeric value in the value column. Use an ackNumber column (empty values are skipped), or aaCount, which counts rows of any kind.
TAtlasInspector does not list a property. Only published properties of integer, Int64, floating-point, enumeration, Char and string types are listed. Public properties, sets, class properties such as Font and events are not. Read-only properties are listed but cannot be changed.
The spell checker reports a word that is in the dictionary. Lookups ignore case for ASCII letters only, so a dictionary entry café does not match CAFÉ; add the other spelling too. Digits and hyphens split words (e-mail is checked as e and mail), and an apostrophe only joins letters (isn't).