Net-Base Magasin

03.06.2026

Responsive layouter i Delphi FMX: Breakpoints uten designer-kaos (med layout-router som kildekodeeksempel)

Responsive Layouts FMX blir i praksis raskt skjøre: resize‑stormer, DPI‑endringer, rotasjon og „Visible‑Layouts“ skaper dobbel tilstand og vanskelig å debugge reflows. Dette innlegget viser en layout‑router med Breakpoints som kontrollerer UI‑blokker i kjøretid...

03.06.2026

Fra magasinetema til prosjektpraksis

Egnede tjeneste- og tekniske sider for innlegget

Når man i Delphi FireMonkey må støtte flere formfaktorer, ender man fort opp med Responsive Layouts FMX – og like raskt i en blanding av Align-kaskader, skjulte layout-containere og Designer-workarounds som kollapser ved neste DPI- eller rotasjonsendring. I modnet forretningsprogramvareklienter er dette spesielt ubehagelig: UI-en videreutvikles, team skiftes, og plutselig ligger logikk bundet til visuelle detaljer.

Kjernen i problemet: FireMonkey tilbyr mange byggeklosser (f.eks. Align, Anchors, TScaledLayout, TFlowLayout, TGridPanelLayout), men ikke et «naturlig» Breakpoint-system som på weben. Man kan selvfølgelig reagere på størrelsesendringer, men uten klar arkitektur ender det i «if Width < … then …» spredt over mange Forms.

Denne artikkelen viser en Layout-Router: en liten komponent som administrerer Breakpoints sentralt og henger Controls (eller hele layout-blokker) om mellom forhåndsdefinerte Slots. Mål: tilstander bevares, koden er vedlikeholdbar, og kanttilfeller som rotasjon, nestede layouts og re-entrancy blir dempet. I tillegg noen mindre åpenbare knep som i praksis utgjør forskjellen mellom «kjører i demo» og «kjører stabilt i drift».

Hvorfor Breakpoints i FMX er annerledes enn på weben

I web-oppsett er Breakpoints ofte deklarative (CSS Media Queries). I FMX er layoutbeslutninger typisk imperative ved kjøring: Man bytter oppsett i OnResize. I tillegg kommer plattformspecifikke særegenheter:

  • Device-piksler vs. logiske piksler: ClientWidth/ClientHeight er i logiske enheter (avhengig av skalering). DPI-endringer (f.eks. Windows Per-Monitor-DPI) kan trigge layout på nytt uten at «fysisk» noe endrer seg.
  • Rotasjon og Safe Areas: Mobile plattformer leverer insets (Notch/Safe Area) – avhengig av OS og enhet. Et «breakpoint kun basert på bredde» er ofte for snevert, fordi den brukbare flaten kan være mindre enn vindusstørrelsen.
  • Layout-pass: FireMonkey beregner layouter i faser. Hvis man endrer Parent/Align i feil øyeblikk, oppstår bivirkninger (f.eks. dobbelt reflow eller flimrende størrelser).

En Layout-Router adresserer dette ved å (1) frikoble «når» (Resize/Scale/Rotation) fra «hvordan» (layout-regler) og (2) konsentrere reglene på ett sted. For tekniske ledere er den viktigste effekten: De får et klart, verifiserbart beslutningssenter i stedet for mange lokale spesialtilfeller.

Arkitektur: Layout-Router med Slots i stedet for å opprette Controles

Det ryddige trikset for FMX: ikke dynamisk gjenopprette Controls, men flytte eksisterende Controls mellom Slots. Et Slot er enkelt et container (f.eks. TLayout) som representerer et område av UI-en: sidepanel, verktøylinje, innhold, bunntekst, detaljrute.

Fordeler i individuell bedriftsprogramvare:

  • Tilstander bevares (innhold i redigeringsfelt, rulleposisjon, valgte elementer), fordi instanser ikke bygges opp på nytt.
  • Mindre risiko for dobbel registrering av hendelser, timere eller bindings.
  • Layout-regler blir synlige: «hvilken blokk ligger i hvilket Slot» kan for hver Breakpoint følges og gjennomgås.

Viktig i praksis: Del inn UI-blokkene grovt nok. Hvis du flytter 30 enkelte kontroller, blir rute-listen selv en feilkilde. Bedre er containere som layFilterBar, layNavigation, layResultList, layDetails.

Kildeutdrag: Breakpoint-router for responsive oppsett i FMX

Følgende kode er ment som en hjelpeenhet du kan bruke i FMX-Forms. Den beregner et breakpoint (XS/SM/MD/LG/XL) og flytter definerte Controls inn i definerte slot-containere. Viktige detaljer:

  • Debounce via TThread.ForceQueue: flere Resize-Events blir slått sammen til én oppdatering (mindre UI-risting, færre reflow-løkker).
  • Re-Entrancy-beskyttelse: Layout-oppdatering utløser ofte igjen Resize/Layout.
  • Valgfritt: Orientering (Portrait/Landscape) kan inngå i breakpoint-logikken.

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

// En mapping: hvilket Control skal plasseres i hvilken slot (container) for et breakpoint.
TNBRoute = record
Control: TControl;
TargetSlot: TControl; // typisk TLayout eller 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; // manuelt beregne på nytt
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 må ikke være 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: kun én kjøring per meldingssløyfe
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;

// Merk: Parent-bytte endrer Z-orden.
// Hvis rekkefølge er viktig, kall DefineRoute i ønsket rekkefølge.
for LRoute in LList do
begin
if (LRoute.Control.Parent <> LRoute.TargetSlot) then
LRoute.Control.Parent := LRoute.TargetSlot;

// Sett Align først etter Parent, ellers kan Bounds bli tolket annerledes.
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 bevisst grove, siden FMX-målplattformer varierer mye.
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.

Slik bruker du routeren i et skjema

Du definerer slots som TLayout (f.eks. layTop, layLeft, layContent) og registrerer deretter per breakpoint hvor hvilke blokker skal ligge. Typisk er at sidebar og detaljpanelet i små breakpoints flyttes under hverandre.

Delphi
procedure TFrmMain.FormCreate(Sender: TObject);
begin
  FRouter := TNBLayoutRouter.Create(Self);
  FRouter.AttachRoot(LayoutRoot); // f.eks. et TLayout som er Client-aligned

  // XS: alt under hverandre
  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 til venstre, Details til høyre
  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);

  // Valgfritt: egen breakpoint-logikk
  FRouter.OnGetBreakpoint :=
    function(const S: TSizeF): TNBLayoutBreakpoint
    begin
      if (S.cx < 700) or (S.cy < 420) then
        Result := bpSM
      else
        Result := bpMD;
    end;
end;

Plassering: Hvorfor «omhengning» ofte er mer stabilt enn å bytte Visible

En vanlig tilnærming er å holde separate layout-trær for hver variant og bare togg­le Visible. Det føles praktisk i designeren, men gir typiske bivirkninger:

  • Dobbel binding/Events: To like controls må holdes synkronisert (f.eks. to filterlinjer).
  • Tab-rekkefølge og fokus: Ved bytte mister man ofte fokus eller ender opp i usynlige controls hvis TabStop/HitTest er ugunstig satt.
  • State drift: Rulleposisjoner, seleksjonstilstander eller redigerte tekster divergerer.

Omhengning beholder én entydig instans. Viktig er å dele opp layout-blokkene slik at de kan flyttes uavhengig (f.eks. «Sidebar» som egen container i stedet for mange enkeltcontrols). Dette lønner seg i vedlikehold og feilsøking: Du debugger én instans, ikke to parallelle skygge-UIer.

Fallgruver i praksis (og hvordan du feilsøker dem)

1) Resize-stormer og reentrancy

FMX trigger OnResize ikke bare ved brukerendring, men også ved stilbytter, parent-endringer og delvis ved DPI-endringer. Uten debounce kan appen henge i layout-sløyfer. Routeren bruker TThread.ForceQueue for å skyve endringene til neste UI-tick.

Feilsøkningstips: Logging (f.eks. via OutputDebugString) med breakpoint, størrelse og en update-counter hjelper med å finne reflow-sløyfer. Hvis du i tillegg logger tidspunktet når ApplyRoutes starter og slutter, ser du raskt om en enkelt resize «kaskaderer».

2) Z-Order, HitTest og «usynlige» klikksperrer

Parent-bytt endrer Z-Order. Hvis overlays (f.eks. flyouts) ikke lenger mottar klikk, skyldes det ofte at en Client-aligned container ligger over dem og at HitTest er aktiv. Variante: For overlay-områder reserver et eget slot øverst og parent slike controls kun der. I FMX er HitTest (om et control fanger opp mus-/touch-hendelser) oftere årsaken enn synligheten.

3) TGridPanelLayout og prosentvise størrelser

TGridPanelLayout kan ved prosentbaserte kolonner/rader i kombinasjon med Align=Client og dynamisk omparentering utløse uventede nyberegninger. Hvis du må bruke Grid, plasser Grid i en slot, og flytt kun hele Grid-blokker, ikke Grid-barn. Det reduserer kombinasjonen av layout-passene.

4) Fokus, virtuelt tastatur og «hoppende» inndatfelt

Et hjørnetilfelle som opptrer i mobile FMX-apper og også på Windows-tabletter: Ved omparentering kan et fokusert Edit-kontroll kortvarig miste Parent. Dette kan lukke det virtuelle tastaturet eller tilbakestille markøren. Erfaring viser: Ta vare på gjeldende fokus før Routing (Focused/IFMXFocusControl), og gjenopprett fokus etter Routing (i samme UI-tick). Dette lønner seg særlig for inndataskjemaer som veksler mellom «tospaltet» (Tablet/PC) og «enspaltet» (Phone).

Varianter: Breakpoints etter formfaktor i stedet for bare etter bredde

I reelle multiplattform-klienter er «bredde» alene ofte ikke riktig signal. Fornuftige varianter:

  • Bredde og høyde: svært flate vinduer (f.eks. kasse-terminaler, delte skjermer) trenger andre regler.
  • Orientering: Landscape på tablets er ofte «desktop-lignende», Portrait heller «mobile-lignende».
  • Safe-Area-brukbar flate: På iOS/Android kan den effektiv brukbare høyden krympe betydelig på grunn av systemlinjer. Den som bare ser på Height, ruter noen ganger «for sent».

Routeren er bevisst bygget slik at du kan bytte ut breakpoint-funksjonen. Det er også nyttig i legacy-situasjoner, når samme form kjører i flere hosts (f.eks. én gang som normalt vindu, én gang i en innebygd container).

Uvanlig ryddig: Layout-routing som «transaksjon»

I større skjermer handler problemet mindre om selve breakpoints enn om rekkefølgen av UI-operasjonene. Et praksisnært mønster er å behandle routing som en transaksjon: først ta avgjørelse, deretter omparentere, så utføre sideeffekter (Visibility, Fokus, datarefresh) i ordnet rekkefølge.

Konkret betyr det: Unngå at enkeltkontroller under omparentering utløser egne hendelser som igjen starter layout eller data-tilgang. I FMX skjer dette for eksempel når et Parent-skifte utløser OnEnter/OnExit eller når et LiveBinding-uttrykk reevalueres på grunn av et Bounds-Update. Hvis du ser slike effekter, hjelper en sentral «Updating»-bryter (som i Routeren) pluss et tydelig post-steg: Først etter ApplyRoutes får kostbare operasjoner kjøre (f.eks. laste liste på nytt, binde detaljvisning).

Spesielt for klienter med REST-tilgang er dette relevant: En uønsket reload under en resize kan føre til unødvendige forespørsler. Det merkes ikke i LAN, men i VPN eller på mobil med én gang.

Når tilnærmingen lønner seg – og hvor den har grenser

Layout-routeren lønner seg når:

  • en FMX-applikasjon lever videre over år, og flere utviklere arbeider på de samme skjermene,
  • UI-blokker kan skilles klart (Sidebar/Details/Content),
  • du trenger reproduserbare breakpoint-regler, i stedet for ad-hoc Align-tuning.

Begrensninger blir tydelige når en skjerm må være svært „fluid“ (mange dynamiske fliser, ekte Masonry-oppsett). Da er TFlowLayout/TGridPanelLayout eller egendefinerte layout-klasser mer hensiktsmessige. Hvis svært mange enkeltkontroller bytter mellom slots, blir vedlikeholdet av rutene uoversiktlig – da er det bedre å dele opp i større blokker eller innføre et deklarativt konfigurasjonslag (f.eks. en JSON-konfigurasjon for slot-tilordninger som lastes ved oppstart).

Konklusjon: For responsive oppsett i FMX er „omkobling med breakpoints“ en pragmatisk mellomløsning: mindre designer-kaos, klare regler, stabile tilstander. Den erstatter ikke en gjennomtenkt UI-struktur, men gir deg et robust rammeverk for å videreutvikle FMX-klienter i digitale virksomhetsløsninger på tvers av formfaktorer på en kontrollert måte.

Hvis du i en eksisterende Delphi- eller FMX-applikasjon ønsker å etterføre en slik layout-arkitektur på en ryddig måte, uten å risikere UI-regresjoner i driftsituasjoner, kan du gjerne avstemme det teknisk med oss: drøft prosjekt eller moderniseringsprosjekt med Net-Base.

I faglig sammenheng spiller også Delphi Fmx Breakpoints og Firemonkey Layout en viktig rolle når integrasjoner, dataflyter og videreutvikling må samspille på en ryddig måte.

Drøft prosjekt eller moderniseringsprosjekt med Net-Base.

Neste steg

Når et tema blir et reelt prosjekt, bør arkitektur, eksisterende systemer og drift tidlig vurderes samlet.

Vi bistår ikke bare med enkeltspørsmål, men også når kodesnutter, legacy-temaer eller portalideer skal utvikles til et robust virksomhetsprosjekt.

  • Eksisterende tilstand, målbildet og tekniske risikoer vurderes samlet.
  • REST, datatilgang, portaler og utrulling blir ikke utsatt som sene følger.
  • Dere ser tidlig hvilken vei som er økonomisk og driftsmessig levedyktig.

Del innlegg

Del dette innlegget direkte

LinkedIn, X, XING, Facebook, WhatsApp og e‑post er umiddelbart tilgjengelig. For Instagram forbereder vi lenke og kort tekst umiddelbart.

E-post

Instagram åpnes i en ny fane. Lenken og kortteksten kopieres først til utklippstavlen.