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/ClientHeightsú 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.
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.
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:
Landscapena 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ť.
Ď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á.