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/ClientHeightzijn 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.
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.
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:
Landscapeop 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
Heightbekijkt, 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.
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.