Net-Base Magasin

03.06.2026

Responsivt oppsett i Delphi FMX: Breakpoints utan kaos i designaren (med Layout-Router som kjeldesnutt)

Responsive FMX-layouts blir i praksis raskt skjøre: resize-stormar, DPI-byttar, rotasjon og «Visible-Layouts» skapar dobbel tilstand og vanskelege å feilsøkje reflows. Denne artikkelen viser ein Layout-Router med Breakpoints som kontrollerer UI-blokker ved køyring...

03.06.2026

Frå magasinetema til prosjektpraksis

Passande teneste- og tekniske sider til innlegget

Den som i Delphi FireMonkey må støtte fleire formfaktorar, endar raskt opp med Responsive Layouts FMX – og like raskt i ein blanding av Align-kaskadar, skjulte layout-containerar og designer-workarounds som raknar ved neste DPI- eller rotasjonsendring. I etablerte business-programklientar er dette særleg ubehageleg: UI-en blir vidareutvikla, team skiftar, og plutseleg heng logikk i visuelle detaljar.

Kjernen i problemet: FireMonkey tilbyr mange byggeklossar (t.d. Align, Anchors, TScaledLayout, TFlowLayout, TGridPanelLayout), men ikkje eit «naturlig» Breakpoint-System som på weben. Ein kan visst reagere på storleiksendringar, men utan klar arkitektur endar det i «if Width < … then …» spreidd over mange Forms.

Denne artikkelen viser ein Layout-Router: ein liten komponent som handterer Breakpoints sentralt og heng Controls (eller heilheitlege layout-blokkar) mellom førehandsskisserte Slots. Målet: tilstandar blir verande, koden er vedlikehaldbar, og randtilfelle som rotasjon, nestede layout og Re-Entrancy blir dempa. I tillegg nokre mindre åpenbare knep som i praksis skil «kjører i demo» frå «kjører stabilt i drift».

Kvifor Breakpoints i FMX er annleis enn på weben

I web-oppsett er Breakpoints oftast deklarative (CSS Media Queries). I FMX er layoutval typisk imperative ved kjøretid: Ved OnResize blir ting bytta. I tillegg kjem plattformspesifikke eigenskapar:

  • Device-Pixel vs. logiske Pixel: ClientWidth/ClientHeight er i logiske einingar (avhengig av skalering). DPI-skifte (t.d. Windows Per-Monitor-DPI) kan trigge layout på nytt utan at noko «fysisk» har endra seg.
  • Rotasjon og Safe Areas: Mobile plattformar leverer Insets (Notch/Safe Area) – avhengig av OS og Device. Ein «Breakpoint berre etter breidde» er ofte for snever, fordi den brukbare flata kan vere mindre enn den reine vindaugstorleiken.
  • Layout-Pass: FireMonkey reknar ut layout i fasar. Om ein endrar Parent/Align i feil augneblink, oppstår biverknadar (t.d. meirfaldig reflow eller flakkande storleikar).

Ein Layout-Router adresserer dette ved å (1) skilja «når» (Resize/Scale/Rotation) frå «korleis» (layout-reglar) og (2) samla reglane på éin stad. For tekniske leiarar er den viktigaste effekten: Dei får eit klart, etterprøvbart beslutningssenter i staden for mange lokale unntakstilfelle.

Arkitektur: Layout-Router med Slots i staden for å opprette Controls

Den reine knepen for FMX: ikkje dynamisk å gjenopprette Controls, men å flytte eksisterande Controls mellom Slots. Ein Slot er enkelt ein container (t.d. TLayout) som representerer eit område av UI-en: Sidebar, Toolbar, Content, Footer, Details-Pane.

Fordelar i skreddarsydd bedriftsprogramvare:

  • Tilstandar blir verande (Edit-Text, Scrollposition, selekterte Items), fordi instansar ikkje blir bygd opp på nytt.
  • Lågare risiko for dobbel oppkopling av Events, timarar eller Bindings.
  • Layout-reglane blir synlege: «kva blokk ligg i kva Slot» kan etterprøvast per Breakpoint og gjennomgåast.

Viktig i praksis: Del UI-blokker inn i grove einingar. Om ein flyttar 30 individuelle Controls, blir sjølve rutelista ei feilkjelde. Det er betre med containerar som layFilterBar, layNavigation, layResultList, layDetails.

Kodeeksempel: Breakpoint-router for responsivt oppsett i FMX

Følgjande kode er meint som ein hjelpekomponent du kan bruke i FMX-Forms. Den bereknar ein Breakpoint (XS/SM/MD/LG/XL) og hengjer definerte Controls i definerte slot-container. Viktige detaljar:

  • Debounce über TThread.ForceQueue: fleire Resize-Events blir samla til ein oppdatering (mindre UI-flimring, færre reflow-løkker).
  • Re-Entrancy-Schutz: Layout-Update triggar ofte på nytt Resize/Layout.
  • Optional: Orientierung (Portrait/Landscape) kan inngå i breakpoint-logikken.
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);

  // Eit mapping: kva Control skal plasserast i kva Slot (Container) for ein 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 rekalkulere
    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å ikkje vere 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: berre éin gong per message-loop
  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;

    // Obs: byte av Parent endrar Z-orden.
    // Om rekkefølgja er viktig, kall DefineRoute i ønskja rekkefølgje.
    for LRoute in LList do
    begin
      if (LRoute.Control.Parent <> LRoute.TargetSlot) then
        LRoute.Control.Parent := LRoute.TargetSlot;

      // Set Align fyrst etter Parent, elles kan Bounds eventuelt bli tolka annleis.
      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 er medvite grove, fordi FMX-målplattformene varierer sterkt.
  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 brukar du routeren i ein Form

Du definerer slotar som TLayout (t.d. layTop, layLeft, layContent) og registrerer deretter per breakpoint kvar blokk skal liggja. Vanleg er at sidepanelet og detaljpanelet ved små breakpoints flyt under kvarandre.

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;

Kontekst: Kvifor «omhenging» ofte er meir stabilt enn Visible-Schalten

Ein utbreidd tilnærming er å halde separate layout-tre for kvar variant og berre toggla Visible. Det verkar praktisk i designaren, men har typiske biverknader:

  • Dobbelt binding/hendingar: To like controls må haldast synkroniserte (t.d. to filterreiar).
  • Tab-Reihenfolge und Fokus: Ved omkopling mistar ein ofte fokus eller hamnar i usynlege controls dersom TabStop/HitTest står uheldig.
  • Tilstandsdrift: Rulleposisjonar, seleksjonstilstandar eller redigerte tekstar kan divergere.

Omhenginga held instansen entydig. Det er viktig å skjera layout-blokker slik at dei kan flyttast uavhengig (t.d. «Sidebar» som eigen container i staden for mange enkeltcontrols). Dette løner seg i vedlikehald og feilfinning: du feilsøker éin instans, ikkje to parallelle skugge-UIar.

Stolpar i praksis (og korleis ein feilsøker dei)

1) Resize-stormar og reentrancy

FMX triggar OnResize ikkje berre ved brukarendring av storleik, men òg ved style-byt, parent-endringar og av og til ved DPI-endringar. Utan debounce heng appen i layout-løkker. Routeren nyttar TThread.ForceQueue for å flytte endringane til neste UI-tikk.

Feilsøkingstips: Logging (t.d. via OutputDebugString) med breakpoint, storleik og ein oppdaterings-teller hjelper å finne reflow-løkker. Om du i tillegg loggar tidspunktet då ApplyRoutes startar og sluttar, ser du raskt om ein enkelt resize kaskaderer.

2) Z-Order, HitTest und «usynlege» klikkblokkerar

Parent-endringar endrar Z-Order. Dersom overlays (t.d. Flyouts) ikkje lenger tek klikk, skuldast det ofte at ein client-aligned container ligg over og HitTest er aktiv. Variante: For overlay-område reserver ein bevisst ein separat slot heilt øvst og parentar berre slike controls der. I FMX er HitTest (om eit control fanger opp mus-/touch-hendingar) ofte årsaka meir enn synlegheit.

3) TGridPanelLayout und prozentuale Größen

TGridPanelLayout kan ved prosentvise kolonnar/rader i kombinasjon med Align=Client og dynamisk omkopling utløse uventa rekalkulasjonar. Hvis du må bruke Grid, plasser Grid i ein Slot, og flytt berre heile Grid-blokkene, ikkje Grid-borna. Det reduserer kombinatorikken i layout-passa.

4) Fokus, virtuelle tastatur og «hoppande» inndatofelt

Ein randtilfelle som skjer i mobile FMX-appar og òg på Windows-tabletar: Ved omkopling kan eit fokusert Edit-Control kortvarig miste Parent. Det kan lukke det virtuelle tastaturet eller tilbakestille markøren. Ein praktisk løysing er å mellomlagre gjeldande fokus før Routing (Focused/IFMXFocusControl) og gjenopprette fokus etter Routing (i same UI-tick). Dette gir spesielt god nytte for inndataskjema som vekslar mellom «tospalta» (Tablet/PC) og «einspalta» (telefon).

Variantar: Breakpoints etter formfaktor i staden for berre etter breidd

I reelle multiplattform-klientar er «breidd» åleine ofte ikkje det rette signalet. Moglege variantar:

  • Breidd og høgd: svært flate vindauge (t.d. kasse-terminalar, delte skjermer) treng andre reglar.
  • Orientering: Landscape på tabletar er ofte «desktop-liknande», portrait heller «mobil-liknande».
  • Safe-Area-nytteflate: På iOS/Android kan den effektive brukbare høgda krympe mykje grunna systemlinjer. Dei som berre ser på Height, routar av og til «for seint».

Routeren er medvite bygd slik at du kan bytte ut Breakpoint-funksjonen. Det er nyttig i legacy-situasjonar når same form køyrer i fleire hosts (t.d. ein gong som normalt vindauge, ein gong i ein innebygd container).

Uvanleg ryddig: Layout-Routing som «Transaksjon»

På større skjermar heng problemet mindre ved breakpointa sjølve og meir ved rekkefølgja av UI-operasjonar. Eit praksisreint mønster er å handsame Routing som ein transaksjon: først avgjere, så koble om, deretter utføre bieffektar (Visibility, fokus, dataoppfrisking) i ordna rekkje.

Konkret inneber dette: Unngå at einskilde controls under omkoplinga utløyser eigne hendingar som igjen startar layout- eller datatilgang. I FMX skjer dette til dømes når Parent-skifte utløser OnEnter/OnExit eller når eit LiveBinding-uttrykk blir re-evaluert av eit Bounds-oppdatering. Om du ser slike effektar, hjelp ein sentral «Updating»-brytar (som i Routeren) pluss eit klart post-steg: Først etter ApplyRoutes får kostbare operasjonar kjøre (t.d. laste lista på nytt, binde detaljvising).

Særleg for klientar med REST-tilgang er dette relevant: Eit uønskt reload under eit resize kan føre til unødige førespurslar. Det merkar du ikkje i LAN, men i VPN eller på mobilnett straks.

Når tilnærminga løner seg – og kvar ho har grenser

Layout-routeren løner seg når:

  • ein FMX-applikasjon lever vidare i fleire år og fleire utviklarar jobbar på dei same skjermane,
  • UI-blokker kan delast klart (Sidebar/Details/Content),
  • du treng reproduserbare Breakpoint-reglar i staden for ad-hoc Align-tilpassing.

Grensene blir synlege når ein skjerm må vere sterkt «fluid» (mange dynamiske flisar, ekte Masonry-layouts). Då er TFlowLayout/TGridPanelLayout eller eigne layout-klasser meir eigna. Også når svært mange enkeltkontroller vekslar mellom slotar, blir vedlikehald av rutene uoversiktleg – då er det betre å dele opp i større blokkar eller innføre eit deklarativt konfigurasjonslag (t.d. ei JSON-konfigurasjon for slot-tilordningar som blir lasta ved oppstart).

Konklusjon: For responsive layout i FMX er «omkopling med Breakpoints» ein pragmatisk mellomting: mindre designer-kaos, klare reglar, stabile tilstandar. Det erstattar ikkje ei gjennomtenkt UI-struktur, men det gir dykk eit robust rammeverk for å vidareutvikle FMX-klientar i digitale bedriftsløysingar kontrollert på tvers av formfaktorar.

Hvis de i ei eksisterande Delphi- eller FMX-applikasjon ønskjer å implementere ei slik layout-arkitektur på ein ryddig måte, utan å risikere UI-regresjonar i driftsscenarier, kan de gjerne teknisk drøfte det med oss: prosjekt eller moderniseringsoppdrag med Net-Base.

I fagleg samanheng spelar også Delphi Fmx Breakpoints og Firemonkey Layout ei viktig rolle når integrasjonar, dataflyt og vidareutvikling må fungere godt saman.

Drøfte prosjekt eller moderniseringsoppdrag med Net-Base.

Neste steg

Når temaet blir eit reelt prosjekt, bør arkitektur, eksisterande system og drift vurderast tidleg saman.

Vi støttar ikkje berre ved enkeltspørsmål, men òg når korte kildekodesnuttar, legacy-tema eller portalidéar skal utviklast til eit robust bedriftsprosjekt.

  • Eksisterande tilstand, målbiletet og tekniske risikoar blir vurderast samla.
  • REST, datatilgang, portalar og utrulling blir ikkje utsette til seinare som etterverknader.
  • De ser tidleg kva veg som er økonomisk og driftsmessig berekraftig.

Del innlegg

Del dette innlegget direkte

LinkedIn, X, XING, Facebook, WhatsApp og e-post er tilgjengelege med ein gong. For Instagram klargjer vi lenke og kort tekst med det same.

E-post

Instagram opnar i ein ny fane. Lenkje og kort tekst blir kopiert til utklippstavla på førehand.