Net-Base Magazin

03.06.2026

Reszponzív elrendezések a Delphi FMX‑ben: Breakpointok tervezői káosz nélkül (Layout‑Router mint forrásrészlet)

Az FMX-reszponzív elrendezések a gyakorlatban gyorsan törékennyé válnak: az átméretezési viharok, DPI-változások, képernyőforgatás és a „Visible-Layouts” kettős állapotot hoznak létre, és nehezen hibakereshető újrarendezéseket okoznak. Ez a bejegyzés bemutat egy Layout-Routert breakpointokkal, amely futásidőben kontrollálja az UI-blokkokat...

03.06.2026

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/ClientHeight logikai 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.ForceQueue segí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.
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);

  // 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.

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;

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: Landscape tá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.

Projekt vagy modernizációs terv megbeszélése Net-Base-vel.

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ó.

Bejegyzés megosztása

Ezt a bejegyzést közvetlenül megosztani

LinkedIn, X, XING, Facebook, WhatsApp és E-Mail azonnal elérhetők. Instagramra a linket és a rövid szöveget közvetlenül előkészítjük.

E-mail

Az Instagram egy új lapon nyílik meg. A link és a rövid szöveg előzetesen a vágólapra másolódik.