Net-Base Časopis

03.06.2026

Responzivni rasporedi u Delphi FMX: Breakpoints bez haosa u Designeru (s Layout-Routerom kao primjerom koda)

FMX-responsive rasporedi u praksi brzo postaju krhki: oluje promjena veličine, promjene DPI‑a, rotacija i „Visible-Layouts“ uzrokuju duplicirano stanje i teško otklonjive reflow-e. Ovaj članak prikazuje Layout-Router s Breakpoints koji kontrolira UI-blokove za vrijeme izvođenja.

03.06.2026

Od teme magazina do projektne prakse

Povezane stranice usluga i tehnologije za članak

Ko u Delphi FireMonkey mora podržavati više formfaktora, brzo završi kod Responsive Layouts FMX – i podjednako brzo i u mješavini Align-kaskada, skrivenih layout-kontejnera i designer-workarounda koji se raspadnu pri sljedećoj promjeni DPI-ja ili rotacije. U zrelim poslovnim softverskim klijentima to je posebno neugodno: UI se dalje razvija, timovi se mijenjaju i iznenada logika ovisi o vizualnim detaljima.

Suština problema: FireMonkey nudi mnogo gradivnih elemenata (npr. Align, Anchors, TScaledLayout, TFlowLayout, TGridPanelLayout), ali nema „nativni“ breakpoint-sistem kao na webu. Može se reagirati na promjene veličine, ali bez jasne arhitekture to završi u „if Width < … then …“ raširenim po mnogim formama.

Ovaj članak prikazuje Layout-Router: malu komponentu koja centralno upravlja breakpoint-ovima i prevezuje Controls (ili čitave blokove layouta) između pripremljenih slotova. Cilj: stanja ostaju sačuvana, kod je održiv, a rubni slučajevi poput rotacije, ugniježđenih layouta i re-entrancyja se ublažavaju. Uz to dolazi nekoliko manje očitih trikova koji u praksi čine razliku između „radi u demo-u“ i „radi stabilno u produkciji“.

Zašto su Breakpointi u FMX drugačiji nego na webu

U web-layoutima su breakpointi obično deklarativni (CSS Media Queries). U FMX su odluke o layoutu u runtimeu tipično imperativne: na OnResize se prebacuje. Uz to postoje specifičnosti po platformi:

  • Device-Pixel vs. logische Pixel: ClientWidth/ClientHeight su u logičkim jedinicama (ovisno o skaliranju). DPI-promjene (npr. Windows Per-Monitor-DPI) mogu ponovno pokrenuti layout bez da se „fizički“ išta promijenilo.
  • Rotacija i Safe Areas: Mobilne platforme daju insets (Notch/Safe Area) – ovisno o OS-u i uređaju. „Breakpoint samo prema širini“ često je premalo, jer je upotrebljivi prostor manji od čiste veličine prozora.
  • Layout-pas: FireMonkey izračunava layoute u fazama. Ako se roditelj/Align mijenja u pogrešnom trenutku, nastaju nuspojave (npr. višestruki reflow ili treperenje veličina).

Layout-Router rješava to tako što (1) odvaja „kada“ (Resize/Scale/Rotation) od „kako“ (pravila layouta) i (2) koncentrira pravila na jedno mjesto. Za tehničke leadove najvažniji efekt je jasan, provjerljiv centar odluka umjesto mnogih lokalnih posebnih slučajeva.

Arhitektura: Layout-Router sa slotovima umjesto stvaranja Controls

Čist trik za FMX: ne dinamički stvarati Controls, već postojeće Controls premještati između slotova. Slot je jednostavno kontejner (npr. TLayout) koji predstavlja područje UI-ja: bočna traka (sidebar), alatna traka (toolbar), sadržaj (content), podnožje (footer), panel detalja (details-pane).

Prednosti u prilagođenom poslovnom softveru:

  • Stanja ostaju sačuvana (pozicija u polju za uređivanje, pozicija skrolanja, izabrani elementi), jer se instance ne rekonstruiraju.
  • Manje rizika od duple vezanosti događaja, timera ili bindings.
  • Pravila layouta postaju vidljiva: „koji blok se nalazi u kojem slotu“ može se po Breakpointu pratiti i provjeriti.

Važno za praksu: podijelite UI-blokove dovoljno grubo. Ako premještate 30 pojedinačnih kontrola, sama lista ruta postat će izvor grešaka. Bolje su kontejneri kao što su layFilterBar, layNavigation, layResultList, layDetails.

Izvorni isječak: Breakpoint-Router za responzivne rasporede FMX

Sljedeći kod je zamišljen kao pomoćna jedinica koju možete koristiti u FMX-formama. On izračunava breakpoint (XS/SM/MD/LG/XL) i premješta definisane Controls u definisane Slot-Container. Važni detalji:

  • Debounce preko TThread.ForceQueue: više Resize-Events se objedini u jedno ažuriranje (manje treperenja korisničkog sučelja, manje reflow-petlji).
  • Zaštita od re-entrancyja: ažuriranje layouta često samo ponovno pokreće Resize/Layout.
  • Opcionalno: orijentacija (Portrait/Landscape) može se uključiti u logiku breakpointa.

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

// Mapiranje: koji Control treba u koji Slot (kontejner) za dati Breakpoint.
TNBRoute = record
Control: TControl;
TargetSlot: TControl; // tipično TLayout ili 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čno ponovno izračunavanje
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 ne smiju biti 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: izvršiti samo jednom po message-loopu
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;

// Pažnja: promjena Parent-a mijenja Z-order.
// Ako je redoslijed važan, pozvati DefineRoute u željenom redoslijedu.
for LRoute in LList do
begin
if (LRoute.Control.Parent <> LRoute.TargetSlot) then
LRoute.Control.Parent := LRoute.TargetSlot;

// Align postaviti tek nakon Parent-a, inače se Bounds mogu drugačije interpretirati.
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;
// Breakpointi namjerno grubi, jer se ciljane FMX platforme značajno razlikuju.
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.

Kako koristiti Router u formi

Definišete slotove kao TLayout (npr. layTop, layLeft, layContent) i zatim po Breakpointu registrujete gdje koji blokovi stoje. Tipično je da Sidebar i Details-Pane u malim breakpointima jedna ispod druge.

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;

Kontekst: Zašto je „Umhängen“ često stabilnije od prebacivanja svojstva Visible

Uobičajen pristup je imati za svaku varijantu odvojena stabla layouta i samo Visible prebacivati. U Designeru to djeluje praktično, ali ima tipične nuspojave:

  • Dvostruko Binding/Events: Dva slična kontrola moraju se održavati sinhronizirano (npr. dvije trake filtera).
  • Redoslijed tabulatora i fokus: Prilikom prebacivanja fokus se gubi ili završite u nevidljivim kontrolama ako TabStop/HitTest stoje nepovoljno.
  • State Drift: Pozicije skrolanja, stanja selekcije ili uređeni tekstovi divergiraju.

Umhängen održava instancu jedinstvenom. Važno je rezati layout-blokove tako da se mogu nezavisno premještati (npr. „Sidebar“ kao zaseban kontejner umjesto mnogo pojedinačnih kontrola). Upravo se to isplati pri održavanju i analizi grešaka: debugujete jednu instancu, ne dvije paralelne sjenke UI-a.

Zamke u praksi (i kako ih debugovati)

1) Resize-oluje i Re-Entrancy

FMX pokreće OnResize ne samo pri korisničkom mijenjanju veličine, već i pri promjenama stila, promjenama parenta i ponekad pri promjenama DPI-ja. Bez Debounce-a aplikacija zapada u layout-petlje. Router koristi TThread.ForceQueue da pomjeri promjene u sljedeći UI-tick.

Savjet za debugovanje: logovanje (npr. preko OutputDebugString) sa breakpointom, veličinom i brojačem update-a pomaže pronaći reflow-petlje. Ako dodatno logujete trenutak kada ApplyRoutes počinje i završava, brzo ćete vidjeti da li se pojedinačni Resize „kaskadira“.

2) Z-Order, HitTest i „nevidljivi“ blokatori klikova

Promjena parenta mijenja Z-Order. Ako Overlays (npr. Flyouts) više ne primaju klikove, često je uzrok to što se iznad njih nalazi Client-aligned kontejner i HitTest je aktivan. Varijanta: za overlay-površine namjerno predvidite zaseban slot na vrhu i samo tamo postavite parent za takve kontrole. U FMX-u je HitTest (da li kontrola hvata miš-/touch-događaje) češći uzrok nego sama vidljivost.

3) TGridPanelLayout i procentualne veličine

TGridPanelLayout može kod postotnih kolona/reda u kombinaciji s Align=Client i dinamičkim premještanjem izazvati neočekivane preracunavanja. Ako morate koristiti Grid, smjestite Grid u Slot i premještajte samo cijele Grid-blokove, ne Grid-djecu. To smanjuje kombinatoriku layout-pasa.

4) Fokus, virtuelna tastatura i „poskakujuća“ polja za unos

Rubni slučaj koji se javlja u mobilnim FMX-apps i također na Windows-tabletima: pri premještanju fokusirani Edit-Control može nakratko izgubiti parent. To može zatvoriti virtuelnu tastaturu ili resetirati kursor. U praksi se pokazalo korisnim: prije routinga privremeno sačuvati trenutni fokus (Focused/IFMXFocusControl), nakon routinga (u istom UI-ticku) fokus vratiti. To se posebno isplati kod obrazaca za unos koji preraspoređuju između „dvodijelnog“ (Tablet/PC) i „jednodijelnog“ (Phone) prikaza.

Varijante: Breakpoints prema formfaktoru umjesto samo prema širini

U stvarnim multiplatform-klijentima „širina“ često nije jedini odgovarajući signal. Korisne varijante:

  • Širina i visina: vrlo plitki prozori (npr. kase-terminale, podijeljeni ekrani) trebaju drugačija pravila.
  • Orijentacija: Landscape na tabletima je često „slično desktopu“, Portrait više „mobilno-like“.
  • Iskoristivi prostor Safe-Area: Na iOS/Androidu efektivna upotrebljiva visina može se značajno smanjiti sistemskim trakama. Tko gleda samo Height, ponekad routa „prekasno“.

Router je namjerno građen tako da možete zamijeniti funkciju Breakpoint-a. To je korisno i u legacy-situacijama, kada isti formular radi u više hostova (npr. jednom kao normalan prozor, jednom u ugrađenom containeru).

Neobično čisto: Layout-Routing kao „Transakcija“

Na većim ekranima problem manje leži u samim Breakpoint-ima, a više u redoslijedu UI-operacija. Praktičan obrazac je tretirati routing kao transakciju: prvo odlučiti, zatim premjestiti, pa tek onda uredno izvršiti nuspojave (vidljivost, fokus, osvježavanje podataka).

To konkretno znači: izbjegavajte da pojedina Controls tijekom premještanja sami triggeruju evente koji potom pokreću layout ili pristup podacima. U FMX to se događa npr. kada pri promjeni parenta fire-uje OnEnter/OnExit ili kada se LiveBinding-izraz ponovo evaluira zbog bounds-update-a. Ako primijetite takve efekte, pomaže centralni „Updating“ prekidač (kao u Routeru) plus jasan naknadni korak: tek nakon ApplyRoutes smiju se pokretati skupi procesi (npr. ponovno učitavanje liste, bindiranje detaljnog prikaza).

Posebno je relevantno za klijente s REST-pristupom: neplanirani reload tokom resize-a može generirati nepotrebne zahtjeve. To u LAN-u često nije vidljivo, ali preko VPN-a ili mobilno odmah jesu.

Kada se pristup isplati – i gdje ima ograničenja

Layout-Router se isplati kada:

  • FMX-aplikacija živi godinama i više razvijača radi na istim ekranima,
  • UI-blokovi se mogu jasno odvojiti (Sidebar/Details/Content),
  • trebate reproducibilna pravila za Breakpoints, umjesto ad-hoc podešavanja Align-a.

Granice postaju vidljive kada ekran mora biti izrazito „fluidan“ (mnogo dinamičkih pločica, pravi Masonry-izgledi). Tada su TFlowLayout/TGridPanelLayout ili vlastite klase layouta prikladnije. Također, ako se vrlo mnogo pojedinačnih kontrola prebacuje između slotova, održavanje ruta postaje neuredno – onda je bolje koristiti veće blokove ili uvesti deklarativni sloj konfiguracije (npr. JSON-konfiguracija za dodjele slotova koja se učitava pri pokretanju).

Zaključak: Za responzivne rasporede u FMX-u, „Umhängen mit Breakpoints“ je pragmatičan kompromis: manje nereda u dizajnu, jasna pravila, stabilna stanja. Ne zamjenjuje promišljenu UI-strukturu, ali pruža robustan okvir kojim možete kontrolisano dalje razvijati FMX-klijente u digitalnim korporativnim rješenjima preko različitih form-faktora.

Ako u postojećoj Delphi- ili FMX-aplikaciji želite čisto implementirati takvu arhitekturu layouta, bez rizika od UI-regresija u operativnim scenarijima, rado to možemo tehnički razvrstati s vama: razgovarajte o projektu ili zahvatu modernizacije s Net-Base.

U stručnom kontekstu Delphi Fmx Breakpoints i Firemonkey Layout također igraju važnu ulogu kada integracije, tokovi podataka i dalji razvoj moraju funkcionirati usklađeno.

Razgovarajte o projektu ili modernizacijskom zahvatu s Net-Base.

Sljedeći korak

Ako se tema pretvori u stvarni projekat, arhitekturu, postojeći sistem i operacije trebalo bi rano zajednički razmotriti.

Pružamo podršku ne samo pri pojedinačnim pitanjima, već i kada iz fragmenata izvornog koda, naslijeđenih sistema ili ideja za portal treba nastati robustan poslovni projekat.

  • Postojeće stanje, ciljno stanje i tehnički rizici procjenjuju se zajedno.
  • REST, pristup podacima, portali i Rollout neće se odgađati za kasnije faze.
  • Pravovremeno prepoznajete koji pristup je ekonomski i operativno održiv.

Podijeli objavu

Ovu objavu direktno proslijediti

LinkedIn, X, XING, Facebook, WhatsApp i e-pošta su odmah dostupni. Za Instagram pripremamo link i kratak tekst.

E-pošta

Instagram se otvara u novom tabu. Link i kratak tekst se prethodno kopiraju u međuspremnik.