Net-Base Revista

03.06.2026

Layouts responsius en Delphi FMX: Breakpoints sense el caos del Designer (amb Layout-Router com a fragment de codi font)

Aquests layouts responsius FMX es tornen ràpidament fràgils a la pràctica: tempestes de redimensionament, canvis de DPI, rotacions i «Visible-Layouts» generen un estat duplicat i reflows difícils de depurar. Aquest article mostra un Layout-router amb Breakpoints que controla blocs UI en temps d'execució.

03.06.2026

Del tema de la revista a la pràctica del projecte

Pàgines de serveis i tècniques pertinents per a l'article

Qui, a Delphi, ha de donar suport a diversos factors de forma en FireMonkey, acaba ràpidament amb Responsive Layouts FMX — i igualment ràpidament amb una barreja de cascades d’Align, contenidors de layout ocults i solucions d’editor que s’ensorren en el següent canvi de DPI o de rotació. En clients de software empresarial madurs això és especialment incòmode: la UI evoluciona, els equips canvien i de sobte la lògica depèn de detalls visuals.

El nucli del problema: FireMonkey ofereix molts components (p. ex. Align, Anchors, TScaledLayout, TFlowLayout, TGridPanelLayout), però no té un sistema de Breakpoints «natiu» com al web. Es pot reaccionar als canvis de mida, però sense una arquitectura clara això acaba en «if Width < … then …» estès per moltes forms.

Aquest article presenta un Layout-Router: una petita component que gestiona els breakpoints de manera centralitzada i penja els controls (o blocs complets de layout) entre ranures (slots) predefinides. Objectiu: els estats es mantenen, el codi és mantenible i casos límit com rotació, layouts anidats i re-entrades queden amortits. A més, hi ha alguns trucs menys evidents que a la pràctica marquen la diferència entre «funciona a la demo» i «funciona estable en producció».

Per què els Breakpoints en FMX són diferents que al web

A la web els breakpoints solen ser declaratius (CSS Media Queries). A FMX les decisions de layout són típicament imperatives en temps d’execució: es canvia en OnResize. A això s’afegeixen peculiaritats específiques de plataforma:

  • Device-Pixel vs. píxels lògics: ClientWidth/ClientHeight estan en unitats lògiques (depèn de l’escala). Canvis de DPI (p. ex. Windows Per-Monitor-DPI) poden tornar a disparar els layouts sense que «físicament» passi res.
  • Rotació i Safe Areas: les plataformes mòbils proporcionen insets (Notch/Safe Area) – depenent del sistema operatiu i del dispositiu. Un «breakpoint només per amplada» sovint és massa simplista, perquè l’àrea útil és menor que la mida nominal de la finestra.
  • Layout-Pass: FireMonkey calcula els layouts en fases. Si canvies Parent/Align en el moment equivocat s’originen efectes secundaris (p. ex. reflows múltiples o parpelleig de mides).

Un Layout-Router afronta això deslligant (1) el «quan» (Resize/Scale/Rotation) del «com» (regles de layout) i (2) concentrant les regles en un únic lloc. Per a responsables tècnics l’efecte més rellevant: obtenen un centre de decisió clar i verificable en lloc de molts casos especials locals.

Arquitectura: Layout-Router amb Slots en lloc de crear Controls

El truc net per FMX: no crear controls dinàmicament, sinó reassignar els controls existents entre slots. Un slot és simplement un contenidor (p. ex. TLayout) que representa una zona de la UI: sidebar, toolbar, content, footer, details-pane.

Avantatges en software empresarial a mida:

  • Els estats es mantenen (camp d’edició, posició de scroll, ítems seleccionats), perquè les instàncies no es tornen a crear.
  • Menys risc de doble cablejat d’events, timers o bindings.
  • Les regles de layout es fan visibles: «quin bloc està en quin slot» es pot seguir i revisar per cada breakpoint.

Important per a la pràctica: talleu els blocs d’UI amb prou granularitat. Si es reassignen 30 controls individuals, la llista de rutes esdevé ella mateixa una font d’errors. Millor utilitzar contenidors com layFilterBar, layNavigation, layResultList, layDetails.

Fragment de codi: Router de punts de trencament per a layouts adaptatius FMX

El següent codi està pensat com una unitat auxiliar que es pot utilitzar en FMX-Forms. Calcula un punt de trencament (XS/SM/MD/LG/XL) i reassigna controls definits a contenidors slot definits. Detalls importants:

  • Debounce via TThread.ForceQueue: diversos esdeveniments de redimensionament es combinen en una única actualització (menys parpelleig de la UI, menys bucles de reflow).
  • Protecció contra re-entrades: una actualització del layout sovint desencadena noves operacions de Resize/Layout.
  • Opcional: orientació (Portrait/Landscape) es pot fer servir dins la lògica de punts de trencament.

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);

// Un mapeig: quin Control ha d’anar a quin Slot (contenidor) per a cada Breakpoint.
TNBRoute = record
Control: TControl;
TargetSlot: TControl; // typischerweise TLayout oder 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; // recalcular manualment
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 no poden ser 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: només una vegada per cicle de missatges
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;

// Atenció: el canvi de Parent modifica l’ordre Z.
// Si l’ordre és rellevant, cridar DefineRoute en l’ordre desitjat.
for LRoute in LList do
begin
if (LRoute.Control.Parent <> LRoute.TargetSlot) then
LRoute.Control.Parent := LRoute.TargetSlot;

// Assignar Align només després d’establir el Parent; si no, els Bounds podrien interpretar-se de manera diferent.
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;
// Els breakpoints són deliberadament amplis, ja que les plataformes objectiu de FMX varien considerablement.
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.

Com utilitzar el Router en un formulari

Definiu slots com a TLayout (p. ex. layTop, layLeft, layContent) i registreu després per cada Breakpoint on van situats els blocs. És típic que la Sidebar i el panell de detalls en breakpoints petits passin a estar un sota l’altre.

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;

Classificació: Per què el reparentat sovint és més estable que canviar la propietat Visible

Un enfocament estès és mantenir arbres de layout separats per a cada variant i només alternar Visible. Això resulta còmode al designer, però té efectes secundaris típics:

  • Binding/Events duplicats: Dos controls similars han de mantenir-se sincronitzats (p. ex. dues barres de filtre).
  • Ordre de tabulació i focus: En commutar es perd el focus o es pot acabar en controls invisibles si TabStop/HitTest estan en configuracions desafavorables.
  • Divergència d’estat: Les posicions de desplaçament, els estats de selecció o els textos editats divergeixen.

El reparentat manté una única instància. És important segmentar els blocs de layout perquè es puguin moure de manera independent (p. ex. „Sidebar“ com a contenidor propi en lloc de molts controls individuals). Això es tradueix en avantatges en manteniment i anàlisi d’errors: depureu una instància, no dues UIs paral·leles.

Problemes freqüents a la pràctica (i com depurar-los)

1) Tempestes de redimensionament i reentrància

FMX activa OnResize no només en redimensionament per part de l’usuari, sinó també en canvis d’estil, canvis de parent i parcialment en canvis de DPI. Sense debounce l’aplicació queda atrapada en bucles de layout. El Router utilitza TThread.ForceQueue per ajornar els canvis al següent tick de la UI.

Consell de depuració: el logging (p. ex. via OutputDebugString) amb breakpoint, mida i un comptador d’actualitzacions ajuda a localitzar els bucles de reflow. Si a més registreu el moment en què ApplyRoutes comença i acaba, veureu ràpidament si un únic redimensionament provoca un efecte de cascada.

2) Z-Order, HitTest i bloquejadors de clic „invisibles“

Un canvi de parent modifica la Z-Order. Si els overlays (p. ex. Flyouts) deixen de rebre clics, sovint és perquè hi ha un contenidor alineat al client per sobre i HitTest està actiu. Variant: per a superfícies d’overlay, prevegeu conscientment un slot separat a la part superior i parenteu només allà aquests controls. En FMX, HitTest (si un control intercepta esdeveniments de ratolí/tacte) sovint és la causa més freqüent que la visibilitat.

3) TGridPanelLayout i mides percentuals

TGridPanelLayout pot provocar recàlculs inesperats amb columnes/files en percentatge en combinació amb Align=Client i reubicacions dinàmiques. Si cal utilitzar Grid, col·loqueu el Grid en un slot i torneu a emplaçar només blocs complets del Grid, no els fills del Grid. Això redueix la combinatòria de passades de layout.

4) Fokus, virtuelle Tastatur und „springende“ Eingabefelder

Un cas marginal que apareix en apps FMX mòbils i també en tablets Windows: durant la reubicació un control Edit focalitzat pot perdre temporalment el seu parent. Això pot tancar el teclat virtual o reiniciar el cursor. Ha demostrat ser pràctic: desar temporalment el focus actual abans del Routing (Focused/IFMXFocusControl) i restaurar-lo després del Routing (en el mateix UI-Tick). Val la pena especialment en formularis d’entrada que canvien entre „zweispaltig“ (Tablet/PC) i „einspaltig“ (Phone).

Varianten: Breakpoints nach Formfaktor statt nur nach Breite

En clients multiplataforma reals, l’„Breite“ sovint no és l’indicador correcte. Varianten raonables:

  • Breite und Höhe: finestres molt planes (p. ex. Kassen-Terminals, pantalles dividides) necessiten regles diferents.
  • Orientierung: Landscape en tablets sovint és „desktop-ähnlich“, el Portrait més „mobile-like“.
  • Safe-Area-Nutzfläche: a iOS/Android l’altura efectivament utilitzable pot reduir-se significativament per les barres del sistema. Qui només mira Height de vegades fa el Routing „zu spät”.

El Router està deliberadament dissenyat perquè pugueu substituir la funció de Breakpoint. Això també és útil en situacions legacy, quan el mateix formulari s’executa en diversos hosts (p. ex. una vegada com a finestra normal, una altra en un contenidor embegut).

Ungewöhnlich sauber: Layout-Routing als „Transaktion“

En pantalles més grans el problema depèn menys dels Breakpoints en si i més de l’ordre de les operacions de la UI. Un patró pràctic és tractar el Routing com una transacció: primer decidir, després reubicar, i finalment executar ordenadament els efectes secundaris (visibilitat, focus, refresc de dades).

Concretament això vol dir: eviteu que controls individuals durant la reubicació desencadenin els seus propis events que, al seu torn, iniciïn layout o accés a dades. En FMX això passa, per exemple, quan en el canvi de parent s’activen OnEnter/OnExit o una expressió de LiveBinding es reavalua a causa d’una actualització de bounds. Si observeu aquests efectes, ajuda un interruptor central „Updating“ (com en el Router) més un pas clar posterior: només després de ApplyRoutes poden executar-se operacions costoses (p. ex. recarregar una llista, enllaçar la vista de detalls).

Sobretot en clients amb accés REST això és rellevant: una recàrrega no desitjada durant un redimensionament pot generar peticions innecessàries. Això no es notarà en LAN, però sí immediatament en VPN o en mòbil.

Wann sich der Ansatz lohnt – und wo er Grenzen hat

El Layout-Router compensa quan:

  • una aplicació FMX perdura durant anys i diversos desenvolupadors treballen en les mateixes pantalles,
  • es poden separar clarament blocs de la UI (Sidebar/Details/Content),
  • necessiteu regles de Breakpoint reproductibles, en lloc d’ajustaments d’alineació ad hoc.

Veureu límits quan una pantalla hagi de ser molt «fluid» (moltes rajoles dinàmiques, veritables Masonry-Layouts). En aquests casos són més adequats TFlowLayout/TGridPanelLayout o classes de layout pròpies. També, quan molts controls individuals intercanvien entre slots, el manteniment de les rutes es torna poc manejable — llavors és millor tallar blocs més grans o introduir una capa de configuració declarativa (p. ex. una configuració JSON per a assignacions de slots que es carregui a l’inici).

Conclusió: Per als layouts responsius d’FMX, la reassignació mitjançant breakpoints és una via pragmàtica: menys caos del disseny, regles clares, estats estables. No substitueix una estructura d’UI ben pensada, però proporciona un marc sòlid per desenvolupar de manera controlada clients FMX en solucions empresarials digitals a través dels diferents factors de forma.

Si voleu implementar de manera neta una arquitectura de layout d’aquest tipus en una aplicació existent Delphi o FMX, sense posar en risc regressions de la UI en escenaris operatius, ho podem avaluar tècnicament amb vosaltres: parlar del projecte o iniciativa de modernització amb Net-Base.

En l’àmbit tècnic també són rellevants Delphi Fmx Breakpoints i Firemonkey Layout quan cal que les integracions, els fluxos de dades i el desenvolupament continu es coordinin de manera neta.

Parlar del projecte o iniciativa de modernització amb Net-Base.

Pas següent

Quan un tema esdevé un projecte real, l'arquitectura, l'entorn existent i les operacions s'haurien de considerar conjuntament des de bon començament.

No només donem suport en qüestions puntuals, sinó també quan, a partir de fragments de codi font, temes de sistemes heredats o idees de portal, ha de sorgir un projecte empresarial sòlid.

  • L'estat actual, la visió objectiu i els riscos tècnics s'avaluen conjuntament.
  • REST, l'accés a les dades, els portals i el desplegament no es releguen a fases posteriors.
  • Vostè veurà aviat quin camí és econòmicament i operativament viable.

Comparteix la publicació

Comparteix aquesta publicació directament

LinkedIn, X, XING, Facebook, WhatsApp i E-Mail estan disponibles de forma immediata. Per a Instagram preparem directament l’enllaç i un text breu.

Correu electrònic

Instagram s'obre en una pestanya nova. L'enllaç i el text curt es copien prèviament al porta-retalls.