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