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:

pascal
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:

pascal
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:

pascal
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:

text
ID,Name,Balance,Active
1,Acorn Design,1200.5,true
2,Harbor Services,850,true
3,Northwind Workshop,2400,false
4,Cedar Systems,450,true

Filter the customers by name prefix, and show everyone again:

pascal
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:

pascal
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:

pascal
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:

pascal
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.

  1. Create a folder named DataGridEditing. Type the code from steps 2 to 6, in order, into a text file named DataGridEditing.dpr in that folder, then open it in Delphi; the IDE creates the project file. In Project > Options > Building > Delphi Compiler > Search path, add the Atlas Info src folder and the Atlas VCL src folder (see Getting started). FireDAC ships with Delphi, so nothing else is needed.

  2. 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.

    pascal
    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;
  3. Add the two memory tables. CreateDataSet makes an empty in-memory table from the field definitions, and AppendRecord adds a posted record, so both tables start in browse state.

    pascal
    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;
  4. Write the constructor. It calls CreateNew, because there is no form file. It connects TAtlasFilterEdit to a TAtlasDataFilter on the customers, and puts a TAtlasDBLookupCombo below the grid that shows the category name and stores its ID in CategoryID. LogicalRowHeight is set to 30 pixels at 96 PPI, and the CategoryID column (index 2) is hidden because the lookup shows it.

    pascal
    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;
  5. 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 calls EditValue, which validates the text for the field type and leaves the record in edit state. Export CSV calls ExportCSV, which refuses to run while a change is pending. Both show the error text when they are refused.

    pascal
    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;
  6. Finish with the program block, then build and run (F9).

    pascal
    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 shows Priority for Acorn Design.

  7. Try an edit. Type 1250.75 in the balance box and click Set balance. The Balance cell of the current row changes and the label reads Editing: 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. Type abc and click Set balance: a message reports Invalid finite number: abc and the record is not put into edit state.

  8. Try the filter, the lookup and the export. Type ar in the filter box: only Harbor Services and Cedar Systems remain, and clearing the box shows all four. Pick Partner in the category box: the record goes into edit state, and Post stores category 3. Click Export CSV: the message names the file, which starts with ID,Name,Balance,Active. With a change pending, the export shows Post or cancel before exporting instead.

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.