Net-Base Magasin

03.06.2026

Responsiva layouter i Delphi FMX: Breakpoints utan designerkaos (med Layout-Router som källkodsexempel)

Responsiva FMX-layouts blir i praktiken snabbt sköra: intensiva storleksändringar (resize-stormar), DPI‑växlingar, rotation och „Visible-Layouts“ skapar dubbla tillstånd och svårdebuggade reflows. Den här artikeln visar en layout‑router med breakpoints som kontrollerar UI‑block under körning...

03.06.2026

Från magasinets tema till projektpraxis

Passande tjänste- och tekniksidor för inlägget

Den som i Delphi FireMonkey måste hantera flera formfaktorer hamnar snabbt vid Responsive Layouts FMX – och lika snabbt i en blandning av Align-kaskader, dolda layout-containrar och designer-workarounds som fallerar vid nästa DPI- eller rotationsbyte. I befintliga affärsprogramsklienter är det särskilt besvärligt: UI:n vidareutvecklas, team byts ut, och plötsligt hänger logik på visuella detaljer.

Kärnan i problemet: FireMonkey erbjuder många byggstenar (t.ex. Align, Anchors, TScaledLayout, TFlowLayout, TGridPanelLayout), men inget „nativt“ Breakpoint-System som på webben. Man kan visserligen reagera på storleksändringar, men utan tydlig arkitektur slutar det i „if Width < … then …“ utspritt över många Forms.

Det här inlägget visar en Layout-Router: en liten komponent som hanterar Breakpoints centralt och flyttar Controls (eller hela layout-block) mellan förberedda Slots. Målet: tillstånd bevaras, koden blir underhållbar, och kantfall som rotation, nästlade layouter och Re-Entrancy dämpas. Dessutom några mindre uppenbara knix som i praktiken skiljer „fungerar i demo“ från „fungerar stabilt i drift“.

Varför Breakpoints i FMX skiljer sig från webben

I webblayouter är Breakpoints oftast deklarativa (CSS Media Queries). I FMX är layoutbeslut vid körning typiskt imperativa: vid OnResize byts det. Därtill kommer plattformsspecifika särdrag:

  • Device-pixlar vs logiska pixlar: ClientWidth/ClientHeight är i logiska enheter (beroende av skalning). DPI-växlingar (t.ex. Windows Per-Monitor-DPI) kan trigga om layouter utan att något „fysiskt“ ändras.
  • Rotation och Safe Areas: Mobila plattformar levererar Insets (Notch/Safe Area) – beroende på OS och enhet. Ett „Breakpoint som endast baseras på bredd“ är ofta för snävt, eftersom den användbara ytan är mindre än fönsterstorleken.
  • Layout-Pass: FireMonkey beräknar layouter i faser. Om man ändrar Parent/Align vid fel tidpunkt uppstår sidoeffekter (t.ex. upprepade reflow eller fladdrande storlekar).

En Layout-Router adresserar detta genom att (1) avkoppla det „När“ (Resize/Scale/Rotation) från det „Hur“ (Layout-regler) och (2) koncentrera reglerna på en plats. För tekniska Leads är den viktigaste effekten: De får ett tydligt, verifierbart beslutscentrum istället för många lokala specialfall.

Arkitektur: Layout-Router med Slots istället för Control-Erzeugung

Den rena knepen för FMX: inte dynamiskt skapa Controls på nytt, utan flytta befintliga Controls mellan Slots. En Slot är helt enkelt en container (t.ex. TLayout) som representerar ett område av UI:t: Sidebar, Toolbar, Content, Footer, Details-Pane.

Fördelar i skräddarsydd företagsprogramvara:

  • Tillstånd bevaras (Edit-Text, Scrollposition, valda objekt), eftersom instanser inte byggs om.
  • Mindre risk för dubbla kopplingar av Events, Timers eller Bindings.
  • Layout-regler blir synliga: „vilket Block ligger i welchem Slot“ kan för varje Breakpoint följas och granskas.

Viktigt i praktiken: dela upp UI-blocken grovt nog. Om du flyttar 30 enskilda kontroller blir själva ruttlistan en felkälla. Bättre är containrar som layFilterBar, layNavigation, layResultList, layDetails.

Källkodsexempel: Breakpoint-Router för responsiva layouter i FMX

Följande kod är avsedd som en hjälpenhet som du kan använda i FMX-formulär. Den beräknar en Breakpoint (XS/SM/MD/LG/XL) och flyttar definierade kontroller till definierade slot-containrar. Viktiga detaljer:

  • Debounce över TThread.ForceQueue: flera Resize-händelser slås ihop till en uppdatering (mindre UI-flimmer, färre reflow-loopar).
  • Re-Entrancy-Schutz: Layoutuppdateringar triggar ofta i sin tur ytterligare Resize/Layout.
  • Optional: Orientierung (stående/liggande) kan ingå i breakpoint-logiken.
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);

  // En mappning: vilken Control ska placeras i vilken slot (container) för en 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; // manuell omberäkning
    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 får inte vara 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: tillämpas endast en gång 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;

    // Observera: byte av Parent ändrar Z-ordning.
    // Om ordningen är viktig, anropa DefineRoute i önskad ordning.
    for LRoute in LList do
    begin
      if (LRoute.Control.Parent <> LRoute.TargetSlot) then
        LRoute.Control.Parent := LRoute.TargetSlot;

      // Sätt Align först efter att Parent är satt, annars kan Bounds eventuellt tolkas annorlunda.
      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 avsiktligt grova, eftersom FMX-målplattformarna varierar kraftigt.
  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.

Så använder du routern i ett formulär

Du definierar slots som TLayout (t.ex. layTop, layLeft, layContent) och registrerar sedan per breakpoint var vilka block ska ligga. Typiskt är att Sidebar och Details-Pane i små breakpoints flyttar under varandra.

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;

Placering: Varför att byta förälder ofta är mer stabilt än att växla Visible

En vanlig strategi är att ha separata layouträd för varje variant och bara växla Visible. Det är bekvämt i designern, men det ger typiska bieffekter:

  • Dubbla bindningar/ändelser: Två liknande kontroller måste hållas synkroniserade (t.ex. två filterrader).
  • Tab-ordning och fokus: Vid växling tappar man fokus eller hamnar i osynliga kontroller om TabStop/HitTest är illa inställda.
  • State Drift: Scrollpositioner, selektionsstatus eller redigerade texter kan glida isär.

Att byta förälder bevarar instansen entydigt. Viktigt är att dela upp layoutblocken så att de kan flyttas oberoende (t.ex. „Sidebar“ som egen container istället för många enskilda kontroller). Det lönar sig i underhåll och felsökning: du debuggar en instans, inte två parallella skugguppsättningar.

Fallgropar i praktiken (och hur man debuggar dem)

1) Resize-stormar och re-entrancy

FMX triggar OnResize inte bara vid användarändring, utan även vid stilbyten, föräldraändringar och delvis vid DPI-ändringar. Utan debounce kan appen fastna i layout-loopar. Routern använder TThread.ForceQueue för att skjuta ändringarna till nästa UI-tick.

Debugging-tips: Loggning (t.ex. via OutputDebugString) med breakpoint, storlek och en uppdateringsräknare hjälper till att hitta reflow-loopar. Om du dessutom loggar tidpunkten då ApplyRoutes startar och slutar ser du snabbt om en enskild resize kaskaderar.

2) Z-Order, HitTest och „osynliga“ klick-hinder

Byten av parent ändrar Z-order. Om overlays (t.ex. flyouts) inte längre går att klicka på beror det ofta på att en client-aligned container ligger ovanpå och HitTest är aktiv. Alternativ: reservera uttryckligen en separat slot högst upp för overlay-ytor och parenta sådana kontroller där. I FMX är HitTest (om en kontroll fångar mus-/touch-händelser) oftare orsaken än synlighet.

3) TGridPanelLayout och procentuella Größen

TGridPanelLayout kan vid procentuella kolumner/rader i kombination med Align=Client och dynamisk omhängning orsaka oväntade omberäkningar. Om du måste använda Grid, placera Grid i en slot och flytta endast hela Grid-block, inte Grid-barnen. Det minskar kombinationerna av layout-pass.

4) Fokus, virtuellt tangentbord och ‚hoppande‘ inmatningsfält

Ett specialfall som förekommer i mobila FMX-appar och även på Windows-tablets: vid omhängning kan ett fokuserat Edit-Control kortvarigt förlora sin Parent. Det kan stänga det virtuella tangentbordet eller återställa markören. Praktiskt har följande visat sig fungera: innan routing mellanlagra aktuell fokus (Focused/IFMXFocusControl), efter routing (i samma UI-tick) återställa fokus. Detta lönar sig särskilt för inmatningsformulär som växlar mellan ‚tvåspaltigt‘ (Tablet/PC) och ‚enspaltigt‘ (Phone).

Varianter: Breakpoints efter formfaktor istället för bara efter bredd

I verkliga multiplattforms‑klienter är „bredd“ ensam ofta inte rätt signal. Meningsfulla varianter:

  • Bredd och höjd: mycket platta fönster (t.ex. kassa‑terminaler, delade skärmar) behöver andra regler.
  • Orientering: Landscape på tablets är ofta „desktop‑lik“, Portrait snarare „mobil‑lik“.
  • Safe‑Area‑använd yta: På iOS/Android kan den effektivt användbara höjden krympa avsevärt på grund av systemfält. Den som bara tittar på Height routar ibland „för sent“.

Routern är medvetet konstruerad så att du kan byta ut breakpoint‑funktionen. Det är också användbart i legacy‑situationer när samma form körs i flera hosts (t.ex. en gång som normalt fönster, en gång i en inbäddad container).

Ovanligt rent: Layout‑Routing som ‚transaktion‘

På större skärmar handlar problemet mindre om breakpoints i sig än om ordningen för UI‑operationerna. Ett praktiskt mönster är att behandla routing som en transaktion: först besluta, sedan omhänga, därefter utföra sidoeffekter (synlighet, fokus, datauppdatering) i ordnad följd.

Konkret innebär det: undvik att enskilda kontroller under omhängningen triggar egna events som i sin tur startar layout eller dataåtkomst. I FMX händer detta till exempel när OnEnter/OnExit utlöses vid Parent‑byte eller när ett LiveBinding‑uttryck återutvärderas genom en bounds‑uppdatering. Om du ser sådana effekter hjälper en central „Updating“‑brytare (som i routern) plus ett tydligt post‑steg: först efter ApplyRoutes får dyra operationer köras (t.ex. ladda om en lista, binda detaljvy).

Särskilt för klienter med REST‑åtkomst är detta relevant: en oönskad omladdning under en ändring av storlek kan leda till onödiga förfrågningar. Det märks inte i LAN, men i VPN eller mobilt omedelbart.

När tillvägagångssättet är värt det – och var det har begränsningar

Layout‑routern är värd insatsen när:

  • en FMX‑applikation lever vidare över flera år och flera utvecklare arbetar på samma skärmar,
  • UI‑block kan tydligt separeras (Sidebar/Details/Content),
  • du behöver reproducerbara breakpoint‑regler istället för ad‑hoc Align‑tuning.

Begränsningar blir tydliga när en skärm måste vara mycket „fluid“ (många dynamiska kacheln, äkta Masonry-Layouts). Då är TFlowLayout/TGridPanelLayout eller egna layout‑klasser mer lämpliga. Även när väldigt många enskilda kontroller byter mellan slots blir underhållet av rutterna oöverskådligt – då är det bättre att dela upp i större block eller införa ett deklarativt konfigurationslager (t.ex. en JSON‑konfiguration för slot‑tilldelningar som läses in vid start).

Slutsats: För responsiva FMX‑layouts är „omhängning med breakpoints“ en pragmatisk mellanväg: mindre designer‑kaos, tydliga regler, stabila tillstånd. Det ersätter ingen genomtänkt UI‑struktur, men det ger er ett robust ramverk för att kontrollerat vidareutveckla FMX‑clients i digitala företagslösningar över formfaktorer.

Om ni i en befintlig Delphi‑ eller FMX‑applikation vill införa en sådan layoutarkitektur på ett ordnat sätt utan att riskera UI‑regressioner i driftsscenarier, kan ni gärna tekniskt bedöma det med oss: diskutera projekt eller moderniseringsprojekt med Net-Base.

I det tekniska sammanhanget spelar också Delphi Fmx Breakpoints och Firemonkey Layout en viktig roll när integrationer, dataflöden och vidareutveckling måste samspela på ett ordnat sätt.

Diskutera projekt eller moderniseringsprojekt med Net-Base.

Nästa steg

När ett ämne blir ett verkligt projekt bör arkitektur, befintliga system och drift behandlas gemensamt redan i ett tidigt skede.

Vi stöder inte bara vid enstaka frågor, utan även när kodsfragment, legacy-frågor eller portalidéer ska utvecklas till ett robust företagsprojekt.

  • Nuläge, målbild och tekniska risker bedöms tillsammans.
  • REST, dataåtkomst, portaler och utrullning skjuts inte upp som sena följder.
  • Ni ser tidigt vilken väg som är ekonomiskt och driftsmässigt bärkraftig.

Dela inlägg

Dela det här inlägget direkt

LinkedIn, X, XING, Facebook, WhatsApp och e‑post är omedelbart tillgängliga. För Instagram förbereder vi länken och en kort text direkt.

E-post

Instagram öppnas i en ny flik. Länken och korttexten kopieras till urklipp först.