Catalog · How the catalog works
dialog4D
Moved to https://github.com/eduardoparaujo/Dialog4D.
- ★ 2 on GitHub
Anyone may press Interesting, signed in or not. It helps others find this project.
README
Dialog4D
A modern, fully asynchronous dialog framework for Delphi FMX.
Visually consistent, fully themeable dialogs across Windows, macOS, iOS and Android — with per-form FIFO queueing, worker-thread await, custom buttons, structured telemetry, and zero dependency on native OS dialog APIs.
Table of Contents
- Overview
- Key Features
- Platform Support
- Requirements
- Installation
- Quick Start
- API Reference
- Custom Buttons
- Theming
- Text Provider and Localization
- Telemetry
- Unit Overview
- Compilation Order
- Migration from FMX.DialogService
- Design Decisions
- Versioning
- License
Overview
FMX.DialogService delegates dialog rendering to the native operating system.
That is practical for simple apps but imposes hard limits: you cannot style dialogs
to match your application, you cannot queue multiple dialogs per window, and you have
no insight into how users interact with them.
Dialog4D takes a different approach. Every dialog is rendered as a pure FMX overlay directly inside the parent form — no native OS dialog is ever created. This gives you full control over appearance, behavior, and lifecycle while preserving the fully asynchronous model that FMX requires for mobile platforms.
Key Features
| Feature | Description |
|---|---|
| Fully asynchronous | Non-blocking on all platforms. The main thread is never stalled. |
| Per-form FIFO queue | Multiple calls for the same form are serialized automatically. No dialog is ever lost. |
| Worker-thread await | TDialog4DAwait lets background threads block and wait for a dialog result without blocking the UI. Configurable timeout. |
| Custom buttons | TDialog4DCustomButton — arbitrary captions, any TModalResult, four visual roles. |
| Programmatic close | TDialog4D.CloseDialog closes the active dialog from any thread. |
| Complete theming | Colors, radii, typography, overlay, and button styles — all configurable. |
| Theme snapshots | Configuration is captured at call time, so queued dialogs are unaffected by later theme changes. |
| Localization | Implement IDialog4DTextProvider to supply button captions and titles in any language. |
| Structured telemetry | 7 lifecycle events with full timing, close-reason, and button context. |
| Desktop keyboard | Enter triggers the default button; Escape triggers cancel. |
| Android back-button | Hardware back key consumed safely via OnKeyUp on the overlay; never propagates to the host form. |
| Form-destruction safety | A form-owned hook detects parent form destruction and safely suppresses pending callbacks. |
| Zero OS dependency | Pure FMX visual tree. No ShowMessage, no MessageDlg, no native alert sheets. |
Platform Support
| Platform | Status |
|---|---|
| Windows | Full support |
| macOS | Full support |
| iOS | Full support |
| Android | Full support — close finalization deferred to the main loop |
Requirements
- Delphi 10.4 Sydney or later
- FireMonkey (FMX) — VCL is not supported
- No third-party library dependencies
Installation
Via GetIt Package Manager
GetIt publication coming soon. Follow this repository to be notified.
Manual
- Clone or download this repository.
- Add the
Sourcefolder to your project search path: Project → Options → Building → Delphi Compiler → Search Path - Add to the
usesclause of any unit that shows dialogs:
uses
Dialog4D, // main API — always required
Dialog4D.Types, // types, theme, records
Dialog4D.Await; // optional: worker-thread awaitQuick Start
Information dialog
TDialog4D.MessageDialogAsync(
'Your file was saved successfully.',
TMsgDlgType.mtInformation,
[TMsgDlgBtn.mbOK],
TMsgDlgBtn.mbOK,
procedure(const AResult: TModalResult)
begin
// called on the main thread after the user closes the dialog
end
);Confirmation dialog
TDialog4D.MessageDialogAsync(
'Delete this record? This action cannot be undone.',
TMsgDlgType.mtConfirmation,
[TMsgDlgBtn.mbYes, TMsgDlgBtn.mbNo],
TMsgDlgBtn.mbNo,
procedure(const AResult: TModalResult)
begin
if AResult = mrYes then DeleteRecord;
end
);Custom buttons
TDialog4D.MessageDialogAsync(
'Your session is about to expire.',
TMsgDlgType.mtWarning,
[
TDialog4DCustomButton.Default('Keep Me Signed In', mrOk),
TDialog4DCustomButton.Make('Sign Out Now', mrClose),
TDialog4DCustomButton.Cancel('Remind Me Later')
],
procedure(const AResult: TModalResult)
begin
case AResult of
mrOk: RenewSession;
mrClose: SignOut;
end;
end,
'Session Expiring'
);Worker-thread await
TTask.Run(procedure
var
LStatus: TDialog4DAwaitStatus;
LResult: TModalResult;
begin
LResult := TDialog4DAwait.MessageDialogOnWorker(
'The import is taking longer than expected. Continue?',
TMsgDlgType.mtConfirmation,
[TMsgDlgBtn.mbYes, TMsgDlgBtn.mbNo],
TMsgDlgBtn.mbYes,
LStatus,
'Import',
nil,
True,
30000 // 30-second timeout
);
if (LStatus = dasCompleted) and (LResult = mrYes) then
ContinueImport
else
CancelImport;
end);Programmatic close
// Thread-safe — call from anywhere
TDialog4D.CloseDialog(MainForm, mrCancel);API Reference
TDialog4D
All methods are class methods and thread-safe.
MessageDialogAsync — standard buttons
class procedure MessageDialogAsync(
const AMessage: string;
const ADialogType: TMsgDlgType;
const AButtons: TMsgDlgButtons;
const ADefaultButton: TMsgDlgBtn;
const AOnResult: TDialog4DResultProc;
const ATitle: string = '';
const AParent: TCommonCustomForm = nil;
const ACancelable: Boolean = True
); overload; static;Enqueues a dialog and returns immediately. If another dialog is already active for the same form the request is queued and shown automatically when the current one closes.
| Parameter | Description |
|---|---|
AMessage |
Message body. |
ADialogType |
mtInformation, mtWarning, mtError, mtConfirmation, or mtCustom. |
AButtons |
One or more TMsgDlgBtn values. At least one required. |
ADefaultButton |
Activated by Enter on desktop. |
AOnResult |
Callback invoked on the main thread. |
ATitle |
Optional title. Empty = text provider default. |
AParent |
Optional parent form. nil = resolved automatically. |
ACancelable |
When True, backdrop tap closes the dialog via the cancel button. |
MessageDialogAsync — custom buttons
class procedure MessageDialogAsync(
const AMessage: string;
const ADialogType: TMsgDlgType;
const AButtons: TArray<TDialog4DCustomButton>;
const AOnResult: TDialog4DResultProc;
const ATitle: string = '';
const AParent: TCommonCustomForm = nil;
const ACancelable: Boolean = True
); overload; static;Same behavior as the standard overload but accepts fully custom buttons. See Custom Buttons for details.
CloseDialog
class procedure CloseDialog(
const AForm: TCommonCustomForm = nil;
const AResult: TModalResult = mrCancel
); static;Programmatically closes the active dialog for AForm. Thread-safe. Silently ignored if
no dialog is active. Queued dialogs are not affected. Close reason recorded as
crProgrammatic in telemetry.
ConfigureTheme
class procedure ConfigureTheme(const ATheme: TDialog4DTheme); static;Sets the global theme. Captured as a snapshot at request time — changing the theme
after MessageDialogAsync returns does not affect already-queued dialogs.
ConfigureTextProvider
class procedure ConfigureTextProvider(const AProvider: IDialog4DTextProvider); static;Registers the global text provider for button captions and dialog titles.
ConfigureTelemetry
class procedure ConfigureTelemetry(const AProc: TDialog4DTelemetryProc); static;Registers the telemetry sink. Set to nil to disable.
TDialog4DAwait
Worker-thread aware helper. All methods are class methods.
MessageDialog — smart, any thread
class procedure MessageDialog(
const AMessage: string;
const ADialogType: TMsgDlgType;
const AButtons: TMsgDlgButtons; // or TArray<TDialog4DCustomButton>
const ADefaultButton: TMsgDlgBtn;
const AOnResult: TDialog4DResultProc;
const ATitle: string = '';
const AParent: TCommonCustomForm = nil;
const ACancelable: Boolean = True;
const ACallbackOnMain: Boolean = False;
const ATimeoutMs: Cardinal = 300000
); static;Detects the calling thread automatically:
- Main thread — behaves identically to
TDialog4D.MessageDialogAsync. Non-blocking. - Worker thread — blocks until the user responds or
ATimeoutMselapses, then invokesAOnResulton the worker thread. WhenACallbackOnMainisTrue, the callback is re-dispatched to the main thread automatically.
MessageDialogOnWorker — blocking, worker thread only
class function MessageDialogOnWorker(
const AMessage: string;
const ADialogType: TMsgDlgType;
const AButtons: TMsgDlgButtons; // or TArray<TDialog4DCustomButton>
const ADefaultButton: TMsgDlgBtn;
out AStatus: TDialog4DAwaitStatus;
const ATitle: string = '';
const AParent: TCommonCustomForm = nil;
const ACancelable: Boolean = True;
const ATimeoutMs: Cardinal = 300000
): TModalResult; static;Blocks the calling thread until the dialog closes or the timeout elapses.
Raises EDialog4DAwait if called from the main thread.
AStatus value |
Meaning |
|---|---|
dasCompleted |
The user responded. Result holds the modal result. |
dasTimedOut |
No response before ATimeoutMs. Result is mrNone. |
TDialogService4D
Drop-in migration facade. Exposes a TDialogService-compatible surface plus the
Dialog4D-specific methods not available in FMX.DialogService.
// MessageDialogAsync — standard and custom button overloads
// CloseDialog — not available in FMX.DialogServiceSee Migration from FMX.DialogService for details.
Custom Buttons
TDialog4DCustomButton replaces the TMsgDlgBtn + IDialog4DTextProvider pipeline
with buttons that have arbitrary captions and any TModalResult value.
Constructors
| Constructor | Visual role | IsDefault |
IsDestructive |
ModalResult |
|---|---|---|---|---|
Make(caption, result [, isDefault, isDestructive]) |
Explicit control | You choose | You choose | You choose |
Default(caption, result) |
Primary action (blue) | True |
False |
You choose |
Destructive(caption, result) |
Dangerous action (red) | False |
True |
You choose |
Cancel(caption) |
Cancel (neutral) | False |
False |
Always mrCancel |
Make is the base constructor. Default, Destructive and Cancel are convenience
shortcuts that call Make with pre-set flags.
Rules
- At most one button should have
IsDefault = True. If none does, the first button is promoted automatically. - A button with
ModalResult = mrCancelis used as the Escape / backdrop target. - Any
TModalResultvalue is valid exceptmrNone(0), which Dialog4D reserves internally. - For application-defined values, declare named constants above 12 (the last standard
value,
mrClose) to avoid future collisions:
const
mrSaveAndClose = TModalResult(100);
mrSaveAsDraft = TModalResult(101);Example — four visual roles
TDialog4D.MessageDialogAsync(
'You have unsaved changes.',
TMsgDlgType.mtConfirmation,
[
TDialog4DCustomButton.Default ('Save and Close', mrSaveAndClose),
TDialog4DCustomButton.Make ('Close Without Saving', mrNoToAll),
TDialog4DCustomButton.Destructive('Discard All Changes', mrAbort),
TDialog4DCustomButton.Cancel ('Stay Here')
],
procedure(const AResult: TModalResult)
begin
case AResult of
mrSaveAndClose: SaveAndClose;
mrNoToAll: CloseWithoutSaving;
mrAbort: DiscardChanges;
end;
end,
'Unsaved Changes'
);Theming
Applying a theme
var
LTheme: TDialog4DTheme;
begin
LTheme := TDialog4DTheme.Default;
LTheme.SurfaceColor := $FF1E1E2E; // dark surface
LTheme.TextTitleColor := $FFCDD6F4;
LTheme.TextMessageColor := $FF9399B2;
LTheme.AccentInfoColor := $FF89B4FA;
LTheme.AccentErrorColor := $FFF38BA8;
LTheme.OverlayOpacity := 0.60;
LTheme.MessageTextAlign := dtaLeading; // left-align long messages
TDialog4D.ConfigureTheme(LTheme);
end;Theme field reference
Geometry
| Field | Type | Default | Description |
|---|---|---|---|
DialogWidth |
Single |
320 |
Card width. Adjusted by platform ratio at runtime. |
DialogMinHeight |
Single |
170 |
Minimum card height. |
DialogMaxHeightRatio |
Single |
0.85 |
Maximum card height as a fraction of the parent form height. |
ContentMinHeight |
Single |
50 |
Minimum height of the message scroll area. |
CornerRadius |
Single |
14 |
Card corner radius. |
Overlay
| Field | Type | Default | Description |
|---|---|---|---|
OverlayColor |
TAlphaColor |
$FF000000 |
Backdrop fill color. |
OverlayOpacity |
Single |
0.45 |
Backdrop opacity. |
Typography
| Field | Type | Default | Description |
|---|---|---|---|
TitleFontSize |
Single |
16 |
Title font size. Title is always centered. |
MessageFontSize |
Single |
14 |
Message body font size. |
MessageTextAlign |
TDialog4DTextAlign |
dtaCenter |
Message body alignment. Use dtaLeading for long text. |
Buttons
| Field | Type | Default | Description |
|---|---|---|---|
ButtonHeight |
Single |
44 |
Height of each button. |
ButtonFontSize |
Single |
14 |
Button label font size. |
ButtonNeutralFillColor |
TAlphaColor |
$FFF3F3F3 |
Fill for neutral buttons. |
ButtonNeutralTextColor |
TAlphaColor |
$FF2C2C2C |
Text for neutral buttons. |
ButtonNeutralBorderColor |
TAlphaColor |
$FFCECECE |
Border for neutral buttons. |
Colors
| Field | Type | Default | Description |
|---|---|---|---|
SurfaceColor |
TAlphaColor |
White | Card background. |
TextTitleColor |
TAlphaColor |
$FF2C2C2C |
Title text. |
TextMessageColor |
TAlphaColor |
$FF848484 |
Message body text. |
AccentInfoColor |
TAlphaColor |
$FF3085D6 |
Default button + mtInformation icon. |
AccentWarningColor |
TAlphaColor |
$FFF3A867 |
mtWarning icon. |
AccentErrorColor |
TAlphaColor |
$FFE64D4D |
mtError icon + destructive buttons. |
AccentConfirmColor |
TAlphaColor |
$FF9E9E9E |
mtConfirmation icon. |
AccentNeutralColor |
TAlphaColor |
$FFEFEFEF |
mtCustom icon circle. |
Default-button highlight
| Field | Type | Default | Description |
|---|---|---|---|
ShowDefaultButtonHighlight |
Boolean |
True |
Renders a subtle ring inside the default button. |
DefaultButtonHighlightColor |
TAlphaColor |
White | Ring color. |
DefaultButtonHighlightThickness |
Single |
1 |
Ring stroke thickness. |
DefaultButtonHighlightOpacity |
Single |
0.95 |
Ring opacity. |
DefaultButtonHighlightInset |
Single |
1 |
Ring inset from the button edge. |
Other
| Field | Type | Default | Description |
|---|---|---|---|
TreatCloseAsCancel |
Boolean |
False |
When True, mrClose is treated as cancel for backdrop-tap purposes. |
Text Provider and Localization
Implement IDialog4DTextProvider to supply captions in any language:
type
TPortugueseProvider = class(TInterfacedObject, IDialog4DTextProvider)
public
function ButtonText(const ABtn: TMsgDlgBtn): string;
function TitleForType(const ADlgType: TMsgDlgType): string;
end;
function TPortugueseProvider.ButtonText(const ABtn: TMsgDlgBtn): string;
begin
case ABtn of
TMsgDlgBtn.mbOK: Result := 'OK';
TMsgDlgBtn.mbCancel: Result := 'Cancelar';
TMsgDlgBtn.mbYes: Result := 'Sim';
TMsgDlgBtn.mbNo: Result := 'Não';
TMsgDlgBtn.mbClose: Result := 'Fechar';
else
Result := 'Botão';
end;
end;
function TPortugueseProvider.TitleForType(const ADlgType: TMsgDlgType): string;
begin
case ADlgType of
TMsgDlgType.mtInformation: Result := 'Informação';
TMsgDlgType.mtWarning: Result := 'Atenção';
TMsgDlgType.mtError: Result := 'Erro';
TMsgDlgType.mtConfirmation: Result := 'Confirmação';
else
Result := '';
end;
end;
// Register at application startup
TDialog4D.ConfigureTextProvider(TPortugueseProvider.Create);Custom buttons bypass the text provider entirely — their captions are used verbatim.
Telemetry
uses Dialog4D, Dialog4D.Types, Dialog4D.Telemetry.Format;
TDialog4D.ConfigureTelemetry(
procedure(const AData: TDialog4DTelemetry)
begin
TFile.AppendAllText(
'dialog_events.log',
TDialog4DTelemetryFormat.FormatTelemetry(AData) + sLineBreak
);
end
);Event sequence — normal interaction
tkShowRequested MessageDialogAsync was called
tkShowDisplayed Overlay visible; open animation finished
tkCloseRequested Button pressed, backdrop tapped, or key pressed
tkClosed Close animation finished; visual tree destroyed
tkCallbackInvoked AOnResult executed successfullyEvent sequence — form destroyed while dialog is open
tkOwnerDestroying Parent form destructor fired; callback suppressedClose reasons
| Value | Triggered by |
|---|---|
crButton |
User clicked or tapped a button |
crBackdrop |
Backdrop tap (ACancelable = True) |
crKeyEnter |
Enter key on desktop |
crKeyEsc |
Escape key on desktop or Android hardware back |
crProgrammatic |
TDialog4D.CloseDialog |
crOwnerDestroying |
Parent form destroyed |
TDialog4DTelemetry fields
| Field | Type | Description |
|---|---|---|
Kind |
TDialog4DTelemetryKind |
Event type |
DialogType |
TMsgDlgType |
Dialog semantic type |
Title |
string |
Title at show time |
MessageLen |
Integer |
Character count of the message body |
ButtonsCount |
Integer |
Number of buttons displayed |
HasCancelButton |
Boolean |
Whether a cancel button was present |
DefaultResult |
TModalResult |
Modal result of the default button |
Result |
TModalResult |
Effective modal result at close |
CloseReason |
TDialog4DCloseReason |
How the dialog was closed |
ButtonKind |
TMsgDlgBtn |
Button that was triggered (mbOK placeholder for custom buttons) |
ButtonCaption |
string |
Caption of the triggered button |
ButtonWasDefault |
Boolean |
Whether the triggered button was the default |
Tick |
UInt64 |
TThread.GetTickCount64 at event time |
ElapsedMs |
UInt64 |
Milliseconds since the dialog was shown |
EventDateTime |
TDateTime |
Wall-clock time of the event |
For custom buttons, use
ButtonCaptionto identify which button was clicked in telemetry —ButtonKindis alwaysmbOKas a placeholder.
Unit Overview
| Unit | Purpose |
|---|---|
Dialog4D |
Main entry point. Public API, global configuration, per-form FIFO registry. |
Dialog4D.Types |
All shared types: theme, telemetry, custom button, enums, callbacks. |
Dialog4D.Host.FMX |
FMX visual host. Owns the entire visual tree for one dialog instance. Internal. |
Dialog4D.Await |
Worker-thread await helper (TDialog4DAwait, TDialog4DAwaitStatus). |
Dialog4D.Telemetry.Format |
Converts TDialog4DTelemetry records to human-readable log strings. |
Dialog4D.TextProvider.Default |
Built-in English IDialog4DTextProvider. |
Dialog4D.Internal.Queue |
Internal utility — QueueOnMainThread dispatch helper. |
DialogService4D |
Drop-in migration facade with a FMX.DialogService-compatible API. |
Compilation Order
If you add the units to a package or need to control the build order manually:
1. Dialog4D.Internal.Queue
2. Dialog4D.Types
3. Dialog4D.TextProvider.Default
4. Dialog4D.Telemetry.Format
5. Dialog4D.Host.FMX
6. Dialog4D
7. Dialog4D.Await
8. DialogService4DMigration from FMX.DialogService
Step-by-step
- Replace
FMX.DialogServicewithDialogService4Din theusesclause. - Replace
TDialogService.MessageDialogwithTDialogService4D.MessageDialogAsync. - Remove the
HelpCtxpositional argument (Dialog4D does not use it). - Optionally configure a theme and text provider at startup.
Before and after
// Before — FMX.DialogService
TDialogService.MessageDialog(
'Continue?',
TMsgDlgType.mtConfirmation,
[TMsgDlgBtn.mbYes, TMsgDlgBtn.mbNo],
TMsgDlgBtn.mbNo,
0, // HelpCtx — remove this
procedure(const AResult: TModalResult)
begin
if AResult = mrYes then DoIt;
end
);
// After — DialogService4D (drop-in facade)
TDialogService4D.MessageDialogAsync(
'Continue?',
TMsgDlgType.mtConfirmation,
[TMsgDlgBtn.mbYes, TMsgDlgBtn.mbNo],
TMsgDlgBtn.mbNo,
procedure(const AResult: TModalResult)
begin
if AResult = mrYes then DoIt;
end
);Design Decisions
Why per-form queueing instead of a global queue? In multi-window applications, dialogs for different forms are logically independent. A global queue would force the user to dismiss a dialog for Form A before seeing one for Form B. Per-form queueing matches user expectations: each window manages its own dialog stack.
Why is the theme captured at request time? This allows theme changes (e.g. dark mode toggle) without corrupting the appearance of dialogs that are already queued. Each dialog renders exactly as configured when it was requested.
Why does FinalizeCloseNow run callbacks after Cleanup?
All visual state is destroyed before the callback executes. This ensures the callback
cannot interact with a half-destroyed dialog tree. If the callback needs to show
another dialog, it goes through the normal queue on the next main-thread cycle.
Why is QueueOnMainThread in a separate internal unit?
The pattern was originally duplicated across three units. A single owner means bug
fixes apply everywhere automatically and the dependency is explicit in the uses
clause of each consumer.
Versioning
Dialog4D follows Semantic Versioning:
| Increment | When |
|---|---|
PATCH x.x.1 |
Bug fix; no API change |
MINOR x.1.0 |
New feature; backward compatible |
MAJOR 1.0.0 |
Breaking API change |
The framework version is tracked in this README and in Git tags. Each unit tracks its own version independently in its file header — a unit that has not been modified since its initial release keeps its original version number. Only units whose code changes have their header updated.
Current versions
| Unit | Version |
|---|---|
Dialog4D |
1.0.0 |
Dialog4D.Types |
1.0.0 |
Dialog4D.Host.FMX |
1.0.0 |
Dialog4D.Await |
1.0.0 |
DialogService4D |
1.0.0 |
Dialog4D.TextProvider.Default |
1.0.0 |
Dialog4D.Telemetry.Format |
1.0.0 |
Dialog4D.Internal.Queue |
1.0.0 |
License
MIT License — see LICENSE for the full text.
Copyright (c) 2026 Eduardo Kumada

