Net-Base Rivista

03.06.2026

Layout reattivi in Delphi FMX: Breakpoints senza caos nel Designer (con Layout-Router come frammento di codice)

I layout responsive in FMX diventano rapidamente fragili nella pratica: raffiche di ridimensionamento, variazioni di DPI, rotazioni e «Visible-Layouts» generano stato duplicato e reflow difficili da debugare. Questo contributo mostra un Layout-Router con Breakpoints che controlla i blocchi UI durante l’esecuzione...

03.06.2026

Dal tema della rivista alla pratica di progetto

Pagine di servizi e tecniche correlate all'articolo

Chi in Delphi FireMonkey deve gestire più form factor arriva rapidamente a Responsive Layouts FMX – e altrettanto rapidamente a una miscela di catene di Align, container di layout nascosti e workaround del designer, che collassano al prossimo cambio di DPI o rotazione. Nei client di software aziendale consolidati questo è particolarmente sgradevole: la UI viene evoluta, i team cambiano e improvvisamente la logica dipende da dettagli visivi.

Il nucleo del problema: FireMonkey offre molti mattoni (es. Align, Anchors, TScaledLayout, TFlowLayout, TGridPanelLayout), ma non un sistema “nativo” di breakpoint come sul web. Si può reagire ai cambi di dimensione, ma senza un’architettura chiara si finisce con tanti „if Width < … then …“ distribuiti su molte Forms.

Questo articolo mostra un Layout-Router: una piccola componente che gestisce centralmente i breakpoint e rimappa Controls (o interi blocchi di layout) tra slot predisposti. Obiettivo: gli stati rimangono intatti, il codice è manutenibile e casi limite come rotazione, layout annidati e re-entrancy vengono attenuati. A questo si aggiungono alcuni accorgimenti meno ovvi che in pratica fanno la differenza tra “funziona in demo” e “gira stabile in produzione”.

Perché i breakpoint in FMX sono diversi rispetto al web

Nei layout web i breakpoint sono per lo più dichiarativi (CSS Media Queries). In FMX le decisioni di layout sono tipicamente imperative a runtime: si cambia nel OnResize. A questo si sommano peculiarità specifiche delle piattaforme:

  • Pixel del dispositivo vs. pixel logici: ClientWidth/ClientHeight sono in unità logiche (dipendenti dalla scala). I cambi di DPI (es. Windows Per-Monitor-DPI) possono riattivare i layout senza che cambi “fisicamente” nulla.
  • Rotazione e Safe Areas: Le piattaforme mobili forniscono inset (Notch/Safe Area) – a seconda di OS e dispositivo. Un „breakpoint basato solo sulla larghezza“ è spesso troppo semplicistico, perché l’area utilizzabile può essere minore della sola dimensione della finestra.
  • Passaggio di layout: FireMonkey calcola i layout in fasi. Se si modifica Parent/Align nel momento sbagliato si generano effetti collaterali (es. ricalcoli multipli o dimensioni lampeggianti).

Un Layout-Router affronta questo separando (1) il „quando“ (Resize/Scale/Rotation) dal „come“ (regole di layout) e (2) concentrando le regole in un unico punto. Per i responsabili tecnici l’effetto più importante è ottenere un centro decisionale chiaro e verificabile invece di molti casi particolari locali.

Architettura: Layout-Router con slot invece di creare Control

Il trucco pulito per FMX: non ricreare dinamicamente i Controls, ma spostare le istanze esistenti tra slot. Uno slot è semplicemente un container (es. TLayout) che rappresenta un’area dell’UI: sidebar, toolbar, content, footer, pannello dei dettagli.

Vantaggi nel software aziendale personalizzato:

  • Gli stati rimangono intatti (Edit, posizione di scorrimento, elementi selezionati), perché le istanze non vengono ricostruite.
  • Minore rischio di doppie associazioni di eventi, timer o binding.
  • Le regole di layout diventano visibili: „quale blocco sta in quale slot“ può essere tracciato e verificato per ogni breakpoint.

Importante per la pratica: suddividete i blocchi UI in modo sufficientemente ampio. Se ricollegate 30 controlli singoli, la lista delle rotte diventa essa stessa una fonte di errori. Meglio utilizzare container come layFilterBar, layNavigation, layResultList, layDetails.

Snippet di codice sorgente: Breakpoint-Router per layout responsive FMX

Il codice seguente è pensato come unità di supporto che potete utilizzare nei form FMX. Calcola un breakpoint (XS/SM/MD/LG/XL) e riattacca i controlli definiti nei corrispondenti slot-container. Dettagli importanti:

  • Debounce tramite TThread.ForceQueue: più eventi di resize vengono aggregati in un unico aggiornamento (meno jitter dell’interfaccia, meno cicli di reflow).
  • Protezione dalla ri-entrata: l’aggiornamento del layout spesso innesca a sua volta Resize/Layout.
  • Opzionale: orientamento (ritratto/paesaggio) può essere integrato nella logica dei breakpoint.

unit NB.FMX.LayoutRouter;

interface

uses
System.Classes, System.SysUtils, System.Types, System.Generics.Collections,
FMX.Types, FMX.Controls;

type
TNBLayoutBreakpoint = (bpXS, bpSM, bpMD, bpLG, bpXL);

// Una mappatura: quale Control deve essere in quale slot (container) per un breakpoint.
TNBRoute = record
Control: TControl;
TargetSlot: TControl; // tipicamente TLayout o TPresentedControl
Align: TAlignLayout;
end;

TNBRouteList = TList<TNBRoute>;

TNBGetBreakpointEvent = reference to function(const AClientSize: TSizeF): TNBLayoutBreakpoint;

TNBLayoutRouter = class(TComponent)
private
FRoot: TControl;
FPending: Boolean;
FUpdating: Boolean;
FCurrent: TNBLayoutBreakpoint;
FOnGetBreakpoint: TNBGetBreakpointEvent;
FRoutes: TObjectDictionary<Integer, TNBRouteList>;
function KeyOf(const ABp: TNBLayoutBreakpoint): Integer;
procedure RootResized(Sender: TObject);
procedure ApplyPending;
procedure ApplyRoutes(const ABp: TNBLayoutBreakpoint);
function DefaultBreakpoint(const AClientSize: TSizeF): TNBLayoutBreakpoint;
public
constructor Create(AOwner: TComponent); override;
destructor Destroy; override;

procedure AttachRoot(const ARoot: TControl);
procedure DefineRoute(const ABp: TNBLayoutBreakpoint; const AControl, ASlot: TControl;
const AAlign: TAlignLayout = TAlignLayout.Client);

procedure Invalidate; // ricalcolo manuale
property Current: TNBLayoutBreakpoint read FCurrent;
property OnGetBreakpoint: TNBGetBreakpointEvent read FOnGetBreakpoint write FOnGetBreakpoint;
end;

implementation

{ TNBLayoutRouter }

constructor TNBLayoutRouter.Create(AOwner: TComponent);
begin
inherited;
FRoutes := TObjectDictionary<Integer, TNBRouteList>.Create([doOwnsValues]);
FCurrent := bpMD;
end;

destructor TNBLayoutRouter.Destroy;
begin
if Assigned(FRoot) then
FRoot.OnResize := nil;
FRoutes.Free;
inherited;
end;

procedure TNBLayoutRouter.AttachRoot(const ARoot: TControl);
begin
if FRoot = ARoot then
Exit;

if Assigned(FRoot) then
FRoot.OnResize := nil;

FRoot := ARoot;
if Assigned(FRoot) then
FRoot.OnResize := RootResized;

Invalidate;
end;

procedure TNBLayoutRouter.DefineRoute(const ABp: TNBLayoutBreakpoint; const AControl,
ASlot: TControl; const AAlign: TAlignLayout);
var
LKey: Integer;
LList: TNBRouteList;
LRoute: TNBRoute;
begin
if (AControl = nil) or (ASlot = nil) then
raise EArgumentNilException.Create(‚Control/Slot non possono essere nil‘);

LKey := KeyOf(ABp);
if not FRoutes.TryGetValue(LKey, LList) then
begin
LList := TNBRouteList.Create;
FRoutes.Add(LKey, LList);
end;

LRoute.Control := AControl;
LRoute.TargetSlot := ASlot;
LRoute.Align := AAlign;
LList.Add(LRoute);
end;

function TNBLayoutRouter.KeyOf(const ABp: TNBLayoutBreakpoint): Integer;
begin
Result := Ord(ABp);
end;

procedure TNBLayoutRouter.RootResized(Sender: TObject);
begin
Invalidate;
end;

procedure TNBLayoutRouter.Invalidate;
begin
if (FRoot = nil) or FUpdating then
Exit;

// Debounce: applicare solo una volta per ciclo di messaggi
if FPending then
Exit;

FPending := True;
TThread.ForceQueue(nil,
procedure
begin
ApplyPending;
end);
end;

procedure TNBLayoutRouter.ApplyPending;
var
LBp: TNBLayoutBreakpoint;
LSize: TSizeF;
begin
if (FRoot = nil) then
Exit;

if not FPending then
Exit;

FPending := False;

LSize := TSizeF.Create(FRoot.Width, FRoot.Height);

if Assigned(FOnGetBreakpoint) then
LBp := FOnGetBreakpoint(LSize)
else
LBp := DefaultBreakpoint(LSize);

if LBp = FCurrent then
Exit;

ApplyRoutes(LBp);
FCurrent := LBp;
end;

procedure TNBLayoutRouter.ApplyRoutes(const ABp: TNBLayoutBreakpoint);
var
LList: TNBRouteList;
LRoute: TNBRoute;
begin
if FUpdating then
Exit;

FUpdating := True;
try
if not FRoutes.TryGetValue(KeyOf(ABp), LList) then
Exit;

// Attenzione: il cambiamento del Parent modifica lo Z-Order.
// Se l’ordine è rilevante, chiamare DefineRoute nell’ordine desiderato.
for LRoute in LList do
begin
if (LRoute.Control.Parent <> LRoute.TargetSlot) then
LRoute.Control.Parent := LRoute.TargetSlot;

// Impostare Align solo dopo aver assegnato il Parent; altrimenti i Bounds possono essere interpretati diversamente.
LRoute.Control.Align := LRoute.Align;
LRoute.Control.Visible := True;
end;

finally
FUpdating := False;
end;
end;

function TNBLayoutRouter.DefaultBreakpoint(const AClientSize: TSizeF): TNBLayoutBreakpoint;
var
W: Single;
begin
W := AClientSize.cx;
// Breakpoint intenzionalmente grezzi, poiché le piattaforme target FMX variano molto.
if W < 480 then Exit(bpXS);
if W < 768 then Exit(bpSM);
if W < 1024 then Exit(bpMD);
if W < 1440 then Exit(bpLG);
Result := bpXL;
end;

end.

Come usare il Router in una Form

Si definiscono slot come TLayout (p. es. layTop, layLeft, layContent) e si registrano poi, per ogni breakpoint, dove risiedono i diversi blocchi. Tipico è che la Sidebar e il Details-Pane nei breakpoint piccoli si dispongano uno sotto l’altro.

Delphi
procedure TFrmMain.FormCreate(Sender: TObject);
begin
  FRouter := TNBLayoutRouter.Create(Self);
  FRouter.AttachRoot(LayoutRoot); // z. B. ein TLayout, das Client-aligned ist

  // XS: alles untereinander
  FRouter.DefineRoute(bpXS, layToolbar, slotTop, TAlignLayout.Top);
  FRouter.DefineRoute(bpXS, laySidebar, slotContent, TAlignLayout.Top);
  FRouter.DefineRoute(bpXS, layDetails, slotContent, TAlignLayout.Top);
  FRouter.DefineRoute(bpXS, layMain,    slotContent, TAlignLayout.Client);

  // MD: Sidebar links, Details rechts
  FRouter.DefineRoute(bpMD, layToolbar, slotTop,    TAlignLayout.Top);
  FRouter.DefineRoute(bpMD, laySidebar, slotLeft,   TAlignLayout.Client);
  FRouter.DefineRoute(bpMD, layMain,    slotCenter, TAlignLayout.Client);
  FRouter.DefineRoute(bpMD, layDetails, slotRight,  TAlignLayout.Client);

  // Optional: eigene Breakpoint-Logik
  FRouter.OnGetBreakpoint :=
    function(const S: TSizeF): TNBLayoutBreakpoint
    begin
      if (S.cx < 700) or (S.cy < 420) then
        Result := bpSM
      else
        Result := bpMD;
    end;
end;

Inquadramento: Perché lo „spostamento“ spesso è più stabile rispetto all’attivazione/disattivazione di Visible

Un approccio diffuso è mantenere per ogni variante alberi di layout separati e limitarsi a togglare Visible. Questo sembra comodo nel designer, ma comporta effetti collaterali tipici:

  • Binding/Events duplicati: Due controlli simili devono essere mantenuti sincronizzati (p. es. due barre di filtro).
  • Ordine di tabulazione e focus: Durante il cambio si perde il focus o si finisce su controlli invisibili se TabStop/HitTest non sono impostati correttamente.
  • Deriva di stato: posizioni di scorrimento, stati di selezione o testi editati divergono.

Lo spostamento mantiene un’istanza unica. È importante suddividere i blocchi di layout in modo che possano essere spostati indipendentemente (p. es. la „Sidebar“ come container separato invece di molti controlli singoli). Proprio questo ripaga in termini di manutenzione e analisi degli errori: si esegue il debug di un’unica istanza, non di due UI parallele in shadow.

Trappole pratiche (e come eseguirne il debug)

1) Tempeste di resize e ri-entrancy

FMX innesca OnResize non solo al ridimensionamento da parte dell’utente, ma anche al cambio di style, alle modifiche del parent e in parte ai cambiamenti di DPI. Senza debounce l’app rimane bloccata in cicli di layout. Il Router utilizza TThread.ForceQueue per spostare le modifiche al tick UI successivo.

Suggerimento per il debug: logging (p. es. tramite OutputDebugString) con breakpoint, dimensione e un contatore di aggiornamenti aiuta a individuare i cicli di reflow. Se registrate inoltre il momento in cui ApplyRoutes inizia e termina, vedrete rapidamente se un singolo resize si „propaga“ a catena.

2) Z-Order, HitTest e blocchi di click „invisibili“

Il cambio di parent modifica lo Z-Order. Se overlay (p. es. Flyouts) non ricevono più click, spesso è perché sopra c’è un container client-aligned con HitTest attivo. Una variante è prevedere appositamente uno slot separato in cima per le aree overlay e parentare quei controlli solo lì. In FMX HitTest (se un controllo intercetta eventi mouse/touch) è più spesso la causa della mancata interazione rispetto alla visibilità.

3) TGridPanelLayout e dimensioni percentuali

TGridPanelLayout può provocare ricalcoli inaspettati con colonne/righe percentuali in combinazione con Align=Client e rimontaggi dinamici. Se dovete usare un Grid, inserite il Grid in uno slot e rimontate solo blocchi Grid interi, non i figli del Grid. Questo riduce la combinatoria dei passaggi di layout.

4) Focus, tastiera virtuale e campi di input ’saltanti‘

Un caso limite che si verifica nelle app FMX mobili e anche su Windows-tablet: durante il rimontaggio un controllo Edit focalizzato può temporaneamente perdere il Parent. Questo può chiudere la tastiera virtuale o resettare il cursore. È pratica consolidata: salvare il focus corrente prima del routing (Focused/IFMXFocusControl) e ripristinarlo dopo il routing (nello stesso tick UI). Vale soprattutto per maschere di input che passano da «due colonne» (Tablet/PC) a «una colonna» (Phone).

Varianti: Breakpoint basati sul form factor invece che solo sulla larghezza

Nei client multipiattaforma reali la «larghezza» da sola spesso non è il segnale corretto. Varianti sensate:

  • Larghezza e altezza: finestre molto piatte (es. terminali di cassa, schermi divisi) richiedono regole diverse.
  • Orientamento: Landscape sui tablet è spesso „simile al desktop“, il Portrait piuttosto „mobile-like“.
  • Area utile (Safe-Area): su iOS/Android l’altezza effettivamente utilizzabile può ridursi notevolmente a causa delle barre di sistema. Chi considera solo Height a volte effettua il routing „troppo tardi“.

Il Router è progettato intenzionalmente in modo che possiate sostituire la funzione di Breakpoint. Questo è utile anche in situazioni legacy, quando la stessa Form viene eseguita in più host (es. una volta come finestra normale, un’altra in un contenitore embedded).

Particolarmente ordinato: Layout-Routing come «Transazione»

Su schermi più grandi il problema dipende meno dai Breakpoint in sé che dall’ordine delle operazioni UI. Un modello pratico è trattare il routing come una transazione: prima decidere, poi rimontare, quindi eseguire ordinatamente gli effetti collaterali (Visibility, focus, refresh dei dati).

In pratica significa: evitate che singoli controlli durante il rimontaggio scatenino eventi propri che a loro volta avviano layout o accesso ai dati. In FMX ciò avviene per esempio quando al cambio di Parent si attivano OnEnter/OnExit o quando un’espressione LiveBinding viene ricalcolata da un aggiornamento dei bounds. Se osservate tali effetti, aiuta un interruttore centrale «Updating» (come nel Router) più un chiaro post-step: solo dopo ApplyRoutes possono essere eseguite operazioni costose (es. ricaricare una lista, associare la vista dettaglio).

Soprattutto per client con accesso REST questo è rilevante: un reload non voluto durante un ridimensionamento può generare richieste inutili. In LAN non si nota, ma su VPN o in mobilità si avverte subito.

Quando l’approccio conviene — e dove ha limiti

Il Layout-Router è vantaggioso quando:

  • un’applicazione FMX vive per anni e più sviluppatori lavorano sugli stessi schermi,
  • i blocchi UI possono essere chiaramente separati (Sidebar/Details/Content),
  • avete bisogno di regole di Breakpoint riproducibili, invece di Align-Tuning ad hoc.

I limiti si manifestano quando una schermata deve essere molto „fluid“ (molte tessere dinamiche, veri Masonry-Layouts). Allora sono più adatti TFlowLayout/TGridPanelLayout o classi di layout personalizzate. Anche quando moltissimi controlli singoli vengono spostati tra gli slot, la manutenzione delle rotte diventa poco gestibile – in quel caso è meglio suddividere in blocchi più grandi o introdurre uno strato di configurazione dichiarativa (ad es. una configurazione JSON per le assegnazioni degli slot, caricata all’avvio).

Conclusione: Per i layout responsive in FMX la „ri-assegnazione con Breakpoints“ è una via di mezzo pragmatica: meno caos nell’ambiente di progettazione, regole chiare, stati stabili. Non sostituisce una struttura UI ben pensata, ma fornisce un telaio solido per far evolvere controllatamente i client FMX nelle soluzioni aziendali digitali attraverso i fattori di forma.

Se in un’applicazione Delphi esistente o in un’applicazione FMX desiderate implementare correttamente una tale architettura di layout senza rischiare regressioni UI negli scenari operativi, potete inquadrarlo tecnicamente con noi: discutere il progetto o l’intervento di modernizzazione con Net-Base.

Nel contesto professionale anche Delphi Fmx Breakpoints e Firemonkey Layout assumono un ruolo importante, quando integrazioni, flussi di dati e sviluppo devono interagire in modo coerente.

Discutere il progetto o l’intervento di modernizzazione con Net-Base.

Passo successivo

Quando un tema diventa un progetto reale, architettura, sistemi esistenti e gestione operativa dovrebbero essere considerati insieme fin dall'inizio.

Non forniamo solo supporto per questioni isolate, ma anche quando da frammenti di codice sorgente, tematiche legacy o idee di portale deve nascere un progetto aziendale solido.

  • Stato attuale, stato obiettivo e rischi tecnici vengono valutati insieme.
  • REST, l'accesso ai dati, i portali e il rollout non vengono rimandati a fasi successive.
  • Vede in anticipo quale percorso è economicamente ed operativamente sostenibile.

Condividi il post

Condividi direttamente questo articolo

LinkedIn, X, XING, Facebook, WhatsApp e e-mail sono immediatamente disponibili. Per Instagram prepariamo direttamente il link e un breve testo.

E-mail

Instagram si apre in una nuova scheda. Il link e il breve testo vengono copiati prima negli appunti.