Net-Base Magazine

03.06.2026

Responsieve lay-outs in Delphi FMX: Breakpoints zonder chaos in de Designer (met Layout-Router als broncodefragment)

Responsive lay-outs in FMX worden in de praktijk snel fragiel: resize-stormen, DPI-wisselingen, rotatie en „visible-layouts“ veroorzaken dubbele toestand en moeilijk te debuggen reflows. Dit artikel toont een layout-router met breakpoints die UI-blokken tijdens de runtime controleert...

03.06.2026

Van magazinethema naar projectpraktijk

Relevante dienst- en technische pagina's bij het artikel

Wie in Delphi FireMonkey meerdere formfactoren moet bedienen, belandt snel bij Responsive Layouts FMX – en even snel bij een mengeling van Align-kaskades, verborgen layout-containers en designer-workarounds die bij de volgende DPI- of rotatiewissel bezwijken. In volwassen business-softwareclients is dat bijzonder onaangenaam: de UI blijft zich ontwikkelen, teams wisselen, en plotseling hangt logica aan visuele details.

De kern van het probleem: FireMonkey biedt veel bouwstenen (z. B. Align, Anchors, TScaledLayout, TFlowLayout, TGridPanelLayout), maar geen „native“ breakpoint-systeem zoals op het web. Je kunt wel op groottewijzigingen reageren, maar zonder duidelijke architectuur eindigt dat in „if Width < … then …“ verspreid over veel forms.

Dit artikel toont een Layout-Router: een kleine component die breakpoints centraal beheert en Controls (of hele layout-blokken) tussen voorbereidende slots omhangt. Doel: staten blijven behouden, de code is onderhoudbaar en randgevallen zoals rotatie, geneste layouts en re-entrancy worden opgevangen. Daarbij een paar minder voor de hand liggende trucs die in de praktijk het verschil maken tussen „draait in de demo“ en „draait stabiel in productie“.

Waarom Breakpoints in FMX anders zijn als op het web

In web-layouts zijn breakpoints meestal declaratief (CSS Media Queries). In FMX worden layout-beslissingen tijdens runtime typisch imperatief genomen: bij OnResize wordt omgeschakeld. Daarbij komen platformspecifieke eigenaardigheden:

  • Device-Pixel vs. logische Pixel: ClientWidth/ClientHeight zijn in logische eenheden (afhankelijk van schaal). DPI-wissels (bijv. Windows Per-Monitor-DPI) kunnen layouts opnieuw triggeren zonder dat er „fysiek“ iets verandert.
  • Rotatie en Safe Areas: Mobiele platformen leveren insets (Notch/Safe Area) – afhankelijk van OS en device. Een „breakpoint alleen op breedte“ is vaak te kortzichtig, omdat de bruikbare ruimte kleiner is dan de pure venstergrootte.
  • Layout-pass: FireMonkey berekent layouts in fasen. Als je op het verkeerde moment Parent/Align wijzigt, ontstaan bijwerkingen (bijv. meervoudige reflow of flikkerende afmetingen).

Een Layout-Router pakt dit aan door (1) het „wanneer“ (Resize/Scale/Rotation) los te koppelen van het „hoe“ (layout-regels) en (2) de regels op één plaats te concentreren. Voor technische leads is het belangrijkste effect: zij krijgen een helder, verifieerbaar besliscentrum in plaats van vele lokale uitzonderingen.

Architectuur: Layout-Router met Slots in plaats van Control-creatie

De nette truc voor FMX: niet dynamisch controls nieuw aanmaken, maar bestaande Controls tussen Slots omhangen. Een Slot is gewoon een container (z. B. TLayout) die een gebied van de UI representeert: Sidebar, Toolbar, Content, Footer, Details-Pane.

Voordelen in op maat gemaakte bedrijfssoftware:

  • Staten blijven behouden (Edit-Text, scrollpositie, geselecteerde items), omdat instanties niet opnieuw opgebouwd worden.
  • Minder risico op dubbele verdrahting van Events, Timers of Bindings.
  • Layout-regels worden zichtbaar: „welk blok ligt in welke slot“ is per breakpoint na te gaan en te reviewen.

Belangrijk in de praktijk: Snijd UI-blokken grof genoeg. Als u 30 afzonderlijke Controls verplaatst, wordt de route-lijst zelf een bron van fouten. Beter zijn containers zoals layFilterBar, layNavigation, layResultList, layDetails.

Broncodefragment: Breakpoint-router voor responsieve layouts FMX

De volgende code is bedoeld als hulpeenheid die u in FMX-Forms kunt gebruiken. Hij berekent een breakpoint (XS/SM/MD/LG/XL) en plaatst gedefinieerde Controls in gedefinieerde slot-containers. Belangrijke details:

  • Debounce via TThread.ForceQueue: meerdere Resize-Events worden samengevoegd tot één update (minder UI-trillingen, minder reflow-lussen).
  • Re-Entrancy-bescherming: een layout-update triggert vaak opnieuw Resize/Layout.
  • Optioneel: oriëntatie (Portret/Landschap) kan in de breakpoint-logica worden meegenomen.
Delphi
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);

  // Een mapping: welke Control in welke slot (container) geplaatst moet worden voor een breakpoint.
  TNBRoute = record
    Control: TControl;
    TargetSlot: TControl; // typisch TLayout of 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; // manuell neu berechnen
    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 mogen niet nil zijn');

  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: slechts één keer per message-loop toepassen
  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;

    // Let op: een wijziging van Parent verandert de Z-order.
    // Als de volgorde relevant is, roep DefineRoute dan in de gewenste volgorde aan.
    for LRoute in LList do
    begin
      if (LRoute.Control.Parent <> LRoute.TargetSlot) then
        LRoute.Control.Parent := LRoute.TargetSlot;

      // Stel Align pas na het instellen van Parent in, anders worden Bounds mogelijk anders geïnterpreteerd.
      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;
  // Breakpoints bewust grof gekozen, omdat FMX-doelplatformen sterk variëren.
  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.

Zo gebruikt u de router in een formulier

U definieert slots als TLayout (bijv. layTop, layLeft, layContent) en registreert vervolgens per breakpoint waar welke blokken komen te liggen. Het is gebruikelijk dat de sidebar en het details-paneel bij kleine breakpoints onder elkaar komen te staan.

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;

Context: Waarom „herparenten“ vaak stabieler is dan das Visible-Schalten

Een gangbare aanpak is voor elke variant aparte layoutbomen aan te houden en alleen Visible te toggelen. Dat lijkt in de designer handig, maar heeft typische bijwerkingen:

  • Dubbele Binding/Events: Twee vergelijkbare controls moeten synchroon gehouden worden (bijv. twee filterbalken).
  • Tabvolgorde en focus: Bij het omschakelen verliest u focus of belandt u in onzichtbare controls als TabStop/HitTest ongunstig zijn ingesteld.
  • State Drift: Scrollposities, selectiestatussen of bewerkte teksten lopen uit elkaar.

Herparenten houdt de instantie eenduidig. Belangrijk is layout-blokken zo te structureren dat ze onafhankelijk verplaatst kunnen worden (bijv. „Sidebar“ als eigen container in plaats van veel losse controls). Dat betaalt zich terug in onderhoud en foutanalyse: u debugt één instantie, niet twee parallelle schaduw‑UIs.

Veelvoorkomende valkuilen in de praktijk (en hoe u ze debugt)

1) Resize-stormen en Re-Entrancy

FMX triggert OnResize niet alleen bij user-resize, maar ook bij style-wisselingen, parent-wijzigingen en deels bij DPI-wijzigingen. Zonder debounce blijft de app in layout-lussen hangen. De router gebruikt TThread.ForceQueue om de wijzigingen naar de volgende UI-tick te verplaatsen.

Debugging-tip: Logging (bijv. via OutputDebugString) met breakpoint, grootte en een update-counter helpt reflow-lussen te identificeren. Als u daarnaast het tijdstip logt waarop ApplyRoutes start en eindigt, ziet u snel of een enkele resize „kaskadeert“.

2) Z-Order, HitTest en „onzichtbare“ klikblokkers

Een parent-wijziging verandert de Z-Order. Als overlays (bijv. flyouts) niet meer reageren op klikken, komt dat vaak doordat er een client-aligned container boven ligt en HitTest actief is. Variant: reserveer voor overlay-gebieden bewust een aparte slot helemaal bovenaan en parent zulke controls alleen daar. In FMX is HitTest (of een control muis-/touch‑gebeurtenissen opvangt) vaker de oorzaak dan zichtbaarheid.

3) TGridPanelLayout en procentuele groottes

TGridPanelLayout kan bij procentuele kolommen/rijen in combinatie met Align=Client en dynamisch herparenten onverwachte herberekeningen veroorzaken. Als u Grid moet gebruiken, plaats de Grid in een slot en verplaats alleen volledige Grid-blokken, niet de Grid-children. Dat vermindert de combinatoriek van de lay-out-passen.

4) Fokus, virtueel toetsenbord en „springende“ invoervelden

Een randgeval dat voorkomt in mobiele FMX-apps en ook op Windows-tablets: bij het herparenten kan een gefocust Edit-Control tijdelijk zijn Parent verliezen. Dat kan het virtuele toetsenbord sluiten of de cursor resetten. In de praktijk werkt het goed om de huidige focus voor het routeren tussen te slaan (Focused/IFMXFocusControl) en na het routeren (in dezelfde UI-tick) de focus te herstellen. Dat is vooral zinvol voor invoerschermen die wisselen tussen „tweekoloms“ (tablet/pc) en „eenkoloms“ (phone).

Varianten: Breakpoints naar formfactor in plaats van alleen naar breedte

In echte multiplatform-clients is „breedte“ op zichzelf vaak niet het juiste signaal. Zinnige varianten:

  • Breedte en hoogte: zeer platte vensters (bijv. kassa-terminals, gedeelde schermen) hebben andere regels nodig.
  • Oriëntatie: Landscape op tablets is vaak „desktop-achtig“, portrait eerder „mobile-achtig“.
  • Safe-Area-gebruikbare ruimte: op iOS/Android kan de effectief bruikbare hoogte door systeembalken flink krimpen. Wie alleen Height bekijkt, routet soms „te laat“.

De router is bewust zo opgebouwd dat u de breakpoint-functie kunt uitwisselen. Dat is ook nuttig in legacy-situaties wanneer dezelfde vorm in meerdere hosts draait (bijv. eenmaal als normaal venster, eenmaal in een embedded container).

Ongewoon schoon: layout-routing als „transactie“

Bij grotere schermen ligt het probleem minder bij de breakpoints zelf dan bij de volgorde van UI-operaties. Een praktijkgeschikt patroon is het routingproces als transactie te behandelen: eerst beslissen, daarna herparenten, vervolgens bij-effecten (zichtbaarheid, focus, gegevensverversing) geordend uitvoeren.

Concreet betekent dit: voorkom dat individuele controls tijdens het herparenten eigen events triggeren die op hun beurt layout of data-acces starten. In FMX gebeurt dat bijvoorbeeld wanneer bij een Parent-wissel OnEnter/OnExit vuurt of een LiveBinding-expressie door een Bounds-update opnieuw geëvalueerd wordt. Als u zulke effecten ziet, helpt een centrale „Updating“-schakelaar (zoals in de router) plus een duidelijke nabehandeling: pas na ApplyRoutes mogen kostbare zaken draaien (bijv. lijst opnieuw laden, detailweergave binden).

Met name bij clients met REST-toegang is dit relevant: een ongewenste reload tijdens een resize kan tot onnodige requests leiden. Dat valt in het LAN niet op, maar in VPN of mobiel direct.

Wanneer de aanpak de moeite waard is – en waar de grenzen liggen

De layout-router verdient zich terug wanneer:

  • een FMX-applicatie jarenlang in gebruik blijft en meerdere ontwikkelaars aan dezelfde schermen werken,
  • UI-blokken duidelijk gescheiden kunnen worden (sidebar/details/content),
  • u reproduceerbare breakpoint-regels nodig heeft in plaats van ad-hoc Align-tuning.

U loopt tegen grenzen aan wanneer een scherm sterk „fluid“ moet zijn (veel dynamische tegels, echte Masonry-Layouts). Dan zijn TFlowLayout/TGridPanelLayout of eigen layout-klassen geschikter. Ook wanneer zeer veel individuele controls tussen slots wisselen, wordt het onderhoud van de routes onoverzichtelijk – snijd dan liever grotere blokken of introduceer een declaratieve configuratielaag (bijv. een JSON-configuratie voor slot-toewijzingen die bij het opstarten wordt geladen).

Conclusie: Voor responsive layouts in FMX is „overschakelen met breakpoints“ een pragmatische middenweg: minder Designer-Chaos, duidelijke regels, stabiele toestanden. Het vervangt geen doordachte UI-structuur, maar het geeft u een robuust kader om FMX-Clients in digitale ondernemingsoplossingen over verschillende formfactoren gecontroleerd verder te ontwikkelen.

Als u in een bestaande Delphi- of FMX-toepassing een dergelijke layout-architectuur nauwkeurig wilt implementeren, zonder daarbij UI-regressies in bedrijfsscenario’s te riskeren, kunt u dat technisch graag met ons afstemmen: project of moderniseringsproject met Net-Base bespreken.

In het vakmatige kader spelen ook Delphi Fmx Breakpoints en Firemonkey Layout een belangrijke rol, wanneer integraties, datastromen en doorontwikkeling nauw moeten samenwerken.

Project of moderniseringsproject met Net-Base bespreken.

Volgende stap

Wanneer het onderwerp een echt project wordt, zouden architectuur, bestaande omgeving en beheer in een vroeg stadium gezamenlijk moeten worden bekeken.

We ondersteunen niet alleen bij individuele vragen, maar ook wanneer uit broncodefragmenten, legacy-onderwerpen of portalideeën een robuust bedrijfsproject moet ontstaan.

  • Huidige situatie, doelbeeld en technische risico's worden gezamenlijk beoordeeld.
  • REST, gegevens‑toegang, portalen en uitrol worden niet als latere gevolgen uitgesteld.
  • U ziet vroeg welke weg economisch en operationeel houdbaar is.

Bericht delen

Dit bericht direct delen

LinkedIn, X, XING, Facebook, WhatsApp en e-mail zijn direct beschikbaar. Voor Instagram bereiden we de link en een korte tekst direct voor.

E-mail

Instagram opent in een nieuw tabblad. Link en korte tekst worden van tevoren naar het klembord gekopieerd.