YOUSEFUL / DELPHI
Atlas Info: FAQ, examples and tutorial
Learning center · Component reference · Getting started
Atlas Info adds data-aware controls to the standard VCL database stack. It has a grid with validated typed edits
and CSV export, a lookup combo box you can drive from code, and a rich text editor for RTF in blob fields. It also
has a text filter controller with an edit box that drives it. The controls work with any TDataSet through a
TDataSource; the tests and samples use FireDAC TFDMemTable, so no database server is needed to try them. None
of them posts a record on its own: every edit leaves the dataset in edit state until your code calls Post or
Cancel. Add Atlas.DBControls (TAtlasDBGrid, TAtlasDBLookupCombo, TAtlasDBRichEdit, EAtlasDatabaseError)
and Atlas.DBFilter (TAtlasDataFilter, TAtlasFilterEdit, TAtlasTextMatch) to uses. Atlas Info requires
Atlas VCL: put both src folders on the search path, or install the Atlas VCL runtime package before
AtlasInfoRuntime. The design package places the components on the Atlas Data palette page.
FAQ
Does editing through the grid post the record? No. TAtlasDBGrid.EditValue checks the text, puts the dataset
into edit state and assigns the field; TAtlasDBLookupCombo.SelectKey, SelectText and the TAtlasDBRichEdit
methods do the same. Your code decides when to Post or Cancel. The one exception is standard TDataSet
behaviour: moving the cursor to another record posts a pending change.
What text does EditValue accept? Invariant text for the field's type. Integers are range-checked for their
type. Floating-point and BCD fields take a . decimal separator (1250.75, 1e3), Booleans take true, false,
1 or 0, and ftDate takes YYYY-MM-DD. String fields take text up to the field size, and memos take anything.
Empty text means Null unless the field is Required. Other types, including ftDateTime, ftTime,
ftTimeStamp and blobs, are refused: set them through the field (AsDateTime, LoadFromStream) instead. Every
check runs before the record is touched.
Which rows and columns does ExportCSV write? The grid's visible columns that are bound to a field, in column
order, with each field's DisplayLabel as the header. ExportCSV writes every record the dataset currently lets
through, so filters apply. ExportCSV(True) writes only the grid's SelectedRows, in dataset order. Values use
invariant formats (1200.5, true, 2026-01-02, 2026-01-02T12:00:00.000), and fields with commas, quotes or
line breaks are quoted. The current record is restored afterwards. The method returns a string; saving it is up to
you.
Can I export or filter without showing a form? Yes. The controls need a parent window, but it does not have
to be visible: the FilterConsole and
RichTextConsole samples run as console programs
with a form that is never shown. TAtlasDataFilter is non-visual and needs no parent at all.
How does TAtlasDataFilter combine with the dataset's own filter? On the first Apply it remembers the
dataset's OnFilterRecord handler and Filtered value, installs its own handler and sets Filtered to True. Its
handler calls yours first and tests the text only for records yours accepts. The dataset's Filter expression
still applies, because Filtered is True. Clear, freeing the controller, or Apply with empty text puts your
handler and Filtered value back.
Can two filters share a dataset? No. The second controller's Apply raises Only one Atlas filter may own a dataset at a time. Filter one field per dataset, or Clear the first controller before using another. Also do not
assign OnFilterRecord yourself while a filter is installed; assign your handler before the first Apply and it
is combined automatically.
What happens if I free the filter while a record is being edited? Nothing changes until the edit ends.
Changing the filter during an edit would make FireDAC post the record, so the controller detaches. A small
helper owned by the dataset restores your handler and Filtered value after the next Post or Cancel. Clear
and Apply during an edit raise instead of posting.
Does the filter ignore case for accented letters? No. The field text and the filter text are both lower-cased
with LowerCase, which folds only the ASCII letters A to Z. émile matches émile but not Émile. Numbers and
dates are matched as their AsString text.
How do I choose a lookup value in code? Call SelectKey with a key, or SelectText with display text.
SelectText searches the first ListField case-insensitively, as a prefix by default or as the whole value
with Partial False. A key that is not in the lookup table raises Lookup key does not exist and leaves the
master record alone. An unknown text returns False. The lookup table's cursor does not move.
Which fields can hold rich text? Any TBlobField: ftBlob, ftMemo or ftWideMemo. Keep PlainText
False (the constructor sets it). LoadRTF refuses bytes that do not start with {\rtf, FormatSelection
formats the selection in the control, and CommitToField copies the control's RTF into the field. For a rich
editor that is not data-aware, with find and replace, use TAtlasRichEdit from Atlas VCL.
How does the grid handle high DPI and VCL styles? LogicalRowHeight (default 28, allowed 18 to 120) is the row
height at 96 PPI. The grid keeps DefaultRowHeight at ScaledRowHeight, which is the logical height multiplied by
the current PPI and follows per-monitor DPI changes. Cells are painted with the window and highlight colours of
the active VCL style. Row scaling is tested at 96, 144, 192, 288 and 384 PPI. Custom VCL styles, real multi-monitor
and 4K hardware, and accessibility have not been checked by hand.
What is not implemented? Atlas Info adds no column classes, events, sorting, grouping, footers or printing
to the grid. The filter has no multi-condition filters, filter dialog or typing delay, and the rich edit has no
toolbar, spell checking or find and replace. Nothing in Atlas Info reads forms or code written for other component
libraries. Installing the design package in the IDE has not been tried. For data that is not in a TDataSet,
Atlas VCL has TAtlasTable and TAtlasGrid.
Examples
The examples use the customer table from the tutorial: ID, Name, CategoryID, Balance and Active fields,
with four customers (Acorn Design, Harbor Services, Northwind Workshop, Cedar Systems). The rich text example needs
an extra Notes blob field. Each snippet is a complete routine; pass it your own grid, filter or dataset.
Create a grid in code with denser rows, and set a value from text and post it:
uses Data.DB, Vcl.Controls, Atlas.DBControls;
function AddCustomerGrid(Parent: TWinControl; Source: TDataSource): TAtlasDBGrid;
begin
Result := TAtlasDBGrid.Create(Parent);
Result.Parent := Parent;
Result.Align := alClient;
Result.DataSource := Source;
Result.LogicalRowHeight := 22; // at 96 PPI; 33 pixels at 144 PPI
end;
procedure SetBalance(Grid: TAtlasDBGrid; const Text: string);
begin
Grid.EditValue('Balance', Text); // validates, then leaves the record in edit state
Grid.DataSource.DataSet.Post; // EditValue never posts; your code does
end;After SetBalance(Grid, '1250.75') the current customer's balance is 1250.75 and the dataset is browsing again.
Refuse bad input without leaving the record in edit state:
uses System.SysUtils, Data.DB, Atlas.DBControls;
function TrySetBalance(Grid: TAtlasDBGrid; const Text: string): Boolean;
var
DataSet: TDataSet;
begin
DataSet := Grid.DataSource.DataSet;
try
Grid.EditValue('Balance', Text);
DataSet.Post;
Result := True;
except
on E: Exception do
begin
if DataSet.State in dsEditModes then
DataSet.Cancel;
Writeln('Rejected: ', E.Message);
Result := False;
end;
end;
end;TrySetBalance(Grid, 'abc') prints Rejected: Invalid finite number: abc and returns False; TrySetBalance(Grid, '1e3')
stores 1000.
Save the visible rows to a file, or the selected rows to a string:
uses System.SysUtils, System.IOUtils, Atlas.DBControls;
procedure SaveVisibleRows(Grid: TAtlasDBGrid; const FileName: string);
begin
// Visible columns only, every record the dataset's filter lets through.
TFile.WriteAllText(FileName, Grid.ExportCSV, TEncoding.UTF8);
end;
function SelectedAsCSV(Grid: TAtlasDBGrid): string;
begin
Grid.SelectedRows.CurrentRowSelected := True; // add the current record to the selection
Result := Grid.ExportCSV(True);
end;With the CategoryID column hidden (Grid.Columns[2].Visible := False), the file contains:
ID,Name,Balance,Active
1,Acorn Design,1200.5,true
2,Harbor Services,850,true
3,Northwind Workshop,2400,false
4,Cedar Systems,450,trueFilter the customers by name prefix, and show everyone again:
uses System.Classes, Data.DB, Atlas.DBFilter;
function AddNameFilter(Owner: TComponent; DataSet: TDataSet): TAtlasDataFilter;
begin
Result := TAtlasDataFilter.Create(Owner);
Result.DataSet := DataSet;
end;
procedure ShowNamesStartingWith(Filter: TAtlasDataFilter; const Prefix: string);
begin
Filter.Apply('Name', Prefix, atmPrefix); // an empty Prefix clears the filter
end;
procedure ShowAll(Filter: TAtlasDataFilter);
begin
if Filter.Installed and (Filter.DataSet.State in dsEditModes) then
Filter.DataSet.Post; // or Cancel: Clear refuses to run during an edit
Filter.Clear;
end;ShowNamesStartingWith(Filter, 'n') leaves one record, Northwind Workshop. ShowAll(Filter) shows all four again.
Let the user filter by typing:
uses Vcl.Controls, Atlas.DBFilter;
function AddFilterBox(Parent: TWinControl; Filter: TAtlasDataFilter): TAtlasFilterEdit;
begin
Result := TAtlasFilterEdit.Create(Parent);
Result.Parent := Parent;
Result.Filter := Filter;
Result.FieldName := 'Name';
Result.Match := atmContains;
Result.TextHint := 'Filter customer names';
end;Typing SERV leaves Harbor Services; clearing the box shows all four customers.
Choose a lookup value from typed text. The combo box is set up with ListSource on the categories (1 Standard,
2 Priority, 3 Partner), KeyField ID, ListField Name, and DataSource and DataField on the customer's
CategoryID:
uses Atlas.DBControls;
function ChooseCategory(Lookup: TAtlasDBLookupCombo; const Typed: string): Boolean;
begin
Result := Lookup.SelectText(Typed); // case-insensitive prefix match on the first ListField
if Result then
Lookup.DataSource.DataSet.Post; // SelectText leaves the master record in edit state
end;ChooseCategory(Lookup, 'par') returns True and stores CategoryID 3 (Partner) in the current customer.
Store rich text in the Notes blob field and make the first word bold:
uses System.SysUtils, Vcl.Graphics, Atlas.DBControls;
procedure ReplaceNotes(Rich: TAtlasDBRichEdit; const RTF: TBytes);
begin
Rich.LoadRTF(RTF); // validates, writes the field, leaves the record in edit state
Rich.DataSource.DataSet.Post;
end;
procedure EmphasiseFirstWord(Rich: TAtlasDBRichEdit; WordLength: Integer);
begin
Rich.SelStart := 0;
Rich.SelLength := WordLength;
Rich.FormatSelection('Segoe UI', 14, [fsBold], clNavy); // changes the control, starts editing
Rich.CommitToField; // copies the formatted RTF into the field
Rich.DataSource.DataSet.Post;
end;After ReplaceNotes(Rich, TEncoding.ASCII.GetBytes('{\rtf1\ansi Welcome notes}')) and
EmphasiseFirstWord(Rich, 7), the record's notes read "Welcome notes" with "Welcome" in bold navy 14-point
Segoe UI, and the dataset is browsing again.
Tutorial
Sample project: DataGridEditing.
Goal: build a customer editor in which every change waits for an explicit Post or Cancel.
Create a folder named
DataGridEditing. Type the code from steps 2 to 6, in order, into a text file namedDataGridEditing.dprin that folder, then open it in Delphi; the IDE creates the project file. In Project > Options > Building > Delphi Compiler > Search path, add the Atlas Infosrcfolder and the Atlas VCLsrcfolder (see Getting started). FireDAC ships with Delphi, so nothing else is needed.Type the program header and the form class. The form keeps the two tables, their data sources, the grid, the filter, the balance box and a status label. There is no
.dfm: the constructor builds everything.program DataGridEditing; // Atlas Info quick start: a customer table in a FireDAC memory dataset shown // in a TAtlasDBGrid, a TAtlasDBLookupCombo for the category, a // TAtlasFilterEdit over the names, and explicit Post/Cancel buttons. // The form is created in code; there is no .dfm file. uses System.SysUtils, System.Classes, System.Variants, System.IOUtils, Data.DB, FireDAC.Comp.Client, Vcl.Forms, Vcl.Controls, Vcl.StdCtrls, Vcl.ExtCtrls, Vcl.Dialogs, Atlas.DBControls, Atlas.DBFilter; type TMainForm = class(TForm) private FCustomers, FCategories: TFDMemTable; FSource, FCategorySource: TDataSource; FGrid: TAtlasDBGrid; FFilter: TAtlasDataFilter; FBalance: TEdit; FStatus: TLabel; function AddButton(AParent: TWinControl; const ACaption: string; ALeft: Integer; AClick: TNotifyEvent): TButton; procedure PostClick(Sender: TObject); procedure CancelClick(Sender: TObject); procedure SetBalanceClick(Sender: TObject); procedure ExportClick(Sender: TObject); procedure StateChange(Sender: TObject); public constructor Create(AOwner: TComponent); override; end;Add the two memory tables.
CreateDataSetmakes an empty in-memory table from the field definitions, andAppendRecordadds a posted record, so both tables start in browse state.function CreateCustomers(Owner: TComponent): TFDMemTable; begin Result := TFDMemTable.Create(Owner); Result.FieldDefs.Add('ID', ftInteger); Result.FieldDefs.Add('Name', ftWideString, 80); Result.FieldDefs.Add('CategoryID', ftInteger); Result.FieldDefs.Add('Balance', ftFloat); Result.FieldDefs.Add('Active', ftBoolean); Result.CreateDataSet; Result.AppendRecord([1, 'Acorn Design', 2, 1200.5, True]); Result.AppendRecord([2, 'Harbor Services', 1, 850, True]); Result.AppendRecord([3, 'Northwind Workshop', 3, 2400, False]); Result.AppendRecord([4, 'Cedar Systems', 2, 450, True]); Result.First; end; function CreateCategories(Owner: TComponent): TFDMemTable; begin Result := TFDMemTable.Create(Owner); Result.FieldDefs.Add('ID', ftInteger); Result.FieldDefs.Add('Name', ftWideString, 40); Result.CreateDataSet; Result.AppendRecord([1, 'Standard']); Result.AppendRecord([2, 'Priority']); Result.AppendRecord([3, 'Partner']); Result.First; end;Write the constructor. It calls
CreateNew, because there is no form file. It connectsTAtlasFilterEditto aTAtlasDataFilteron the customers, and puts aTAtlasDBLookupCombobelow the grid that shows the category name and stores itsIDinCategoryID.LogicalRowHeightis set to 30 pixels at 96 PPI, and theCategoryIDcolumn (index 2) is hidden because the lookup shows it.constructor TMainForm.Create(AOwner: TComponent); var Header, Bottom: TPanel; FilterEdit: TAtlasFilterEdit; Lookup: TAtlasDBLookupCombo; Caption1: TLabel; begin inherited CreateNew(AOwner); Caption := 'Atlas Info: customer editor'; Width := 900; Height := 560; Position := poScreenCenter; Font.Name := 'Segoe UI'; Font.Size := 10; FCustomers := CreateCustomers(Self); FCategories := CreateCategories(Self); FSource := TDataSource.Create(Self); FSource.DataSet := FCustomers; FSource.OnStateChange := StateChange; FCategorySource := TDataSource.Create(Self); FCategorySource.DataSet := FCategories; Header := TPanel.Create(Self); Header.Parent := Self; Header.Align := alTop; Header.Height := 88; Header.BevelOuter := bvNone; FFilter := TAtlasDataFilter.Create(Self); FFilter.DataSet := FCustomers; FilterEdit := TAtlasFilterEdit.Create(Self); FilterEdit.Parent := Header; FilterEdit.SetBounds(16, 12, 260, 28); FilterEdit.Filter := FFilter; FilterEdit.FieldName := 'Name'; FilterEdit.Match := atmContains; FilterEdit.TextHint := 'Filter customer names'; FBalance := TEdit.Create(Self); FBalance.Parent := Header; FBalance.SetBounds(16, 48, 120, 28); FBalance.TextHint := 'New balance'; AddButton(Header, 'Set balance', 144, SetBalanceClick); AddButton(Header, 'Post', 300, PostClick); AddButton(Header, 'Cancel', 420, CancelClick); AddButton(Header, 'Export CSV', 540, ExportClick); FStatus := TLabel.Create(Self); FStatus.Parent := Header; FStatus.SetBounds(300, 16, 400, 24); Bottom := TPanel.Create(Self); Bottom.Parent := Self; Bottom.Align := alBottom; Bottom.Height := 80; Bottom.BevelOuter := bvNone; Caption1 := TLabel.Create(Self); Caption1.Parent := Bottom; Caption1.SetBounds(16, 10, 300, 22); Caption1.Caption := 'Category of the selected customer'; Lookup := TAtlasDBLookupCombo.Create(Self); Lookup.Parent := Bottom; Lookup.SetBounds(16, 36, 240, 28); Lookup.ListSource := FCategorySource; Lookup.KeyField := 'ID'; Lookup.ListField := 'Name'; Lookup.DataSource := FSource; Lookup.DataField := 'CategoryID'; FGrid := TAtlasDBGrid.Create(Self); FGrid.Parent := Self; FGrid.Align := alClient; FGrid.DataSource := FSource; FGrid.LogicalRowHeight := 30; FGrid.Columns[0].Width := 50; FGrid.Columns[1].Width := 260; FGrid.Columns[2].Visible := False; FGrid.Columns[3].Width := 120; FGrid.Columns[4].Width := 80; StateChange(FSource); end;Add the button helper and the event handlers. The status label follows the data source's
OnStateChange. Post and Cancel end the edit. Set balance callsEditValue, which validates the text for the field type and leaves the record in edit state. Export CSV callsExportCSV, which refuses to run while a change is pending. Both show the error text when they are refused.function TMainForm.AddButton(AParent: TWinControl; const ACaption: string; ALeft: Integer; AClick: TNotifyEvent): TButton; begin Result := TButton.Create(Self); Result.Parent := AParent; Result.SetBounds(ALeft, 48, 110, 28); Result.Caption := ACaption; Result.OnClick := AClick; end; procedure TMainForm.StateChange(Sender: TObject); begin if FCustomers.State in dsEditModes then FStatus.Caption := 'Editing: press Post to keep the change or Cancel to discard it' else FStatus.Caption := 'Browsing: no pending changes'; end; procedure TMainForm.PostClick(Sender: TObject); begin if FCustomers.State in dsEditModes then FCustomers.Post; end; procedure TMainForm.CancelClick(Sender: TObject); begin FCustomers.Cancel; end; procedure TMainForm.SetBalanceClick(Sender: TObject); begin try // Validates the text for the field type; the record stays in edit state. FGrid.EditValue('Balance', FBalance.Text); except on E: Exception do ShowMessage(E.Message); end; end; procedure TMainForm.ExportClick(Sender: TObject); var FileName: string; begin try FileName := TPath.Combine(TPath.GetTempPath, 'customers.csv'); TFile.WriteAllText(FileName, FGrid.ExportCSV, TEncoding.UTF8); ShowMessage('Exported the visible columns to ' + FileName); except on E: Exception do ShowMessage(E.Message); end; end;Finish with the program block, then build and run (F9).
var MainForm: TMainForm; begin Application.Initialize; Application.MainFormOnTaskbar := True; Application.CreateForm(TMainForm, MainForm); Application.Run; end.A window titled Atlas Info: customer editor opens. The grid shows the four customers with the columns ID, Name, Balance and Active, and the label reads
Browsing: no pending changes. The category box showsPriorityfor Acorn Design.Try an edit. Type
1250.75in the balance box and click Set balance. The Balance cell of the current row changes and the label readsEditing: press Post to keep the change or Cancel to discard it. Click Cancel: the balance goes back to 1200.5. Repeat and click Post to keep it. Typeabcand click Set balance: a message reportsInvalid finite number: abcand the record is not put into edit state.Try the filter, the lookup and the export. Type
arin the filter box: only Harbor Services and Cedar Systems remain, and clearing the box shows all four. PickPartnerin the category box: the record goes into edit state, and Post stores category 3. Click Export CSV: the message names the file, which starts withID,Name,Balance,Active. With a change pending, the export showsPost or cancel before exportinginstead.
The finished program is the DataGridEditing
sample. To adapt it, replace CreateCustomers with a FireDAC query on your own connection, and change the
field names passed to EditValue, FilterEdit.FieldName and the lookup's KeyField, ListField and
DataField. For more on filters, CSV output and lookups without a window, continue with
FilterConsole. For rich text in a blob field, see
RichTextConsole.
Troubleshooting
Post or cancel before exporting, Post or cancel before filtering or Post or cancel before clearing the filter.
The dataset is in edit or insert state, and these operations refuse to post it for you. Call Post or Cancel
first, then export, filter or clear.
Typing in the filter box shows an error and the grid does not change. A record was being edited, so
TAtlasFilterEdit passed on the Apply error. The box keeps the typed text while the dataset keeps its previous
filter. Post or cancel, then change the text again. Typing the same text again does not trigger a change.
Only one Atlas filter may own a dataset at a time. A second TAtlasDataFilter tried to filter a dataset that
another one already filters. Use one controller per dataset, or Clear or free the first one before applying
the second.
The application replaced this filter hook. Code assigned the dataset's OnFilterRecord while the filter was
installed. Clear the filter, assign your handler, then call Apply again; your handler is then combined with the
text match.
Fewer records are shown than the text match explains. The dataset had a Filter expression assigned but
Filtered False. Apply sets Filtered to True, which activates the expression too. Empty the Filter property
if you do not want it; Clear sets Filtered back to False.
Names with accented capitals are not found. Matching lower-cases only ASCII letters, so é does not match
É. Type the letter in the case stored in the data, or add your own OnFilterRecord handler (assigned before the
first Apply) that uses a locale-aware comparison.
Invalid finite number for 1,5, or Date fields must contain digits or Expected a valid date in YYYY-MM-DD format for a date. EditValue takes invariant
text: a . decimal separator and ISO dates. Convert text entered in the user's locale first, or assign the field
directly (Field.AsFloat := ...).
Use a typed dataset field API for this field type. The field is a date-time, time, timestamp, blob or other
type that EditValue does not convert. Set it through the field, for example
DataSet.FieldByName('Stamp').AsDateTime := Now after calling Edit.
LoadRTF raises ... has no parent window. Windows Rich Edit needs a window handle. Set the
TAtlasDBRichEdit's Parent before loading; the form can stay hidden.
RTF requires a blob or memo field. DataField names a string or other non-blob field. Use an ftBlob,
ftMemo or ftWideMemo field for rich text.
A change was saved although nobody pressed Post. The cursor moved to another record while the change was
pending, for example by clicking another grid row. Every TDataSet posts a modified record before it moves;
this is standard dataset behaviour, not something Atlas Info adds. Handle the dataset's BeforePost event if the
user must confirm or refuse the change.
The CSV header shows the field name, not my column title. ExportCSV uses the field's DisplayLabel.
With the grid's default columns a Title.Caption change updates DisplayLabel, but with customized columns it
does not. Set Field.DisplayLabel instead.