Net-Base Magazín

03.06.2026

Responzívne rozloženia v Delphi FMX: Breakpointy bez chaosu v návrhovom editore (s Layout-Routerom ako úryvok zdrojového kódu)

Responzívne rozloženia FMX sa v praxi rýchlo stávajú krehkými: vlny zmien veľkosti, prepínanie DPI, rotácia a „Visible-Layouts“ vytvárajú duplicitný stav a ťažko diagnostikovateľné prerátania rozloženia. Tento článok ukazuje Layout-Router s breakpointmi, ktorý za behu riadi UI-bloky.

03.06.2026

Od témy magazínu k projektovej praxi

Súvisiace stránky služieb a technológií k príspevku

Kto v Delphi FireMonkey potrebuje obsluhovať viacero formátov obrazoviek, rýchlo skončí pri Responzívnych rozloženiach FMX – a rovnako rýchlo aj pri zmesi Align-kaskád, skrytých layout-containerov a dočasných riešení v dizajnéri, ktoré pri ďalšej zmene DPI alebo rotácie zlyhávajú. V už etablovaných klientoch podnikových softvérov je to obzvlášť nepríjemné: UI sa ďalej vyvíja, tímy sa menia a zrazu je logika viazaná na vizuálne detaily.

Jadro problému: FireMonkey ponúka množstvo stavebných prvkov (napr. Align, Anchors, TScaledLayout, TFlowLayout, TGridPanelLayout), ale nemá „natívny“ Breakpoint‑systém ako na webe. Síce sa dá reagovať na zmeny veľkosti, no bez jasnej architektúry to končí roztrúsenými „if Width < … then …“ naprieč mnohými Formami.

Tento príspevok ukazuje Layout-Router: malú komponentu, ktorá centralizovane spravuje Breakpoints a presúva Controls (alebo celé layout‑bloky) medzi predpripravenými Slots. Cieľ: stavy zostávajú zachované, kód je udržiavateľný a okrajové prípady ako rotácia, vnorené rozloženia a re‑entrancy sú zmiernené. Pridávajú sa aj niektoré menej zjavné triky, ktoré v praxi rozhodujú medzi „beží v deme“ a „beží stabilne v prevádzke“.

Prečo sú Breakpoints vo FMX iné než na webe

V webových rozloženiach sú Breakpoints väčšinou deklaratívne (CSS Media Queries). Vo FMX sa rozhodnutia o rozložení typicky vykonávajú imperatívne za behu: pri OnResize sa prepína. K tomu sa pridávajú platformovo špecifické zvláštnosti:

  • Fyzické pixely vs. logické pixely: ClientWidth/ClientHeight sú v logických jednotkách (závislé od škálovania). Zmeny DPI (napr. Windows Per-Monitor-DPI) môžu znovu vyvolať layout, bez toho aby sa „fyzicky“ niečo zmenilo.
  • Rotácia a Safe Areas: Mobilné platformy poskytujú Insets (Notch/Safe Area) – v závislosti od OS a zariadenia. „Breakpoint len podľa šírky“ je často nedostatočný, pretože použiteľná plocha môže byť menšia než samotná veľkosť okna.
  • Priebeh layoutu: FireMonkey počíta rozloženia v fázach. Ak zmeníte Parent/Align v nesprávnom okamihu, vznikajú nežiaduce vedľajšie účinky (napr. opakovaný reflow alebo blikanie veľkostí).

Layout-Router rieši toto tým, že (1) oddelí „kedy“ (Resize/Scale/Rotation) od „ako“ (pravidlá rozloženia) a (2) sústredí pravidlá na jednom mieste. Pre technických vedúcich je najdôležitejší efekt ten, že získajú jasné, overiteľné rozhodovacie centrum namiesto mnohých lokálnych výnimiek.

Architektúra: Layout-Router so Slots namiesto vytvárania ovládacích prvkov

Čistý trik pre FMX: nie dynamicky znovu vytvárať Controls, ale prevesiť existujúce Controls medzi predpripravenými Slots. Slot je jednoducho kontajner (napr. TLayout), ktorý reprezentuje oblasť UI: bočný panel, nástrojový panel, obsah, pätička, panel detailov.

Výhody v individuálnom firemnom softvéri:

  • Stavy zostávajú zachované (text v editore, pozícia scrollu, vybrané položky), pretože inštancie sa nevytvárajú nanovo.
  • Menšie riziko duplicitného prepojenia udalostí, timerov alebo bindingov.
  • Pravidlá rozloženia sa stávajú viditeľnými: „ktorý blok je v ktorom Slote“ je možné pre každý Breakpoint sledovať a skontrolovať.

Dôležité pre prax: rozdeľte UI-bloky dostatočne hrubo. Ak premiestnite 30 samostatných ovládacích prvkov, samotný zoznam trás sa stane zdrojom chýb. Lepšie sú kontajnery ako layFilterBar, layNavigation, layResultList, layDetails.

Úryvok zdrojového kódu: Breakpoint-Router pre responzívne rozloženia FMX

Nasledujúci kód je určený ako pomocná jednotka, ktorú môžete používať vo FMX-formulároch. Vypočíta breakpoint (XS/SM/MD/LG/XL) a priradí definované ovládacie prvky do definovaných slot-kontajnerov. Dôležité detaily:

  • Debounce cez TThread.ForceQueue: viaceré Resize-udalosti sa zoskupia do jednej aktualizácie (menej trasenia UI, menej reflow-cykov).
  • Re-Entrancy-Schutz: aktualizácia layoutu často sama opätovne spúšťa Resize/Layout.
  • Voliteľné: orientácia (Portrait/Landscape) môže byť zahrnutá do breakpoint-logiky.
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);

  // Mapovanie: ktoré Control by malo byť v ktorom slote (kontajneri) pre daný breakpoint.
  TNBRoute = record
    Control: TControl;
    TargetSlot: TControl; // typicky TLayout alebo 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; // manuálne prepočítať
    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 nesmú byť 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: iba raz za 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;

    // Pozor: zmena Parent mení Z-poradie.
    // Ak je poradie relevantné, volajte DefineRoute v požadovanom poradí.
    for LRoute in LList do
    begin
      if (LRoute.Control.Parent <> LRoute.TargetSlot) then
        LRoute.Control.Parent := LRoute.TargetSlot;

      // Align nastavte až po nastavení Parent, inak môžu byť Bounds interpretované inak.
      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 sú zámerne hrubé, pretože cieľové platformy FMX sa výrazne líšia.
  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.

Ako použiť Router vo formulári

Definujete sloty ako TLayout (napr. layTop, layLeft, layContent) a potom pre každý Breakpoint zaregistrujete, kde sa ktoré bloky nachádzajú. Typické je, že postranný panel a panel detailov sa na malých Breakpointoch presunú pod seba.

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;

Zaradenie: Prečo je „premiestňovanie“ často stabilnejšie než prepínanie Visible

Bežný prístup je mať pre každú variantu samostatné stromové rozloženie a iba prepínať Visible. V dizajnéri to pôsobí pohodlne, ale má typické vedľajšie účinky:

  • Dvojité Binding/události: Dva podobné ovládacie prvky musia byť udržiavané synchronizované (napr. dve filtračné lišty).
  • Poradie Tab a fokus: Pri prepínaní môžete stratiť fokus alebo skončiť v neviditeľných ovládacích prvkoch, ak TabStop/HitTest sú nevhodne nastavené.
  • State Drift: Pozície posúvania, stavy výberu alebo editované texty sa rozchádzajú.

Premiestňovanie zachováva jednoznačnú inštanciu. Dôležité je deliť layout-bloky tak, aby sa dali presúvať nezávisle (napr. „Sidebar“ ako samostatný kontajner namiesto množstva jednotlivých ovládacích prvkov). Práve to sa vypláca pri údržbe a analýze chýb: ladíte jednu inštanciu, nie dva paralelné tieňové UI.

Úskalia v praxi (a ako ich debugovať)

1) Nárazy OnResize a reentrancia

FMX spúšťa OnResize nielen pri zmene veľkosti používateľom, ale aj pri zmene štýlu, zmene rodiča a čiastočne pri zmene DPI. Bez debounce sa aplikácia zachytí v slučkách rozloženia. Router používa TThread.ForceQueue, aby posunul zmeny do nasledujúceho UI ticku.

Tip na debugovanie: logovanie (napr. cez OutputDebugString) s breakpointom, veľkosťou a počítadlom aktualizácií pomáha nájsť reflow-slučky. Ak navyše zaznamenáte čas, kedy ApplyRoutes začína a končí, rýchlo zistíte, či sa jeden resize „kaskáduje“.

2) Z-Order, HitTest a „neviditeľné“ blokátory kliknutí

Zmena rodiča mení Z-Order. Ak overlaye (napr. Flyouts) už neprechytávajú kliknutia, často je to preto, že nad nimi leží client-aligned kontajner s aktívnym HitTest. Riešenie: pre plochy overlayov zámerne vyčleniť samostatný slot úplne hore a rodičiť takéto ovládacie prvky len tam. Vo FMX je HitTest (či ovládací prvok zachytáva myš-/touch-udalosti) častejšou príčinou než viditeľnosť.

3) TGridPanelLayout a percentuálne veľkosti

TGridPanelLayout môže pri percentuálnych stĺpcoch/riadkoch v kombinácii s Align=Client a dynamickým prehodením spôsobiť neočakávané prepočítania. Ak musíte použiť Grid, umiestnite Grid do slotu a presúvajte len celé Grid-bloky, nie deti Gridu. To znižuje kombinatoriku prechodov layoutu.

4) Fokus, virtuelle Tastatur und „springende“ Eingabefelder

Hraničný prípad, ktorý sa vyskytuje v mobilných FMX-aplikáciách a aj na Windows-tabletoch: pri prehodení sa môže fokusované Edit-Control krátkodobo stratiť rodiča. To môže zavrieť virtuálnu klávesnicu alebo resetovať kurzor. Prakticky sa osvedčilo: pred routovaním dočasne uložiť aktuálny fokus (Focused/IFMXFocusControl) a po routovaní (v tom istom UI-Tick) fokus obnoviť. Toto sa oplatí najmä pri vstupných maskách, ktoré prechádzajú medzi „zweispaltig“ (Tablet/PC) a „einspaltig“ (Phone).

Varianten: Breakpoints nach Formfaktor statt nur nach Breite

V reálnych multiplatformových klientoch nie je samotná „Breite“ často správnym signálom. Užitočné varianty:

  • Breite und Höhe: veľmi ploché okná (napr. Kassen-Terminals, rozdelené obrazovky) potrebujú iné pravidlá.
  • Orientierung: Landscape na tabletoch je často „desktop-ähnlich“, Portrait skôr „mobile-like“.
  • Safe-Area-Nutzfläche: na iOS/Android môže byť efektívne využiteľná výška systémovými lištami výrazne zmenšená. Kto sleduje len Height, routuje niekedy „zu spät“.

Der Router je zámerne postavený tak, aby ste mohli vymeniť Breakpoint-funkciu. To pomáha aj v legacy-situáciách, keď tá istá forma beží v rôznych hostoch (napr. raz ako bežné okno, raz v zabudovanom kontajneri).

Ungewöhnlich sauber: Layout-Routing als „Transaktion“

Vo väčších obrazovkách problém menej spočíva v samotných Breakpoints, skôr v poradí UI-operácií. Praktický vzor je spracovať Routing ako Transaktion: najprv rozhodnúť, potom presunúť, potom vedľajšie efekty (Visibility, Fokus, Datenrefresh) vykonať v usporiadanom poradí.

Konkrétne to znamená: zabráňte tomu, aby jednotlivé Controls počas presunu vyvolávali vlastné udalosti, ktoré následne spúšťajú layout alebo prístup k dátam. Vo FMX sa to stáva napríklad, keď pri zmene rodiča spustí OnEnter/OnExit alebo keď sa LiveBinding-výraz pri Bound-update znovu vyhodnotí. Ak vidíte takéto efekty, pomôže centrálny „Updating“-prepínač (ako v Router) plus jasný post-step: až po ApplyRoutes majú bežať nákladné operácie (napr. znovu načítať zoznam, naviazať detailný pohľad).

Obzvlášť pri klientoch s prístupom REST je to relevantné: nechcený Reload počas Resize môže viesť k zbytočným Requests. V LAN to nemusíte postrehnúť, no v VPN alebo na mobile okamžite.

Kiedy się der Ansatz lohnt – und wo er Grenzen hat

Der Layout-Router sa oplatí, keď:

  • FMX-Anwendung žije roky a viacerí vývojári pracujú na tých istých Screens,
  • UI-Blöcke sa dajú jasne oddeliť (Sidebar/Details/Content),
  • potrebujete reproduzierateľné Breakpoint-Regeln namiesto ad-hoc Align-Tuning.

Obmedzenia uvidíte, keď musí byť obrazovka silne „fluidná“ (mnoho dynamických dlaždíc, skutočné Masonry-rozloženia). Potom sú vhodnejšie TFlowLayout/TGridPanelLayout alebo vlastné triedy layoutu. Aj keď sa veľmi veľa jednotlivých ovládacích prvkov presúva medzi slotmi, údržba trás sa stáva neprehľadnou – v takom prípade je lepšie rozdeliť väčšie bloky alebo zaviesť deklaratívnu konfiguračnú vrstvu (napr. JSON-konfigurácia pre priradenia slotov, ktorá sa načítava pri štarte).

Záver: Pre responzívne rozloženia vo FMX je „presúvanie s Breakpoints“ pragmatickým kompromisom: menej chaosu pri návrhu, jasné pravidlá, stabilné stavy. Nenahrádza premyslenú UI-štruktúru, ale poskytuje spoľahlivý rámec, ktorý vám umožní kontrolovane rozvíjať FMX-klientov v digitálnych podnikových riešeniach naprieč formfaktormi.

Ak chcete v existujúcej Delphi- alebo FMX-aplikácii takúto architektúru rozloženia dôsledne zaviesť, bez rizika UI-regresií v prevádzkových scenároch, môžete to s nami technicky posúdiť: prediskutovať projekt alebo modernizačný zámer s Net-Base.

V odbornom kontexte zohrávajú Delphi FMX Breakpoints a Firemonkey Layout tiež dôležitú úlohu, keď musia integrácie, dátové toky a ďalší vývoj bezchybne spolupracovať.

Projekt alebo modernizačný zámer prediskutovať s Net-Base.

Ďalší krok

Keď sa téma stane reálnym projektom, architektúru, existujúci stav a prevádzku treba včas posudzovať spoločne.

Podporujeme nielen pri jednotlivých otázkach, ale aj vtedy, keď sa z fragmentov zdrojového kódu, tém súvisiacich s legacy systémami alebo nápadov na portál má stať robustný podnikový projekt.

  • Stav, cieľový obraz a technické riziká sa hodnotia spoločne.
  • REST, prístup k dátam, portály a Rollout nebudú odložené na neskôr.
  • Včas zistíte, ktorá cesta je ekonomicky a prevádzkovo životaschopná.

Zdieľať príspevok

Tento príspevok priamo zdieľať

LinkedIn, X, XING, Facebook, WhatsApp a e-mail sú ihneď k dispozícii. Pre Instagram pripravujeme priamo odkaz a krátky text.

E-mail

Instagram sa otvorí v novej karte. Odkaz a krátky text sa predtým skopírujú do schránky.