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.
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.
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:
Landscapepå 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å
Heightroutar 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.
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.