Net-Base Magazín

03.06.2026

Responzivní rozvržení v Delphi FMX: breakpointy bez chaosu v Designeru (s Layout-Routerem ve formě ukázky zdrojového kódu)

Responzivní rozložení ve FMX se v praxi rychle stávají křehkými: přívaly změn velikosti, přepínání DPI, rotace a „Visible-Layouts“ vytvářejí duplicitní stav a těžko laditelné přepočty rozložení. Tento článek ukazuje layout-router s breakpointy, který za běhu kontroluje UI bloky...

03.06.2026

Od tématu magazínu k projektové praxi

Vhodné stránky služeb a technické stránky k příspěvku

Kdo musí ve Delphi FireMonkey obsluhovat více formátů obrazovek, rychle skončí u Responsive Layouts FMX – a stejně rychle u směsi Align-kaskád, skrytých layout-kontejnerů a designerských workaroundů, které při dalším přepnutí DPI nebo rotace selžou. V etablovaných podnikově orientovaných clienstkých aplikacích je to obzvlášť nepříjemné: UI se vyvíjí, týmy se mění a najednou je logika vázaná na vizuální detaily.

Jádrem problému je: FireMonkey nabízí mnoho stavebních bloků (např. Align, Anchors, TScaledLayout, TFlowLayout, TGridPanelLayout), ale žádný „nativní“ breakpointový systém jako na webu. Reagovat lze sice na změny velikosti, ale bez jasné architektury to končí v „if Width < … then …“ roztroušeném po mnoha formách.

Tento příspěvek představuje Layout-Router: malou komponentu, která centrálně spravuje Breakpoints a přepojí ovládací prvky (nebo celé bloky layoutu) mezi připravenými sloty. Cíl: stavy zůstanou zachovány, kód bude udržovatelný a okrajové případy jako rotace, vnořená rozvržení a re-entrancy jsou ošetřeny. Dále uvedu několik méně zřejmých fíglů, které v praxi rozhodují mezi „běží v demo“ a „běží stabilně v provozu“.

Proč jsou Breakpoints v FMX jiné než na webu

Ve webových rozvrženích jsou Breakpoints většinou deklarativní (CSS Media Queries). V FMX se rozhodnutí o rozvržení typicky provádějí imperativně za běhu: při OnResize se přepíná. K tomu přistupují platformně specifické zvláštnosti:

  • Fyzické pixely vs. logické pixely: ClientWidth/ClientHeight jsou v logických jednotkách (závislých na škálování). Změny DPI (např. Windows Per-Monitor-DPI) mohou znovu spustit rozvržení, aniž by se „fyzicky“ cokoli změnilo.
  • Rotace a Safe Areas: Mobilní platformy poskytují insets (Notch/Safe Area) – v závislosti na OS a zařízení. Breakpoint založený pouze na šířce často nestačí, protože použitelná plocha je menší než čistá velikost okna.
  • Průchod rozvržením: FireMonkey počítá layouty ve fázích. Pokud změníte Parent/Align ve špatný okamžik, vznikají vedlejší efekty (např. opakovaný reflow nebo blikající velikosti).

Layout-Router to řeší tím, že (1) odpojuje „kdy“ (Resize/Scale/Rotation) od „jak“ (pravidla rozvržení) a (2) soustředí pravidla na jednom místě. Pro technické vedení je nejdůležitější efekt: získají jasné, ověřitelné rozhodovací centrum místo mnoha lokálních výjimek.

Architektura: Layout-Router se sloty místo vytváření ovládacích prvků

Čistý trik pro FMX: ne dynamicky znovu vytvářet ovládací prvky, ale přepojovat stávající ovládací prvky mezi Slots. Slot je jednoduchý kontejner (např. TLayout), který reprezentuje oblast UI: Sidebar, Toolbar, Content, Footer, Details-Pane.

Výhody v individuálním podnikovém softwaru:

  • Stavy zůstanou zachovány (Edit-Text, pozice posuvu, vybrané položky), protože instance nejsou znovu vytvářeny.
  • Menší riziko duplicitního propojení událostí, časovačů nebo vazeb.
  • Pravidla rozvržení jsou přehledná: „který blok je v kterém slotu“ lze pro každý Breakpoint dohledat a prověřit.

Důležité v praxi: Rozdělte UI bloky dostatečně hrubě. Pokud přepojíte 30 jednotlivých ovládacích prvků, stane se seznam tras sám zdrojem chyb. Lepší jsou kontejnery jako layFilterBar, layNavigation, layResultList, layDetails.

Útržek zdrojového kódu: Breakpoint-Router pro responzivní rozložení FMX

Následující kód je určen jako pomocná jednotka, kterou můžete používat ve FMX formulářích. Vypočítá breakpoint (XS/SM/MD/LG/XL) a přepojí definované ovládací prvky do definovaných slot-kontejnerů. Důležité detaily:

  • Debounce pomocí TThread.ForceQueue: několik událostí změny velikosti se sloučí do jedné aktualizace (méně chvění UI, méně reflow cyklů).
  • Ochrana proti re-entranci: aktualizace rozložení často sama vyvolá opětovné události Resize/Layout.
  • Volitelné: Orientace (Portrait/Landscape) může být zohledněna v logice breakpointů.

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);

// Mapování: které Control má být v kterém slotu (kontejneru) pro daný breakpoint.
TNBRoute = record
Control: TControl;
TargetSlot: TControl; // typicky TLayout nebo 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; // ruční přepočet
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í být 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: aplikovat pouze jednou za průběh smyčky zpráv
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: změna Parent mění Z-Order.
// Pokud je pořadí důležité, volat DefineRoute v požadovaném pořadí.
for LRoute in LList do
begin
if (LRoute.Control.Parent <> LRoute.TargetSlot) then
LRoute.Control.Parent := LRoute.TargetSlot;

// Align nastavte až po změně Parent, jinak mohou být Bounds jinak interpretovány.
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;
// Breakpointy záměrně hrubé, protože cílové platformy FMX se výrazně liší.
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.

Jak používat router ve formuláři

Definujete sloty jako TLayout (např. layTop, layLeft, layContent) a pak pro každý breakpoint zaregistrujete, kde které bloky leží. Typické je, že Sidebar a panel detailů se při malých breakpointech přesunou pod sebe.

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;

Zařazení: Proč je „přepojování“ často stabilnější než přepínání Visible

Běžný přístup je mít pro každou variantu samostatné stromy layoutu a jen přepínat Visible. V Designeru to může působit pohodlně, ale má typické vedlejší efekty:

  • Dvojité Binding/Events: Dva podobné ovládací prvky je nutné udržovat synchronizované (např. dvě filtrační lišty).
  • Pořadí tabulátoru a fokus: Při přepnutí ztratíte fokus nebo skončíte na neviditelných ovládacích prvcích, pokud jsou TabStop/HitTest nevhodně nastaveny.
  • Odchylka stavu: Pozice posuvníků, stavy výběru nebo editované texty se mohou rozcházet.

Přepojování udržuje instanci jednoznačnou. Důležité je rozdělit layoutové bloky tak, aby šly nezávisle přesouvat (např. „Sidebar“ jako vlastní kontejner místo mnoha jednotlivých ovládacích prvků). Právě to se vyplatí při údržbě a analýze chyb: ladíte jednu instanci, ne dvě paralelní stínové UI.

Úskalí v praxi (a jak je debugovat)

1) Vlny Resize a re-entrancy

FMX vyvolává OnResize nejen při uživatelské změně velikosti, ale i při změně stylu, změně Parent a částečně při změnách DPI. Bez odfiltrování opakovaných událostí (debounce) se aplikace může zaseknout v cyklech přepočtu rozložení. Router používá TThread.ForceQueue, aby změny přesunul do dalšího UI-tiku.

Tip pro debug: logování (např. přes OutputDebugString) s breakpointem, informací o velikosti a čítačem aktualizací pomůže najít smyčky přepočtu rozložení. Pokud navíc zalogujete čas, kdy ApplyRoutes začíná a končí, rychle uvidíte, zda jeden resize „kaskáduje“.

2) Z-Order, HitTest a „neviditelní“ blokátory kliků

Změna Parent mění Z-Order. Pokud překryvy (např. Flyouts) přestanou reagovat na kliknutí, často je příčinou to, že nad nimi leží klientem zarovnaný kontejner a HitTest je aktivní. Variantou je vyhradit pro oblasti overlayů samostatný slot úplně nahoře a právě tam tyto ovládací prvky parentovat. Ve FMX je HitTest (zda ovládací prvek zachytí události myši/touch) častěji příčinou než samotná viditelnost.

3) TGridPanelLayout a procentuální velikosti

TGridPanelLayout může při procentuálních sloupcích/řádcích v kombinaci s Align=Client a dynamickým přeházením vyvolat neočekávané přepočty. Pokud musíte použít Grid, umístěte Grid do slotu a přehazujte jen celé bloky Gridu, ne potomky Gridu. To snižuje kombinační množství průchodů rozvržením.

4) Fokus, virtuelle Tastatur und „springende“ Eingabefelder

Okrajový případ, který se vyskytuje v mobilních FMX aplikacích a také na Windows-tabletech: při přeházení může fokusované Edit-Control krátce ztratit rodiče. To může zavřít virtuální klávesnici nebo resetovat kurzor. Prakticky se osvědčilo: před routingem dočasně uložit aktuální fokus (Focused/IFMXFocusControl), po routingu (ve stejném UI-tiku) fokus obnovit. To se vyplatí zejména u vstupních formulářů, které přecházejí mezi „dvousloupcovým“ (Tablet/PC) a „jednosloupcovým“ (Phone) zobrazením.

Varianten: Breakpoints nach Formfaktor statt nur nach Breite

V reálných multiplatformních klientech není „šířka“ často samostatně správným indikátorem. Smysluplné varianty:

  • Šířka a výška: velmi mělká okna (např. pokladní terminály, dělené obrazovky) vyžadují jiná pravidla.
  • Orientace: Landscape na tabletech je často „podobné desktopu“, Portrait spíše „podobné mobilnímu prostředí“.
  • Safe-Area využitelná plocha: Na iOS/Android může být efektivně využitelná výška systémovými lištami výrazně zmenšena. Kdo bere v úvahu pouze Height, routuje někdy „příliš pozdě“.

Router je úmyslně navržen tak, aby bylo možné vyměnit funkci breakpointů. To je užitečné také v legacy situacích, kdy stejný formulář běží v několika hostitelích (např. jednou jako běžné okno, jednou v embedovaném kontejneru).

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

U větších obrazovek problém méně spočívá v samotných breakpointech než v pořadí UI operací. Praktický vzor je považovat routing za transakci: nejdříve rozhodnout, pak přehodit, pak vedlejší efekty (Visibility, Fokus, obnovení dat) provést v řízeném pořadí.

Konkrétně to znamená: Vyvarujte se toho, aby jednotlivé ovládací prvky během přehazování spouštěly vlastní události, které následně spustí layout nebo přístup k datům. Ve FMX se to například stane, když při změně rodiče vyvolá OnEnter/OnExit nebo když se LiveBinding výraz znovu vyhodnotí vlivem aktualizace bounds. Pokud pozorujete takové efekty, pomůže centrální přepínač „Updating“ (jako v Routeru) plus jasný post-krok: teprve po ApplyRoutes se mohou spouštět nákladné operace (např. znovunačtení seznamu, navázání detailního zobrazení).

Zvláště u klientů s přístupem REST je to relevantní: nechtěné přenačtení během změny velikosti může vést k zbytečným požadavkům. V LAN to není patrné, ale v VPN nebo na mobilu hned.

Kdy se přístup vyplatí – a kde má omezení

Layout-router se vyplatí, když:

  • FMX aplikace žije řadu let a na stejných obrazovkách pracuje vícero vývojářů,
  • UI bloky lze jasně oddělit (Sidebar/Details/Content),
  • potřebujete reprodukovatelné pravidla pro breakpointy místo ad-hoc ladění Alignu.

Omezení uvidíte, když musí být obrazovka silně „fluidní“ (mnoho dynamických dlaždic, skutečná Masonry-Layouts). Pak jsou vhodnější TFlowLayout/TGridPanelLayout nebo vlastní třídy layoutu. Pokud se navíc velmi mnoho jednotlivých ovládacích prvků přemisťuje mezi sloty, stává se údržba tras nepřehlednou – v takovém případě je lepší řezat větší bloky nebo zavést deklarativní konfigurační vrstvu (např. JSON konfiguraci pro přiřazení slotů, která se načítá při startu).

Závěr: Pro responzivní rozvržení ve FMX je „přepínání podle breakpointů“ pragmatickým kompromisem: méně chaosu v Designeru, jasná pravidla, stabilní stavy. Nenahrazuje promyšlenou UI strukturu, ale dává vám pevný rámec, který umožní kontrolovaný další vývoj FMX klientů v digitálních podnikových řešeních napříč formfaktory.

Pokud chcete v existující Delphi- nebo FMX aplikaci takovou architekturu rozvržení čistě převést, aniž byste přitom riskovali UI regrese v provozních scénářích, můžete to s námi technicky zařadit: Projekt nebo modernizační záměr projednat s Net-Base.

V odborném prostředí hrají také Delphi Fmx Breakpoints a Firemonkey Layout důležitou roli, když musí integrace, tok dat a další vývoj bezproblémově spolupracovat.

Projekt nebo modernizační záměr projednat s Net-Base.

Další krok

Když se z tématu stane reálný projekt, měly by být architektura, stávající systém a provoz včas posuzovány společně.

Podporujeme nejen při jednotlivých otázkách, ale i v případě, že se z útržků zdrojového kódu, legacy témat nebo nápadů na portál má vyvinout robustní podnikový projekt.

  • Současný stav, cílový stav a technická rizika jsou hodnoceny společně.
  • REST, přístup k datům, portály a nasazení nebudou odkládány na později.
  • Vidíte včas, která cesta je ekonomicky i provozně životaschopná.

Sdílet příspěvek

Sdílet tento příspěvek přímo

LinkedIn, X, XING, Facebook, WhatsApp a e-mail jsou ihned k dispozici. Pro Instagram připravíme odkaz a stručný text.

E-mail

Instagram se otevře v nové záložce. Odkaz a krátký text budou předtím zkopírovány do schránky.