YOUSEFUL / DELPHI
Atlas UI: FAQ, examples and tutorial
Learning center · Component reference
Atlas UI adds four components to the Atlas UI palette page, for three jobs that Atlas VCL does not cover. The
Atlas.Kanban unit holds a Kanban board in two parts: TAtlasKanbanModel is a non-visual model of columns and
cards, with work-in-progress limits, moves your code can veto and a validated JSON format, and TAtlasKanban is
the control that draws a model and lets the user select and move cards. Atlas.Syntax holds TAtlasSyntaxMemo,
a Rich Edit that colours Object Pascal source as you type and keeps its own text-only undo history, together with
the lexer behind it, AtlasPascalTokens. Atlas.Updater holds TAtlasWebUpdater, which reads an update
manifest over HTTPS, compares version numbers, and downloads, verifies and stores the update file without ever
running it. Atlas UI requires Atlas VCL: its runtime package needs AtlasVclRuntime, so add both src folders
to the search path (Getting started shows how). For grids, scheduling, rich text, a
command bar, trees, PDF viewing and spell checking, use the Atlas VCL components (TAtlasGrid, TAtlasSchedule,
TAtlasRichEdit, TAtlasCommandBar, TAtlasTree, TAtlasPdfViewer, TAtlasSpellChecker); Atlas UI does not
add its own versions of them.
FAQ
What does Atlas UI contain, and what does it not? The four components above and the types they use (the card record, the token record and kinds, the update record, two exception classes). It is not a large collection of visual controls, it has no FireMonkey or web versions, and it is not compatible with the form files or programming interfaces of other component suites. The design package registers the four components on the Atlas UI palette page, but installing it into the IDE has not been tested yet; compiling the units into your program, as every sample here does, is the tested route.
How do the Kanban model and the board fit together? The model holds all data; the board only draws it and
keeps the selected card and its scroll position. Assign the model to the board's Model property. Every change
to the model (AddColumn, AddCard, RemoveCard, MoveCard, LoadJSON) repaints every board showing it and
then fires OnChange, so several boards can show one model. The model needs no window, so board rules can be
tested in a console program (Kanban model in a console).
Freeing the model detaches its boards safely.
What does a work-in-progress limit do? AddColumn('active', 'In progress', 3) lets the column hold at most
three cards; 0, the default, means no limit. Adding a card to a full column, or moving one into it from
another column, raises EAtlasKanban with Column work-in-progress limit reached; reordering inside the column
is always allowed. LoadJSON applies the same limits, and the board shows them in the header (In progress 1/3).
How do I stop a move, and what happens when a move fails? Assign BeforeMove and set Allow to False;
MoveCard then returns False and nothing changes. The handler receives the card id and the source and target
column ids and runs after the built-in checks, so it only needs your own rules. It must not change the model:
any change inside it raises EAtlasKanban. Invalid moves (an unknown id, a full column, an index out of range)
raise EAtlasKanban from MoveCard. Moves made on the board never raise: MoveSelectedTo, which the mouse and
the keyboard use, returns False, keeps the reason in LastError and paints it in red at the bottom of the
board.
How do I save a board, and how do I change what the model has no method for? ToJSON returns the whole board
as one line of JSON and LoadJSON replaces the board with a saved one. Loading is atomic: the document is built
and validated in a separate model first, so a bad document raises an exception and leaves the current board as
it was. Every field is required, and a missing one raises the RTL's EJSONException rather than EAtlasKanban.
The model has no methods to remove, rename or reorder columns or to edit a card. Change a card by removing it,
adding it again and moving it back into place with MoveCard and an index; change columns by editing a board
document and loading it. {"schema":1,"columns":[]} empties the board.
Which mouse and keyboard actions does the board support? A click selects a card. Dragging a card onto another
column moves it there, always to the end of that column; there is no drop position or drag image, so use
MoveCard with an index to place a card. The mouse wheel scrolls the columns together and Shift+wheel scrolls
sideways; there are no scroll bars. The control also has arrow-key selection and Ctrl+Left and Ctrl+Right moves,
but on a form the arrow keys never reach it unless you use a small subclass that asks for them (shown under
Examples and in the tutorial). The selection is not scrolled into view.
Can I change how the board looks? Only the font. Columns are 270 pixels wide and card slots 110 pixels high,
scaled with the screen's DPI; each card shows its title in bold and its detail text below. The colours are fixed,
VCL styles are not applied, and there are no properties or events for custom drawing, card templates, avatars,
tags or due dates. Use the card's Detail text for extra information.
What does the syntax editor colour, and what is it not? Object Pascal keywords (in bold), strings, numbers
(decimal, real and $ hexadecimal) and the three comment forms, recomputed for the whole text after every change.
Directives such as override, type names, #13 character codes and multi-line string literals are not
recognized, and compiler directives are coloured as comments. It is a lexer, not a parser or a language service:
there is no code completion, error marking, folding, line numbers, bracket matching or support for other
languages. The other TRichEdit properties and events are there as usual, but any formatting you apply yourself is
replaced at the next change. Colouring needs a window, so give the editor its Parent before its Text, or call
Highlight afterwards.
How does undo work in the syntax editor? The editor keeps a copy of the whole text after every change, up to
128 copies, and UndoText and RedoText step through them. Ctrl+Z, Ctrl+Y, Ctrl+Shift+Z and the inherited Undo
all use this history, so recolouring never becomes an undo step. Undo restores the text only; the caret moves to
the end. The history starts with the empty text of a new editor and cannot be cleared, so undoing past a loaded
file empties the editor. Because every change stores a full copy and recolours everything, the editor suits small
and moderate files; it has not been measured with large ones.
What does TAtlasWebUpdater check, and what is left to my application? It accepts only schema 1 manifests
with a numeric three- or four-part version, an HTTPS url without credentials, the exact byte size and the
sha256 digest. It refuses redirects and any HTTP status other than 200, lets Windows check the server
certificate, downloads the file, checks its size and digest in memory, and only then writes it to the cache
folder you name, through a temporary .part file that is renamed when complete, so no partial or unverified file
ever carries the final name. It never runs, opens or
installs anything, and it does not check publisher signatures: the digest proves the file matches the manifest,
not who wrote the manifest. Installation, rollback and cleaning up old cache files are up to your application.
Why should the updater run on a worker thread? FetchManifest and StageUpdate block until the transfer ends
or fails, with a 10-second connection timeout and a 15-second response timeout, and there is no progress event
or cancellation. Call them from a TTask or TThread and report back with TThread.Queue; the component
reference has an example. The whole download is held in memory. A Win32 program stops a response that grows past
the manifest's size; a Win64 program receives all of it first and only then refuses it, so a misbehaving
server can make it use more memory than the manifest declares.
How can I test update code without an HTTPS server? ParseManifest checks manifest text with no network at
all, and CompareVersions needs nothing but two strings; the Manifest check
sample exercises both. For a download test, run a local HTTP server bound to 127.0.0.1 and set
AllowLoopbackHTTP; plain HTTP is then accepted for exactly that address, not for localhost or any other host.
Examples
Build a board in code without any window. MoveCard with an index reorders cards inside a column.
uses System.SysUtils, Atlas.Kanban;
procedure BuildBoard;
var
Model: TAtlasKanbanModel;
C, I: Integer;
Line: string;
begin
Model := TAtlasKanbanModel.Create(nil);
try
Model.AddColumn('todo', 'To do');
Model.AddColumn('active', 'In progress', 2);
Model.AddColumn('done', 'Done');
Model.AddCard('todo', 'order-42', 'Prepare customer order', 'Pick, pack and label');
Model.AddCard('todo', 'invoice-7', 'Send invoice 7');
Model.AddCard('todo', 'audit', 'Stock audit');
Model.MoveCard('order-42', 'active');
Model.MoveCard('audit', 'active');
Model.MoveCard('audit', 'active', 0); // reorder: audit goes first
for C := 0 to Model.ColumnCount - 1 do
begin
Line := Model.ColumnTitle(C) + ':';
for I := 0 to Model.CardCount(C) - 1 do
Line := Line + ' ' + Model.Card(C, I).ID;
Writeln(Line);
end;
finally
Model.Free;
end;
end;It prints To do: invoice-7, In progress: audit order-42 and Done:.
Enforce a workflow rule with BeforeMove, and see how a veto differs from a refused move: a veto makes
MoveCard return False, a full column raises EAtlasKanban.
uses System.SysUtils, Atlas.Kanban;
type
TDoneRule = class
procedure BeforeMove(Sender: TObject; const CardID, FromColumn, ToColumn: string; var Allow: Boolean);
end;
// A card may enter "done" only from "review".
procedure TDoneRule.BeforeMove(Sender: TObject; const CardID, FromColumn, ToColumn: string; var Allow: Boolean);
begin
Allow := (ToColumn <> 'done') or (FromColumn = 'review');
end;
procedure ShowMoveRules;
var
Model: TAtlasKanbanModel;
Rule: TDoneRule;
procedure Attempt(const CardID, ColumnID: string);
var
Outcome: string;
begin
try
Outcome := BoolToStr(Model.MoveCard(CardID, ColumnID), True);
except
on E: EAtlasKanban do
Outcome := E.Message;
end;
Writeln(CardID, ' -> ', ColumnID, ': ', Outcome);
end;
begin
Model := TAtlasKanbanModel.Create(nil);
Rule := TDoneRule.Create;
try
Model.AddColumn('todo', 'To do');
Model.AddColumn('review', 'Review', 1);
Model.AddColumn('done', 'Done');
Model.AddCard('todo', 'a', 'First');
Model.AddCard('todo', 'b', 'Second');
Model.BeforeMove := Rule.BeforeMove;
Attempt('a', 'done'); // vetoed by the rule
Attempt('a', 'review');
Attempt('b', 'review'); // Review holds one card at most
Attempt('a', 'done'); // allowed from Review
finally
Model.Free;
Rule.Free;
end;
end;It prints a -> done: False, a -> review: True, b -> review: Column work-in-progress limit reached and
a -> done: True.
Save a board as JSON, load it into another model, and see that a bad document leaves the board untouched.
uses System.SysUtils, Atlas.Kanban;
procedure RoundTrip;
var
Original, Restored: TAtlasKanbanModel;
Saved: string;
begin
Original := TAtlasKanbanModel.Create(nil);
Restored := TAtlasKanbanModel.Create(nil);
try
Original.AddColumn('todo', 'To do', 5);
Original.AddCard('todo', 'order-42', 'Prepare customer order', 'Pick, pack and label');
Saved := Original.ToJSON;
Writeln(Saved);
Restored.LoadJSON(Saved);
Writeln('Same board: ', Restored.ToJSON = Saved);
try
Restored.LoadJSON('{"schema":1,"columns":[{"id":"x","title":"X","limit":0,"cards":[]},' +
'{"id":"x","title":"Again","limit":0,"cards":[]}]}');
except
on E: EAtlasKanban do
Writeln('Refused: ', E.Message, '; still ', Restored.ColumnCount, ' column(s)');
end;
finally
Restored.Free;
Original.Free;
end;
end;It prints
{"schema":1,"columns":[{"id":"todo","title":"To do","limit":5,"cards":[{"id":"order-42","title":"Prepare customer order","detail":"Pick, pack and label"}]}]},
then Same board: TRUE and Refused: Invalid or duplicate column; still 1 column(s).
Use the syntax lexer on its own, here to list the comments in some source. Start is 0-based.
uses System.SysUtils, Atlas.Syntax;
procedure ListComments(const Source: string);
var
Token: TAtlasToken;
begin
for Token in AtlasPascalTokens(Source) do
if Token.Kind = atkComment then
Writeln(Token.Start, ': ', Copy(Source, Token.Start + 1, Token.Count));
end;ListComments('X := 1; // one' + #10 + '{ two } Y := ''// not a comment''; (* three *)') prints 8: // one,
15: { two } and 48: (* three *); the text inside the string is not a comment.
Read an update manifest and compare its version with the running one, without any network access.
uses System.SysUtils, Atlas.Updater;
procedure CheckManifest;
const
Manifest = '{"schema":1,"version":"2.1.0","url":"https://updates.example.com/myapp-2.1.0.bin",' +
'"size":1048576,"sha256":"9F86D081884C7D659A2FEAA0C55AD015A3BF4F1B2B0B822CD15D6C15B0F00A08"}';
var
Updater: TAtlasWebUpdater;
Info: TAtlasUpdateInfo;
begin
Updater := TAtlasWebUpdater.Create(nil);
try
Info := Updater.ParseManifest(Manifest);
Writeln(Info.Version, ', ', Info.Size, ' bytes, sha256 ', Info.SHA256);
Writeln('Newer than 2.0.9: ', TAtlasWebUpdater.CompareVersions(Info.Version, '2.0.9') > 0);
Writeln('Newer than 2.1.0.0: ', TAtlasWebUpdater.CompareVersions(Info.Version, '2.1.0.0') > 0);
finally
Updater.Free;
end;
end;It prints 2.1.0, 1048576 bytes, sha256 9f86d081884c7d659a2feaa0c55ad015a3bf4f1b2b0b822cd15d6c15b0f00a08 (the
digest in lower case), Newer than 2.0.9: TRUE and Newer than 2.1.0.0: FALSE.
Give the board the arrow keys. Create TKeyboardKanban wherever you would create TAtlasKanban.
uses Winapi.Windows, Winapi.Messages, Atlas.Kanban;
type
TKeyboardKanban = class(TAtlasKanban)
protected
procedure WMGetDlgCode(var Message: TWMGetDlgCode); message WM_GETDLGCODE;
end;
procedure TKeyboardKanban.WMGetDlgCode(var Message: TWMGetDlgCode);
begin
inherited;
Message.Result := Message.Result or DLGC_WANTARROWS;
end;With it, Up and Down select the previous or next card, Left and Right select a card in the neighbouring column, and Ctrl+Left and Ctrl+Right move the selected card to the end of the neighbouring column, even when buttons or other controls share the form. Without it, the form uses those keys to move the focus.
Tutorial
Sample project: Kanban board.
Goal: build a window with a Kanban board whose cards move from code, from buttons, with the mouse and with the keyboard, under a rule that keeps cards from skipping review.
Create the project. In Delphi choose File > New > VCL Forms Application, then Project > Remove from Project to remove
Unit1: this form is built in code. Save the project asKanbanBoard. Open Project > Options > Building > Delphi Compiler > Search path and add the Atlas UIsrcfolder and the Atlas VCLsrcfolder. Open the program source with Project > View Source; the steps below replace everything in it except the{$R *.res}line the IDE wrote, which you keep under the first line.Start the program with the units it needs.
Atlas.Kanbanholds both the model and the board.program KanbanBoard; uses System.SysUtils, System.Classes, Winapi.Windows, Winapi.Messages, Vcl.Forms, Vcl.Controls, Vcl.StdCtrls, Vcl.ExtCtrls, Atlas.Kanban;Declare two classes.
TKeyboardKanbanis the board with one addition: it asks Windows for the arrow keys, whichTAtlasKanbandoes not, so the form stops using them to move the focus.TBoardFormis the window.type TKeyboardKanban = class(TAtlasKanban) protected procedure WMGetDlgCode(var Message: TWMGetDlgCode); message WM_GETDLGCODE; end; TBoardForm = class(TForm) private FModel: TAtlasKanbanModel; FBoard: TAtlasKanban; FStatus: TLabel; FJson: TMemo; procedure BeforeMove(Sender: TObject; const CardID, FromColumn, ToColumn: string; var Allow: Boolean); procedure ModelChanged(Sender: TObject); procedure MoveLeftClick(Sender: TObject); procedure MoveRightClick(Sender: TObject); procedure ShowJsonClick(Sender: TObject); function ColumnOfCard(const CardID: string): Integer; procedure MoveSelected(Delta: Integer); public constructor Create(AOwner: TComponent); override; end; procedure TKeyboardKanban.WMGetDlgCode(var Message: TWMGetDlgCode); begin inherited; Message.Result := Message.Result or DLGC_WANTARROWS; end;Start the constructor and fill the model. It is created first because everything else refers to it. The second column has a work-in-progress limit of 2.
constructor TBoardForm.Create(AOwner: TComponent); var Bar: TPanel; Button: TButton; begin inherited CreateNew(AOwner); Caption := 'Atlas UI: Kanban board'; Width := 1160; Height := 700; Position := poScreenCenter; Font.Name := 'Segoe UI'; Font.Size := 10; FModel := TAtlasKanbanModel.Create(Self); FModel.AddColumn('todo', 'To do'); FModel.AddColumn('active', 'In progress', 2); FModel.AddColumn('review', 'Review'); FModel.AddColumn('done', 'Done'); FModel.AddCard('todo', 'order-42', 'Prepare customer order', 'Pick, pack and label the parcel'); FModel.AddCard('todo', 'invoice-7', 'Send invoice 7', 'Net 30, attach the delivery note'); FModel.AddCard('todo', 'audit', 'Stock audit', 'Count aisle 3 before Friday'); FModel.AddCard('review', 'catalog', 'Update the catalog', 'New prices for autumn'); FModel.AddCard('done', 'backup', 'Nightly backup', 'Verified restore on the test server');Add a toolbar with three buttons, a status line and a memo for the JSON export, still inside the constructor.
Bar := TPanel.Create(Self); Bar.Parent := Self; Bar.Align := alTop; Bar.Height := 44; Bar.BevelOuter := bvNone; Button := TButton.Create(Self); Button.Parent := Bar; Button.SetBounds(8, 8, 150, 28); Button.Caption := 'Move left (Ctrl+Left)'; Button.OnClick := MoveLeftClick; Button := TButton.Create(Self); Button.Parent := Bar; Button.SetBounds(166, 8, 160, 28); Button.Caption := 'Move right (Ctrl+Right)'; Button.OnClick := MoveRightClick; Button := TButton.Create(Self); Button.Parent := Bar; Button.SetBounds(334, 8, 120, 28); Button.Caption := 'Show JSON'; Button.OnClick := ShowJsonClick; FJson := TMemo.Create(Self); FJson.Parent := Self; FJson.Align := alBottom; FJson.Height := 110; FJson.ReadOnly := True; FJson.ScrollBars := ssVertical; FJson.Font.Name := 'Consolas'; FStatus := TLabel.Create(Self); FStatus.Parent := Self; FStatus.Align := alBottom; FStatus.AlignWithMargins := True;Create the board, give it the model, attach the rule and the change handler, and move one card in code. The constructor ends here.
FBoard := TKeyboardKanban.Create(Self); FBoard.Parent := Self; FBoard.Align := alClient; FBoard.Model := FModel; FModel.BeforeMove := BeforeMove; FModel.OnChange := ModelChanged; FModel.MoveCard('order-42', 'active'); FBoard.SelectCard('order-42'); ActiveControl := FBoard; ModelChanged(FModel); end;Write the rule and the status line.
BeforeMoveonly decides; it must not change the model.ModelChangedruns after every change and counts the cards per column.procedure TBoardForm.BeforeMove(Sender: TObject; const CardID, FromColumn, ToColumn: string; var Allow: Boolean); begin Allow := (ToColumn <> 'done') or (FromColumn = 'review'); end; procedure TBoardForm.ModelChanged(Sender: TObject); var Text: string; C: Integer; begin Text := ''; for C := 0 to FModel.ColumnCount - 1 do Text := Text + Format('%s: %d ', [FModel.ColumnTitle(C), FModel.CardCount(C)]); if FBoard.LastError <> '' then Text := Text + ' last error: ' + FBoard.LastError; FStatus.Caption := Text; end;Write the button handlers. The model addresses columns by position, so
ColumnOfCardfinds the selected card's column;MoveSelectedTodoes the move and reports a refusal inLastErrorinstead of raising.function TBoardForm.ColumnOfCard(const CardID: string): Integer; var C, I: Integer; begin for C := 0 to FModel.ColumnCount - 1 do for I := 0 to FModel.CardCount(C) - 1 do if FModel.Card(C, I).ID = CardID then Exit(C); Result := -1; end; procedure TBoardForm.MoveSelected(Delta: Integer); var Target: Integer; begin Target := ColumnOfCard(FBoard.SelectedCard); if Target < 0 then begin FStatus.Caption := 'Click a card first.'; Exit; end; Target := Target + Delta; if (Target < 0) or (Target >= FModel.ColumnCount) then Exit; if not FBoard.MoveSelectedTo(FModel.ColumnID(Target)) then FStatus.Caption := 'Not moved: ' + FBoard.LastError; FBoard.SetFocus; end; procedure TBoardForm.MoveLeftClick(Sender: TObject); begin MoveSelected(-1); end; procedure TBoardForm.MoveRightClick(Sender: TObject); begin MoveSelected(1); end; procedure TBoardForm.ShowJsonClick(Sender: TObject); begin FJson.Text := FModel.ToJSON; end;End the program with the usual main block.
var Form: TBoardForm; begin Application.Initialize; Application.MainFormOnTaskbar := True; Application.CreateForm(TBoardForm, Form); Application.Run; end.Run it (F9). The window shows four columns. The headers read
To do 2,In progress 1/2,Review 1andDone 1; Prepare customer order is in In progress with a blue outline, because the constructor moved and selected it; the status line readsTo do: 2 In progress: 1 Review: 1 Done: 1.Try the moves. Press Left: Send invoice 7 is selected. Press Ctrl+Right: it moves to the end of In progress, whose header now reads
2/2. Press Left (Stock audit is selected) and Ctrl+Right: nothing moves and the board showsColumn work-in-progress limit reachedin red at its bottom left; Move right gives the same refusal and writesNot moved: Column work-in-progress limit reachedin the status line. Drag Update the catalog onto Done: it moves, because it comes from Review. Drag Prepare customer order onto Done: the rule declines it and the board showsMove declined. Show JSON puts the whole board, as one line of JSON, in the memo.
To adapt it, save FModel.ToJSON to a file in the form's OnClose and call FModel.LoadJSON with it in the
constructor instead of the AddColumn and AddCard calls, so the board survives a restart. The sample project
is the same program with one addition: a -image switch that saves the window as a PNG file instead of opening it.
Troubleshooting
The arrow keys move the focus to another control instead of selecting cards. TAtlasKanban does not answer
WM_GETDLGCODE, so the form treats the arrow keys as dialog keys. Use the TKeyboardKanban subclass from
Examples; nothing else is needed.
The syntax editor shows the text in black. The text was assigned before the editor had a Parent, and
colouring needs a window. Assign Parent first, or call Highlight once after assigning it.
EAtlasKanban: Cannot mutate the board during a move callback. Your BeforeMove handler, or something it
calls, changed the model. The handler may only read the model and set Allow. Make follow-up changes after
MoveCard returns True, or queue them with TThread.ForceQueue.
LoadJSON raises EJSONException with Value 'detail' not found (or 'limit', 'cards', ...). The document
was written by hand or by another program and leaves out a field. LoadJSON needs every field that ToJSON
writes, including an empty detail and a limit of 0. Catch EJSONException as well as EAtlasKanban when
you load files; the board is unchanged either way.
A card dropped with the mouse lands at the bottom of the column. The board has no drop position: a drop
appends to the end of the target column, and a drop on the card's own column moves it to the end. Use
MoveCard(ID, ColumnID, Index) from code to put a card at a position.
Updates require HTTPS in a test against a local server. Plain HTTP is accepted only with
AllowLoopbackHTTP set to True and only for the host 127.0.0.1. Use http://127.0.0.1:<port>/..., not
localhost.
HTTP status 301 or HTTP status 302. The server redirected, and the updater does not follow redirects on
purpose. Put the final HTTPS address in the manifest's url and in the FetchManifest call.
Existing cache file has different content. The cache folder already holds a file with the staged name
(atlas-<version>-<sha256>.bin) but other bytes, for example one changed by hand. The updater never overwrites it.
Delete the file, or stage into another folder.
The window stops responding while it checks for updates. FetchManifest and StageUpdate are synchronous and
can take up to the 10-second connection timeout plus the 15-second response timeout, or much longer for a large
file. Run them on a worker thread, as in the TAtlasWebUpdater reference example.
Windows asks for administrator rights before a small test program starts. Windows treats a 32-bit program
whose file name contains words such as "update", "setup", "install" or "patch", and which has no application
manifest, as an installer. Programs built in the IDE have a manifest by default; a program built with the
command-line compiler and no manifest resource does not. Rename the program or add a manifest that sets
requestedExecutionLevel to asInvoker.
Undo empties the syntax editor after a file was loaded. The text history starts with the empty text the editor had when it was created, and loading a file is one more change. There is no method to clear the history, and a new editor starts with the same empty entry, so this cannot be avoided from outside the component; it is a known limitation.