Sign in Join

Live chat for Free Pascal, Lazarus, Delphi and every Pascal programmer.

Catalog · How the catalog works

Random project

dialog4D

Moved to https://github.com/eduardoparaujo/Dialog4D.

View on GitHub

Anyone may press Interesting, signed in or not. It helps others find this project.

Member rating

No ratings yet.

Sign in to rate 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

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

  1. Clone or download this repository.
  2. Add the Source folder to your project search path: Project → Options → Building → Delphi Compiler → Search Path
  3. Add to the uses clause of any unit that shows dialogs:
delphi
uses
  Dialog4D,        // main API — always required
  Dialog4D.Types,  // types, theme, records
  Dialog4D.Await;  // optional: worker-thread await

Quick Start

Information dialog

delphi
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

delphi
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

delphi
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

delphi
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

delphi
// Thread-safe — call from anywhere
TDialog4D.CloseDialog(MainForm, mrCancel);

API Reference

TDialog4D

All methods are class methods and thread-safe.


MessageDialogAsync — standard buttons

delphi
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

delphi
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

delphi
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

delphi
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

delphi
class procedure ConfigureTextProvider(const AProvider: IDialog4DTextProvider); static;

Registers the global text provider for button captions and dialog titles.


ConfigureTelemetry

delphi
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

delphi
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 ATimeoutMs elapses, then invokes AOnResult on the worker thread. When ACallbackOnMain is True, the callback is re-dispatched to the main thread automatically.

MessageDialogOnWorker — blocking, worker thread only

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

delphi
// MessageDialogAsync — standard and custom button overloads
// CloseDialog        — not available in FMX.DialogService

See 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 = mrCancel is used as the Escape / backdrop target.
  • Any TModalResult value is valid except mrNone (0), which Dialog4D reserves internally.
  • For application-defined values, declare named constants above 12 (the last standard value, mrClose) to avoid future collisions:
delphi
const
  mrSaveAndClose = TModalResult(100);
  mrSaveAsDraft  = TModalResult(101);

Example — four visual roles

delphi
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

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

delphi
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

delphi
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

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

Event sequence — form destroyed while dialog is open

pascal
tkOwnerDestroying  Parent form destructor fired; callback suppressed

Close 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 ButtonCaption to identify which button was clicked in telemetry — ButtonKind is always mbOK as 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:

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

Migration from FMX.DialogService

Step-by-step

  1. Replace FMX.DialogService with DialogService4D in the uses clause.
  2. Replace TDialogService.MessageDialog with TDialogService4D.MessageDialogAsync.
  3. Remove the HelpCtx positional argument (Dialog4D does not use it).
  4. Optionally configure a theme and text provider at startup.

Before and after

delphi
// 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