A magazintémától a projektgyakorlatig
A bejegyzéshez tartozó szolgáltatási és technikai oldalak
Akinek a Delphi FireMonkey-ben több formát kell kiszolgálnia, könnyen a Responsive Layouts FMX-nél köt ki — és ugyanolyan gyorsan egy Align-kaszkádokból, rejtett layout-containerekből és designer-kerülőmegoldásokból álló keveréknél, amelyek a következő DPI- vagy forgásváltásnál felborulnak. Megérett vállalati szoftverkliensek esetén ez különösen kellemetlen: a felhasználói felület továbbfejlődik, csapatok cserélődnek, és hirtelen logika tapad vizuális részletekhez.
A probléma lényege: a FireMonkey sok építőelemet kínál (pl. Align, Anchors, TScaledLayout, TFlowLayout, TGridPanelLayout), de nincs „natív“ Breakpoint-System, mint a weben. Méretváltozásokra ugyan lehet reagálni, de tiszta architektúra nélkül ez sok formon át elosztott „if Width < … then …“-szerű megoldásokhoz vezet.
Ez a bejegyzés bemutat egy Layout-Router-t: egy kis komponenst, amely központilag kezeli a Breakpoints-et és Controls (vagy egész layout-blokkok) között áthelyezi az elemeket előkészített Slots-okba. Cél: az állapotok megmaradjanak, a kód karbantartható legyen, és a szélső esetek — például rotáció, beágyazott layoutok és Re-Entrancy — kezelve legyenek. Emellett néhány kevésbé nyilvánvaló trükk is szerepel, amelyek a gyakorlatban a „demóban fut” és az „üzembiztosan fut” közötti különbséget jelentik.
Warum Breakpoints in FMX anders sind als im Web
Web-layoutokban a Breakpoints többnyire deklaratívak (CSS Media Queries). FMX-ben a layout-döntések futásidőben tipikusan imperatív módon történnek: az OnResize-ben váltunk. Emellett platformspecifikus sajátosságok is adódnak:
- Device-Pixel vs. logische Pixel:
ClientWidth/ClientHeightlogikai egységekben vannak (a skálázástól függően). DPI-váltások (pl. Windows Per-Monitor-DPI) újratriggereket indíthatnak anélkül, hogy „fizikailag“ bármi változna. - Rotation und Safe Areas: Mobil platformok Insets-eket adnak (Notch/Safe Area) — az OS-től és az eszköztől függően. Egy „csak szélesség alapján döntő“ Breakpoint gyakran túl rövidlátó, mert a ténylegesen használható terület kisebb, mint az ablak nyers mérete.
- Layout-Pass: A FireMonkey fázisokban számítja ki a layoutokat. Ha rossz pillanatban változtatjuk meg a Parent/Align beállításokat, mellékhatások léphetnek fel (pl. többszöri reflow vagy villódzó méretek).
Egy Layout-Router erre úgy ad választ, hogy (1) az „amikor”-t (Resize/Scale/Rotation) szétválasztja a „hogyan”-tól (layout-szabályok), és (2) a szabályokat egy helyre koncentrálja. Műszaki vezetők számára a legfontosabb hatás: egy tiszta, ellenőrizhető döntési központot kapnak a számos lokális kivétel helyett.
Architektur: Layout-Router mit Slots statt Control-Erzeugung
A tiszta trükk FMX-re: ne dinamikusan hozzunk létre új Controls-okat, hanem függesszük át a meglévő Controls-okat Slots között. Egy Slot egyszerűen egy konténer (pl. TLayout), amely a felület egy területét reprezentálja: Sidebar, Toolbar, Content, Footer, Details-Pane.
Előnyök egyedi vállalati szoftverekben:
- Az állapotok megmaradnak (Edit-Text, Scrollposition, kiválasztott elemek), mert az objektumpéldányokat nem építjük újra.
- Kisebb a kockázata az események, időzítők vagy Bindings kettős összekötésének.
- A layout-szabályok láthatóvá válnak: hogy „melyik blokk melyik Slotban van“ adott Breakpoint-onként követhető és átvizsgálható.
Gyakorlati szempontból fontos: Vágja a UI-blokkokat elég durván. Ha 30 különálló vezérlőt áthelyez, maga az útvonallista válik hibaforrássá. Jobbak a konténerek, például layFilterBar, layNavigation, layResultList, layDetails.
Forrásrészlet: Breakpoint-router reszponzív elrendezésekhez (FMX)
A következő kód egy segédegységnek szánt modul, amelyet FMX-űrlapokban használhat. Kiszámít egy Breakpointot (XS/SM/MD/LG/XL) és áthelyezi a definiált vezérlőket a definiált slot-konténerekbe. Fontos részletek:
- Debounce a
TThread.ForceQueuesegítségével: több Resize-esemény egy frissítésbe összevonva (kevesebb UI-remegés, kevesebb reflow-ciklus). - Re-entrancy-védelem: a layout-frissítés gyakran maga is újra Resize/Layout-et vált ki.
- Opcionális: tájolás (Portrait/Landscape) belefoglalható a Breakpoint-logikába.
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);
// Egy leképezés: melyik Control melyik Slotba (konténerbe) kerüljön egy adott breakpoint esetén.
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; // kézi újraszámolás
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 nem lehet 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: csak egyszer az üzenetciklus során alkalmazandó
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;
// Figyelem: a Parent váltása megváltoztatja a Z-Order-t.
// Ha a sorrend számít, DefineRoute-ot a kívánt sorrendben kell meghívni.
for LRoute in LList do
begin
if (LRoute.Control.Parent <> LRoute.TargetSlot) then
LRoute.Control.Parent := LRoute.TargetSlot;
// Align-ot csak a Parent beállítása után állítsa, különben a Bounds másként értelmeződhet.
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;
// A breakpointok szándékosan durvák, mivel a FMX célplatformok erősen eltérhetnek.
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.
Hogyan használja a routert egy űrlapon
Slotokat TLayout-ként definiál (pl. layTop, layLeft, layContent) és ezután breakpointonként regisztrálja, hogy mely blokkok hol helyezkedjenek el. Jellemző, hogy a Sidebar és a Details-Pane kis breakpointokban egymás alá kerülnek.
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;Elhelyezés: Miért stabilabb gyakran az áthelyezés, mint a Visible tulajdonság kapcsolgatása
Elterjedt megközelítés, hogy minden variáns számára külön layout-fát tartanak fenn, és csak a Visible-t kapcsolgatják. Ez a designerben kényelmesnek tűnik, de tipikus mellékhatásai vannak:
- Kettős Binding/Events: Két hasonló vezérlőt kell szinkronban tartani (pl. két szűrősáv).
- Tab-Reihenfolge und Fokus: Váltáskor elveszítheti a fókuszt vagy egy nem látható vezérlőbe kerülhet, ha TabStop/HitTest kedvezőtlenül vannak beállítva.
- State Drift: Görgetési pozíciók, kijelölési állapotok vagy szerkesztett szövegek eltérhetnek.
Az áthelyezés egyértelműen megtartja az objektum példányát. Fontos, hogy a layout-blokkokat úgy alakítsa ki, hogy függetlenül áthelyezhetők legyenek (pl. a „Sidebar” mint külön konténer ahelyett, hogy sok különálló vezérlőt használna). Pont ez térül meg a karbantartásban és a hibakeresésben: egyetlen példányt hibakeres, nem két párhuzamos árnyék-UI-t.
Gyakorlati buktatók (és hogyan lehet őket hibakereséssel feltárni)
1) Átméretezési viharok és Re-Entrancy
FMX nem csak a felhasználói átméretezéskor triggereli az OnResize-et, hanem stílusváltozáskor, Parent-Änderungen és néha DPI-változáskor is. Debounce nélkül az alkalmazás layout-ciklusba ragadhat. A router a TThread.ForceQueue-t használja, hogy a változtatásokat a következő UI-tickre tolja.
Hibakeresési tipp: Naplózás (pl. OutputDebugString-en keresztül) breakpointtal, mérettel és egy update-számlálóval segít megtalálni a reflow-ciklusokat. Ha továbbá naplózza azt az időpontot, amikor az ApplyRoutes elindul és befejeződik, gyorsan látható, hogy egyetlen átméretezés „kaskadiert-e”.
2) Z-sorrend, HitTest és „láthatatlan” kattintásblokkolók
A parent-váltás megváltoztatja a Z-sorrendet. Ha overlayek (pl. Flyouts) már nem kapnak kattintásokat, gyakran azért van, mert egy client-aligned konténer fekszik felette és a HitTest aktív. Megoldás: overlay-területekhez szándékosan külön slotot fenntartani legfelül, és csak ott parentelni az ilyen vezérlőket. FMX-ben a HitTest (hogy egy vezérlő elfogja-e a mouse-/touch-eseményeket) gyakrabban az ok, mint a láthatóság.
3) TGridPanelLayout und prozentuale Größen
TGridPanelLayout százalékos oszlopok/sorok esetén, Align=Client-tel és dinamikus szülőváltással váratlan újraszámításokat indíthat el. Ha Gridet kell használni, helyezze a Gridet egy slotba, és csak teljes Grid-blokkokat akasszon át, ne a Grid-gyermekeket. Ez csökkenti a kombinatorikát a layout-passzokban.
4) Fokus, virtuelle Tastatur und „springende“ Eingabefelder
Egy speciális eset, amely mobil FMX-alkalmazásoknál és Windows-típusú tableteknél is előfordul: az áthelyezés során egy fókuszált Edit-vezérlő rövid időre elveszítheti a szülőjét. Ez bezárhatja a virtuális billentyűzetet vagy visszaállíthatja a kurzort. Gyakorlatban bevált megoldás: az útvonal-átállítás előtt ideiglenesen eltárolni az aktuális fókuszt (Focused/IFMXFocusControl), majd az útvonal-átállítás után (ugyanabban az UI-tickben) visszaállítani a fókuszt. Ez különösen érdemes olyan beviteli űrlapoknál, amelyek „kétoszlopos” (Tablet/PC) és „egyoszlopos” (Phone) elrendezés között váltanak.
Varianten: Breakpoints nach Formfaktor statt nur nach Breite
Valós többplatformos klienseknél a „Breite” önmagában gyakran nem megfelelő jel. Érdemes a következő variánsokat figyelembe venni:
- Breite und Höhe: nagyon lapos ablakok (pl. kassza-terminálok, megosztott képernyők) más szabályokat igényelnek.
- Orientierung:
Landscapetáblagépeken gyakran „desktop-ähnlich”, Portrait inkább „mobile-like”. - Safe-Area-Nutzfläche: iOS/Androidon a ténylegesen használható magasság a rendszer sávjai miatt jelentősen csökkenhet. Ha csak a
Height-et nézik, néha „zu spät” történik az útvonal-váltás.
A Router szándékosan úgy van felépítve, hogy kicserélhető legyen a breakpoint-funkció. Ez hasznos legacy helyzetekben is, amikor ugyanaz a forma több hoston fut (pl. egyszer normál ablakban, egyszer beágyazott konténerben).
Ungewöhnlich sauber: Layout-Routing als „Transaktion”
Nagyobb képernyőkön a probléma kevésbé maguknál a breakpointoknál jelentkezik, sokkal inkább a UI-műveletek sorrendjénél. Egy gyakorlati minta: a routingot tranzakciónak kezelni — először dönteni, aztán áthelyezni, majd a mellékhatásokat (Visibility, Fokus, Datenrefresh) rendezett módon végrehajtani.
Konkrétan ez azt jelenti: kerülje, hogy egyes vezérlők az áthelyezés közben saját eseményeket indítsanak, amelyek további layout- vagy adat-hozzáférési műveleteket indítanak el. FMX-ben ez például akkor fordul elő, ha a szülőcsere során OnEnter/OnExit kiváltódik, vagy egy LiveBinding-kifejezés egy Bounds-frissítés miatt újraértékelődik. Ha ilyen hatásokat tapasztal, segít egy központi „Updating” kapcsoló (mint a Routerben) és egy egyértelmű post-step: csak az ApplyRoutes után fussanak drága műveletek (pl. lista újratöltése, részletek kötése).
Különösen releváns ez olyan kliensek esetén, amelyeknek REST-hozzáférésük van: egy nemkívánatos újratöltés átméretezés közben felesleges kéréseket eredményezhet. Ez a LAN-on nem feltűnő, de VPN-en vagy mobil hálózaton azonnal észrevehető.
Wann sich der Ansatz lohnt – und wo er Grenzen hat
A Layout-Router megéri, ha:
- egy FMX-alkalmazás több évig használatban marad, és több fejlesztő dolgozik ugyanazokon a képernyőkön,
- az UI-blokkok világosan elkülöníthetők (Sidebar/Details/Content),
- reprodukálható breakpoint-szabályokra van szükség, ahelyett hogy ad-hoc Align-tuningot alkalmazzanak.
Határokat akkor látni, amikor egy képernyő erősen „fluid”-nak kell lennie (sok dinamikus csempe, valódi masonry-elrendezések). Akkor a TFlowLayout/TGridPanelLayout vagy saját layout-osztályok alkalmasabbak. Ha nagyon sok egyedi vezérlő váltogat slotok között, az útvonalak karbantartása átláthatatlanná válik – ilyenkor jobb nagyobb blokkokra bontani, vagy egy deklaratív konfigurációs réteget bevezetni (pl. egy JSON-konfiguráció a slot-hozzárendelésekhez, amely indításkor töltődik be).
Összegzés: Az FMX-reszponzív elrendezésekhez a breakpointokkal való átkapcsolás pragmatikus középutat jelent: kevesebb tervezői káosz, egyértelmű szabályok, stabil állapotok. Ez nem helyettesíti az átgondolt UI-struktúrát, de megbízható vázat ad ahhoz, hogy az FMX-klienseket digitális vállalati megoldásokban a formfaktorok között kontrolláltan továbbfejlesszék.
Ha egy meglévő Delphi- vagy FMX-alkalmazásban szeretne ilyen layout-architektúrát tisztán átvezetni, anélkül, hogy UI-regressziókat kockáztatna üzemeltetési forgatókönyvekben, technikailag szívesen áttekintjük Önnel a lehetőségeket: beszélje meg projektjét vagy modernizációs tervét Net-Base-tel.
A szakmai környezetben szintén fontos szerepet játszanak a Delphi Fmx Breakpoints és a Firemonkey Layoutok, amikor az integrációknak, adatfolyamoknak és a további fejlesztésnek tisztán kell együttműködnie.
Következő lépés
Ha egy témából valós projekt lesz, az architektúrát, a meglévő rendszert és az üzemeltetést korai fázisban együtt kell vizsgálni.
Nemcsak egyedi kérdésekben támogatunk, hanem akkor is, amikor forráskódrészletekből, örökölt rendszerekkel kapcsolatos témákból vagy portálötletekből robusztus vállalati projektet kell kialakítani.
- A jelenlegi állapotot, a célállapotot és a műszaki kockázatokat együttesen értékeljük.
- REST, az adathozzáférést, a portálokat és a bevezetést nem halasztjuk későbbi fázisokra.
- Ön korán látja, melyik út gazdaságilag és üzemeltetési szempontból tartható.