YOUSEFUL / DELPHI / SAMPLE PROJECTS

Tree and editors

Console program Self-checking Product: Atlas VCL · Components: TAtlasTree, TAtlasSheetGrid, TAtlasWorkbook, TAtlasInspector, TAtlasRichEdit, TAtlasValueEdit

A console program that creates the Atlas VCL data controls on a form that is never shown and verifies what each one does:

Controls that wrap a Windows control need a window handle, so each one is given the hidden form as Parent before it is used. A mismatch raises an exception, the program prints FAIL: ... and exits with code 1.

Build and run

Open TreeAndEditors.dpr in Delphi 13; the IDE creates the project file. Add the Atlas VCL src folder to the project's search path, then build and run for Win32 or Win64. No packages need to be installed. The program needs no input. It checks its own results and stops with an error if any of them differs from the output below.

Expected output

Example
TAtlasTree
  ok: children sorted by caption = Data platform
  ok: case-insensitive caption search
  ok: node moved to the root level
  ok: moving a node under its own child is rejected
  ok: a lazy node shows an expand button before loading
  ok: OnLoadChildren ran once on first expand
  ok: children are not loaded twice
  ok: JSON round trip keeps captions, expansion and lazy flags = [{"caption":"Engineering",...}]
  ok: a rejected import leaves the tree unchanged
TAtlasSheetGrid
  ok: column headers use letters = A
  ok: B1 shows the calculated value = 42
  ok: the grid follows workbook changes = 20
  ok: cell formats apply to the displayed text = 20.0
  ok: the grid detaches when its workbook is freed
TAtlasInspector
  ok: published properties are listed with their values = 3
  ok: SetProperty converts and assigns typed values
  ok: an invalid value is rejected before assignment
  ok: a read-only property is rejected
  ok: destroying the component detaches the inspector
TAtlasRichEdit
  ok: selection formatting applied
  ok: SaveRTF returns RTF bytes
  ok: LoadRTF restores the text = Atlas notes: draft one, draft two.
  ok: LoadRTF restores the formatting
  ok: FindNext selects the first match after the caret
  ok: FindNext continues from the selection
  ok: ReplaceAll reports two replacements
  ok: text after ReplaceAll = Atlas notes: revision one, revision two.
TAtlasValueEdit
  ok: a number is accepted
  ok: NormalizedValue is the stored form = 12.5
  ok: a locale decimal comma is rejected with a message
  ok: blank is valid when not required
  ok: blank is invalid when required
  ok: 2026-02-29 is not a real date
  ok: 2028-02-29 is a leap day
PASS: TreeAndEditors

The tree JSON line shows the format ToJSON writes: an array of node objects, each with caption, expanded, lazy and children. The Archive node was expanded, so it is saved as expanded with its loaded child and lazy false; the never-expanded Later node keeps lazy true.

What to change first

Project files

Download all 34 projects

The ZIP contains every sample project with its README. See getting started for the search path to add.

Source

TreeAndEditors.dpr

pascal
program TreeAndEditors;

// Atlas VCL controls exercised on a hidden form: TAtlasTree hierarchy, lazy
// loading and JSON; TAtlasSheetGrid over a TAtlasWorkbook; TAtlasInspector
// editing published properties; TAtlasRichEdit formatting, RTF, find and
// replace; TAtlasValueEdit validation. Every result is verified and the
// program exits with 0 on success. Nothing is shown on screen.

{$APPTYPE CONSOLE}

uses
  System.SysUtils, System.Classes, Vcl.Forms, Vcl.Controls, Vcl.ComCtrls,
  Vcl.Graphics, Atlas.Data, Atlas.Editors, Atlas.Tree, Atlas.Spreadsheet,
  Atlas.SheetGrid, Atlas.Inspector, Atlas.RichEdit;

type
  TLazyLoader = class
    Calls: Integer;
    procedure LoadChildren(Sender: TObject; Node: TTreeNode);
  end;

  TPriority = (prLow, prNormal, prHigh);
  TTaskSettings = class(TComponent)
  private
    FTitle: string;
    FRetries: Integer;
    FRatio: Double;
    FPriority: TPriority;
    FEnabled: Boolean;
  published
    property Title: string read FTitle write FTitle;
    property Retries: Integer read FRetries write FRetries;
    property Ratio: Double read FRatio write FRatio;
    property Priority: TPriority read FPriority write FPriority;
    property Enabled: Boolean read FEnabled write FEnabled;
    property Version: Integer read FRetries;
  end;

procedure TLazyLoader.LoadChildren(Sender: TObject; Node: TTreeNode);
begin
  Inc(Calls);
  TAtlasTree(Sender).AddNode(Node, 'Loaded on expand');
end;

procedure Expect(Condition: Boolean; const What: string);
begin
  if not Condition then
    raise Exception.Create('Check failed: ' + What);
  Writeln('  ok: ', What);
end;

procedure ExpectText(const Expected, Actual, What: string);
begin
  if Expected <> Actual then
    raise Exception.CreateFmt('%s: expected [%s], got [%s]', [What, Expected, Actual]);
  Writeln('  ok: ', What, ' = ', Actual);
end;

procedure TreeSection(Form: TForm);
var
  Tree, Copy: TAtlasTree;
  Loader: TLazyLoader;
  Root, Lazy, Node: TTreeNode;
  Failed: Boolean;
begin
  Writeln('TAtlasTree');
  Loader := TLazyLoader.Create;
  try
    Tree := TAtlasTree.Create(Form);
    Tree.Parent := Form; // nodes need a window handle
    Copy := TAtlasTree.Create(Form);
    Copy.Parent := Form;
    Root := Tree.AddNode(nil, 'Engineering');
    Tree.AddNode(Root, 'Quality');
    Tree.AddNode(Root, 'Desktop');
    Node := Tree.AddNode(Root, 'Data platform');
    Tree.SortChildren(Root);
    ExpectText('Data platform', Root.Item[0].Text, 'children sorted by caption');
    Expect(Tree.FindCaption('desktop') = Root.Item[1], 'case-insensitive caption search');
    Tree.MoveNode(Node, nil);
    Expect((Node.Parent = nil) and (Root.Count = 2), 'node moved to the root level');
    Failed := False;
    try
      Tree.MoveNode(Root, Root.Item[0]);
    except
      on E: EAtlasTreeError do
        Failed := True;
    end;
    Expect(Failed, 'moving a node under its own child is rejected');
    Tree.OnLoadChildren := Loader.LoadChildren;
    Lazy := Tree.AddNode(nil, 'Archive', True);
    Expect(Lazy.HasChildren and (Lazy.Count = 0), 'a lazy node shows an expand button before loading');
    Lazy.Expand(False);
    Expect((Loader.Calls = 1) and (Lazy.Count = 1), 'OnLoadChildren ran once on first expand');
    Lazy.Collapse(False);
    Lazy.Expand(False);
    Expect(Loader.Calls = 1, 'children are not loaded twice');
    Tree.AddNode(nil, 'Later', True);
    Copy.LoadJSON(Tree.ToJSON);
    ExpectText(Tree.ToJSON, Copy.ToJSON, 'JSON round trip keeps captions, expansion and lazy flags');
    Failed := False;
    try
      Copy.LoadJSON('[{"caption":"no other fields"}]');
    except
      on E: EAtlasTreeError do
        Failed := True;
    end;
    Expect(Failed and (Copy.ToJSON = Tree.ToJSON), 'a rejected import leaves the tree unchanged');
  finally
    Loader.Free;
  end;
end;

procedure SheetSection(Form: TForm);
var
  Workbook: TAtlasWorkbook;
  Sheet: TAtlasSheetGrid;
  Format: TAtlasCellFormat;
begin
  Writeln('TAtlasSheetGrid');
  Workbook := TAtlasWorkbook.Create(Form);
  Workbook.SetCell('A1', '12');
  Workbook.SetCell('A2', '30');
  Workbook.SetCell('B1', '=SUM(A1:A2)');
  Sheet := TAtlasSheetGrid.Create(Form);
  Sheet.Parent := Form;
  Sheet.Workbook := Workbook;
  ExpectText('A', Sheet.Cells[1, 0], 'column headers use letters');
  ExpectText('42', Sheet.Cells[2, 1], 'B1 shows the calculated value');
  Workbook.SetCell('A2', '8');
  ExpectText('20', Sheet.Cells[2, 1], 'the grid follows workbook changes');
  Format := TAtlasCellFormat.Default;
  Format.NumberFormat := anfFixed;
  Format.Decimals := 1;
  Workbook.SetCellFormat('B1', Format);
  ExpectText('20.0', Sheet.Cells[2, 1], 'cell formats apply to the displayed text');
  Workbook.Free;
  Expect(Sheet.Workbook = nil, 'the grid detaches when its workbook is freed');
end;

procedure InspectorSection(Form: TForm);
var
  Settings: TTaskSettings;
  Inspector: TAtlasInspector;
  Failed: Boolean;
begin
  Writeln('TAtlasInspector');
  Settings := TTaskSettings.Create(nil);
  try
    Settings.Title := 'Nightly build';
    Settings.Retries := 3;
    Inspector := TAtlasInspector.Create(Form);
    Inspector.Parent := Form;
    Inspector.Instance := Settings;
    ExpectText('3', Inspector.Strings.Values['Retries'], 'published properties are listed with their values');
    Inspector.SetProperty('Retries', '5');
    Inspector.SetProperty('Ratio', '0.75');
    Inspector.SetProperty('Priority', 'prHigh');
    Inspector.SetProperty('Enabled', 'True');
    Expect((Settings.Retries = 5) and (Settings.Ratio = 0.75) and
      (Settings.Priority = prHigh) and Settings.Enabled, 'SetProperty converts and assigns typed values');
    Failed := False;
    try
      Inspector.SetProperty('Retries', 'five');
    except
      on E: EAtlasPropertyError do
        Failed := True;
    end;
    Expect(Failed and (Settings.Retries = 5), 'an invalid value is rejected before assignment');
    Failed := False;
    try
      Inspector.SetProperty('Version', '9');
    except
      on E: EAtlasPropertyError do
        Failed := True;
    end;
    Expect(Failed, 'a read-only property is rejected');
    Settings.Free;
    Settings := nil;
    Expect(Inspector.Instance = nil, 'destroying the component detaches the inspector');
  finally
    Settings.Free;
  end;
end;

procedure RichEditSection(Form: TForm);
var
  Editor, Copy: TAtlasRichEdit;
  Rtf: TBytes;
begin
  Writeln('TAtlasRichEdit');
  Editor := TAtlasRichEdit.Create(Form);
  Editor.Parent := Form; // the control needs a window before streaming RTF
  Copy := TAtlasRichEdit.Create(Form);
  Copy.Parent := Form;
  Editor.Text := 'Atlas notes: draft one, draft two.';
  Editor.SelStart := 0;
  Editor.SelLength := 11;
  Editor.FormatSelection('Segoe UI', 14, [fsBold], clNavy);
  Expect(fsBold in Editor.SelAttributes.Style, 'selection formatting applied');
  Rtf := Editor.SaveRTF;
  Expect(TEncoding.ASCII.GetString(Rtf, 0, 5) = '{\rtf', 'SaveRTF returns RTF bytes');
  Copy.LoadRTF(Rtf);
  ExpectText(Editor.Text, Copy.Text, 'LoadRTF restores the text');
  Copy.SelStart := 0;
  Copy.SelLength := 11;
  Expect(fsBold in Copy.SelAttributes.Style, 'LoadRTF restores the formatting');
  Editor.SelStart := 0;
  Editor.SelLength := 0;
  Expect(Editor.FindNext('draft') and (Editor.SelStart = 13), 'FindNext selects the first match after the caret');
  Expect(Editor.FindNext('draft') and (Editor.SelStart = 24), 'FindNext continues from the selection');
  Expect(Editor.ReplaceAll('draft', 'revision') = 2, 'ReplaceAll reports two replacements');
  ExpectText('Atlas notes: revision one, revision two.', Editor.Text, 'text after ReplaceAll');
end;

procedure ValueEditSection;
var
  Edit: TAtlasValueEdit;
begin
  Writeln('TAtlasValueEdit');
  Edit := TAtlasValueEdit.Create(nil);
  try
    Edit.ValueKind := ackNumber;
    Edit.Text := '12.50';
    Expect(Edit.Validate, 'a number is accepted');
    ExpectText('12.5', Edit.NormalizedValue, 'NormalizedValue is the stored form');
    Edit.Text := '1,5';
    Expect(not Edit.Validate and (Edit.ValidationMessage <> ''), 'a locale decimal comma is rejected with a message');
    Edit.Text := '';
    Expect(Edit.Validate, 'blank is valid when not required');
    Edit.Required := True;
    Expect(not Edit.Validate, 'blank is invalid when required');
    Edit.ValueKind := ackDate;
    Edit.Text := '2026-02-29';
    Expect(not Edit.Validate, '2026-02-29 is not a real date');
    Edit.Text := '2028-02-29';
    Expect(Edit.Validate, '2028-02-29 is a leap day');
  finally
    Edit.Free;
  end;
end;

var
  Form: TForm;
begin
  try
    Application.Initialize;
    Form := TForm.CreateNew(nil);
    try
      TreeSection(Form);
      SheetSection(Form);
      InspectorSection(Form);
      RichEditSection(Form);
      ValueEditSection;
    finally
      Form.Free;
    end;
    Writeln('PASS: TreeAndEditors');
  except
    on E: Exception do
    begin
      Writeln('FAIL: ', E.ClassName, ': ', E.Message);
      Halt(1);
    end;
  end;
end.