YOUSEFUL / DELPHI / COMPONENTS

TAtlasTable

An in-memory table with a fixed, typed schema, a sorted and filtered view, change notifications and CSV import and export.

Non-visual component Palette: Atlas Unit Atlas.Data · Inherits TComponent · Product Atlas VCL

Description

TAtlasTable (unit Atlas.Data) is the data model behind TAtlasGrid, TAtlasPivot and TAtlasPrintDocument. It keeps rows of strings in memory. Each column has a TAtlasColumnKind that decides which strings are accepted and how they are stored (12.50 becomes 12.5, 1 becomes true, dates are YYYY-MM-DD). Empty values are allowed in every column.

Schema first. Call AddColumn for every column before the first row; once the table holds rows, AddColumn raises. Columns cannot be removed or renamed. Only OnChange is published, so columns are always defined in code.

View and source. Sort and SetFilter build a view. RowCount, Cell, SetCell, DeleteRow and SourceIndex take zero-based view row indices, which change with every sort, filter, insert and delete. SourceIndex returns the current position in the underlying list, not a persistent ID; keep a business key in a column of its own. SourceRowCount counts all rows.

Notifications. Every change (AddColumn, AddRow, SetCell, DeleteRow, Clear, Sort, SetFilter, ImportCSV) calls each listener registered with AddListener, then OnChange. Grids register themselves as listeners, so several views can share one table.

Errors. Invalid input raises EAtlasDataError and leaves the table unchanged; ImportCSV validates the whole text before it replaces any row.

Unit functions. Atlas.Data also declares function AtlasParseCSV(const Text: string): TArray<TAtlasRow>; (quoted fields, doubled quotes, CR, LF or CRLF line ends; a leading byte order mark is skipped; malformed quoting raises EAtlasDataError), function AtlasCSVField(const Text: string): string; (quotes a field that contains a comma, quote, CR or LF) and function AtlasNormalizeValue(Kind: TAtlasColumnKind; const Value: string): string; (validates one value and returns its stored form, exactly as AddRow does).

The table is meant for the VCL main thread and is not thread-safe. It has no database connection, no undo and no persistence other than CSV text.

Members

MemberKindWhat it does
CreateconstructorCreates an empty table: no columns, no sort (SortColumn = -1, Ascending = True) and no filter (FilterColumn = -1).
DestroydestructorFrees the rows.
AddColumnmethodAdds a column and returns its zero-based index.
AddRowmethodValidates one value per column, stores the normalized values and returns the new row's position in the underlying list.
ClearmethodRemoves every row and notifies.
DeleteRowmethodDeletes the row shown at view index ViewRow.
SourceIndexmethodReturns the position in the underlying list of the row at view index ViewRow.
CellmethodReturns the stored value in Column of the row at view index ViewRow: the normalized form (12.5, true), or '' for an empty value.
SetCellmethodValidates Value and stores it in Column of the row at view index ViewRow, then rebuilds the view: the row may move (when the sorted column changed) or disappear (when it no longer matches the filter).
SortmethodSorts the view by Column; -1 restores source order.
SetFiltermethodShows only the rows whose stored value contains Text, ignoring case (ContainsText, Unicode-aware): in column Column, or in any column when Column is -1.
ExportCSVmethodReturns CSV text: a header record with the column names, then one record per row, CRLF line ends, and fields quoted only when they contain a comma, quote, CR or LF.
ImportCSVmethodReplaces all rows with the records of a CSV text.
AddListenermethodRegisters a method that is called, with the table as Sender, after every change and before OnChange.
RemoveListenermethodRemoves a listener registered with AddListener.
ColumnCountpropertyNumber of columns.
RowCountpropertyNumber of rows in the current view, after the filter.
SourceRowCountpropertyNumber of rows in the table, whatever the filter.
ColumnspropertyA copy of the definition of the column at Index, as a TAtlasColumn record; changing the copy does not change the table.
SortColumnpropertyThe column the view is sorted by, or -1 when it is in source order.
AscendingpropertyThe direction of the current sort (True after Create).
FilterTextpropertyThe current filter text; '' when there is no filter.
FilterColumnpropertyThe column the filter searches, or -1 for all columns.
OnChangeeventCalled after every change, after the listeners.

Constructors

Create constructor

constructor Create(AOwner: TComponent); override;

Creates an empty table: no columns, no sort (SortColumn = -1, Ascending = True) and no filter (FilterColumn = -1).

Destructors

Destroy destructor

destructor Destroy; override;

Frees the rows. Grids that show the table receive a free notification and set their Table to nil.

Properties

ColumnCount property

property ColumnCount: Integer read GetColumnCount;

Number of columns.

RowCount property

property RowCount: Integer read GetRowCount;

Number of rows in the current view, after the filter.

SourceRowCount property

property SourceRowCount: Integer read GetSourceRowCount;

Number of rows in the table, whatever the filter.

Columns property

property Columns[Index: Integer]: TAtlasColumn read GetColumn;

A copy of the definition of the column at Index, as a TAtlasColumn record; changing the copy does not change the table. Raises EAtlasDataError for an invalid index.

pascal
Caption := Table.Columns[0].Name;

SortColumn property

property SortColumn: Integer read FSortColumn;

The column the view is sorted by, or -1 when it is in source order.

Ascending property

property Ascending: Boolean read FAscending;

The direction of the current sort (True after Create).

FilterText property

property FilterText: string read FFilter;

The current filter text; '' when there is no filter.

FilterColumn property

property FilterColumn: Integer read FFilterColumn;

The column the filter searches, or -1 for all columns.

Events

OnChange event

property OnChange: TNotifyEvent read FOnChange write FOnChange;

Called after every change, after the listeners. Use it for one application hook such as a status line; use AddListener when several objects need notifications.

Methods

AddColumn method

function AddColumn(const Name: string; Kind: TAtlasColumnKind = ackText; Width: Integer = 120; Editable: Boolean = True): Integer;

Adds a column and returns its zero-based index. Kind decides validation and sorting (default ackText). Width is the preferred grid width in pixels at 96 dpi, at least 16 (default 120). Editable = False makes the column read-only for SetCell and in grids, while AddRow and ImportCSV can still fill it. Raises EAtlasDataError when the table already holds rows, when Name is blank, when Width is below 16, or when another column has the same name (compared ignoring the case of ASCII letters). Notifies listeners.

pascal
Table.AddColumn('Invoice', ackText, 100, False); // read-only key
Table.AddColumn('Amount', ackNumber);

AddRow method

function AddRow(const Values: array of string): Integer;

Validates one value per column, stores the normalized values and returns the new row's position in the underlying list. That is not its view index: a sorted view places the row where it belongs and a filter may hide it. Raises EAtlasDataError when the table has no columns, when the number of values differs from ColumnCount, or when a value does not fit its column; nothing is added then.

pascal
Table.AddRow(['INV-001', '125.50']); // stored as INV-001, 125.5

Clear method

procedure Clear;

Removes every row and notifies. Columns, sort and filter settings are kept. Because the table is empty afterwards, more columns may be added.

DeleteRow method

procedure DeleteRow(ViewRow: Integer);

Deletes the row shown at view index ViewRow. Raises EAtlasDataError when the index is out of range. The source positions of later rows move down by one.

SourceIndex method

function SourceIndex(ViewRow: Integer): Integer;

Returns the position in the underlying list of the row at view index ViewRow. Raises EAtlasDataError when the index is out of range. The position changes when earlier rows are deleted or the table is reloaded, so do not store it as an identifier.

Cell method

function Cell(Column, ViewRow: Integer): string;

Returns the stored value in Column of the row at view index ViewRow: the normalized form (12.5, true), or '' for an empty value. Raises EAtlasDataError for an invalid column or row index.

pascal
if Table.Cell(3, Row) = 'true' then ...

SetCell method

procedure SetCell(Column, ViewRow: Integer; const Value: string);

Validates Value and stores it in Column of the row at view index ViewRow, then rebuilds the view: the row may move (when the sorted column changed) or disappear (when it no longer matches the filter). Raises EAtlasDataError for an invalid index, for a read-only column (Column is read-only) or for a value that does not fit the column; the old value is kept then.

pascal
Table.SetCell(1, 0, '99.90');

Sort method

procedure Sort(Column: Integer; Ascending: Boolean = True);

Sorts the view by Column; -1 restores source order. ackNumber columns compare numerically; the other kinds compare as text with CompareText (ordinal, ignoring the case of ASCII letters), so ISO dates sort chronologically. Empty values come first when ascending and last when descending. Rows with equal keys keep their source order in both directions. The table keeps the sort and reapplies it after every change. Only one sort column is supported. Raises EAtlasDataError for an index other than -1 that is out of range.

pascal
Table.Sort(1, False); // largest amount first
Table.Sort(-1);       // back to source order

SetFilter method

procedure SetFilter(const Text: string; Column: Integer = -1);

Shows only the rows whose stored value contains Text, ignoring case (ContainsText, Unicode-aware): in column Column, or in any column when Column is -1. An empty Text shows every row. The filter matches the normalized text (true, 12.5), not what was originally typed, and there are no expressions or multiple conditions. The filter is kept and reapplied after every change. Raises EAtlasDataError for an invalid column index.

pascal
Table.SetFilter('north');    // any column
Table.SetFilter('false', 3); // column 3 only

ExportCSV method

function ExportCSV(VisibleOnly: Boolean = True): string;

Returns CSV text: a header record with the column names, then one record per row, CRLF line ends, and fields quoted only when they contain a comma, quote, CR or LF. With VisibleOnly = True (the default) it writes the current view in view order; with False, every row in source order. Values are written raw: text starting with =, +, - or @ is not escaped, so apply your own policy before untrusted data reaches spreadsheet software. The string has no byte order mark; TFile.WriteAllText(Name, Text, TEncoding.UTF8) writes one.

ImportCSV method

procedure ImportCSV(const Text: string);

Replaces all rows with the records of a CSV text. The first record must name every column exactly, in order and with the same case; every later record must have exactly ColumnCount fields and every field must fit its column. The whole text is parsed and validated before any row is replaced, so on failure it raises EAtlasDataError (CSV header does not match the table schema, CSV column does not match: <name>, CSV row <n> has an incorrect field count, or the value error) and the table keeps its rows. A header-only text clears the rows; a leading byte order mark is skipped. A blank line is a record with one empty field, so a blank line in a file with several columns fails the field count (one final line break is fine). Sort and filter settings are kept. Load large data with one ImportCSV rather than many AddRow calls: the view is rebuilt and listeners are notified once.

AddListener method

procedure AddListener(const Listener: TNotifyEvent);

Registers a method that is called, with the table as Sender, after every change and before OnChange. Adding the same method of the same object again has no effect; nil is ignored. Remove the listener before its object is freed. TAtlasGrid follows its table this way.

pascal
Table.AddListener(Watcher.TableChanged);

RemoveListener method

procedure RemoveListener(const Listener: TNotifyEvent);

Removes a listener registered with AddListener. A listener may remove itself while it is being called.

Examples

Define a schema, load rows, filter and sort

A read-only key, text, number, boolean and date columns. Prints INV-001 Harbor Books 125.5, INV-003 Harbor Books 42 and 2 of 3 rows shown.

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

Follow changes with a listener

Any number of objects can listen to one table. The watcher recomputes a total after every change: 42.50, then 30.00 once the filter hides two rows.

pascal
uses System.SysUtils, System.Classes, Atlas.Data;

type
  TTotalWatcher = class
    Total: Double;
    procedure TableChanged(Sender: TObject);
  end;

procedure TTotalWatcher.TableChanged(Sender: TObject);
var
  Table: TAtlasTable;
  I: Integer;
begin
  Table := Sender as TAtlasTable;
  Total := 0;
  for I := 0 to Table.RowCount - 1 do
    if Table.Cell(1, I) <> '' then
      Total := Total + StrToFloat(Table.Cell(1, I), TFormatSettings.Invariant);
end;

procedure WatchTotal;
var
  Table: TAtlasTable;
  Watcher: TTotalWatcher;
begin
  Table := TAtlasTable.Create(nil);
  Watcher := TTotalWatcher.Create;
  try
    Table.AddColumn('Item');
    Table.AddColumn('Amount', ackNumber);
    Table.AddListener(Watcher.TableChanged);
    Table.AddRow(['Paper', '12.5']);
    Table.AddRow(['Ink', '30']);
    Table.AddRow(['Pens', '']);
    Writeln(Watcher.Total:0:2); // 42.50
    Table.SetFilter('ink', 0);
    Writeln(Watcher.Total:0:2); // 30.00: the watcher sums the visible rows
    Table.RemoveListener(Watcher.TableChanged);
  finally
    Watcher.Free;
    Table.Free;
  end;
end;

Save and reload CSV

Export every row, then import it back; a file with an invalid value is rejected as a whole and the table keeps its rows.

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

TAtlasColumn · TAtlasColumnKind · TAtlasRow · EAtlasDataError · TAtlasGrid · TAtlasValueEdit · TAtlasPivot · TAtlasPrintDocument

Also in Atlas VCL

TAtlasGrid · TAtlasValueEdit · TAtlasTree · TAtlasWorkbook · TAtlasSheetGrid · TAtlasPivot · TAtlasInspector · TAtlasRichEdit · TAtlasSpellChecker · TAtlasChart · TAtlasGauge · TAtlasThemer · TAtlasWizard · TAtlasWorkspace · TAtlasCommandBar · TAtlasActionTile · TAtlasTileBoard · TAtlasNavigation · TAtlasMap · TAtlasGraph · TAtlasDiagram · TAtlasPrintDocument · TAtlasSchedule · TAtlasScheduler · TAtlasTaskPlan · TAtlasGantt · TAtlasPdfDocument · TAtlasPdfViewer