Net-Base Časopis

03.06.2026

Responzivni rasporedi u Delphi FMX: breakpointi bez kaosa u Designeru (s Layout-Routerom kao izrezak koda)

Responzivni layouti u FMX-u u praksi brzo postaju krhki: valovi promjena veličine, promjene DPI-ja, rotacije i „Visible-Layouts“ stvaraju duplicirano stanje i teško otklonjive reflowove. Ovaj članak prikazuje Layout-Router s breakpointima koji kontrolira UI-blokove za vrijeme izvođenja...

03.06.2026

Od teme magazina do projektne prakse

Povezane stranice usluga i tehnologije za članak

Tko u Delphi FireMonkey mora podržavati više form-faktora, brzo naiđe na Responsive Layouts FMX – i podjednako brzo na mješavinu Align-Kaskaden, skrivenih layout-kontejnera i designer-workarounta koji se raspadnu pri sljedećoj promjeni DPI-ja ili rotacije. U rastućim poslovnim softverskim klijentima to je posebno nezgodno: UI se dalje razvija, timovi se mijenjaju i odjednom logika ovisi o vizualnim detaljima.

Suština problema: FireMonkey nudi mnoge građevne blokove (npr. Align, Anchors, TScaledLayout, TFlowLayout, TGridPanelLayout), ali nema „nativan“ breakpoint-sustav kao na webu. Može se reagirati na promjene veličine, ali bez jasne arhitekture to završi kao „if Width < … then …“ razbacano po mnogim formama.

Ovaj članak prikazuje jedan Layout-Router: malu komponentu koja centralno upravlja breakpointima i prebacuje Controls (ili cijele 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žuju. Uz to dolazi nekoliko manje očitih trikova koji u praksi čine razliku između „radi u demo verziji“ i „radi stabilno u produkciji“.

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

U web-layoutima su Breakpoints većinom deklarativni (CSS Media Queries). U FMX-u se odluke o layoutu tipično donose imperativno za vrijeme izvođenja: u OnResize se izvršava prebacivanje. Uz to postoje specifičnosti po platformama:

  • Device-Pixel vs. logische Pixel: ClientWidth/ClientHeight su u logičkim jedinicama (ovisno o skaliranju). Promjene DPI-ja (npr. Windows Per-Monitor-DPI) mogu ponovno pokrenuti layout bez da se „fizički“ išta promijeni.
  • Rotacija i Safe Areas: Mobilne platforme isporučuju insete (Notch/Safe Area) – ovisno o OS-u i uređaju. „Breakpoint samo po širini“ često je preusko razmišljanje jer je korisni prostor manji od same veličine prozora.
  • Layout-Pass: FireMonkey obračunava layoute u fazama. Ako se Parent/Align mijenja u pogrešnom trenutku, nastaju nuspojave (npr. višestruki reflow ili trepereće veličine).

Layout-Router to rješava tako što (1) odvaja „kada“ (Resize/Scale/Rotation) od „kako“ (pravila layouta) i (2) koncentrira pravila na jednom mjestu. Za tehničke voditelje najvažniji efekt je: dobivaju jasno, provjerljivo središte odluka umjesto mnogih lokalnih posebnih slučajeva.

Arhitektura: Layout-Router mit Slots statt Control-Erzeugung

Čisti trik za FMX: ne dinamički stvarati kontrole, nego premještati postojeće Controls između slotova. Slot je jednostavno kontejner (npr. TLayout) koji predstavlja dio sučelja: bočna traka, alatna traka, sadržaj, podnožje, panel detalja.

Prednosti u prilagođenom poslovnom softveru:

  • Stanja ostaju sačuvana (Edit-Text, pozicija skrola, selektirani elementi), jer se instance ne ponovo stvaraju.
  • Manje rizika od dvostrukog ožičenja Events, Timera ili Bindings.
  • Pravila layouta postaju vidljiva: „koji blok leži u kojem slotu“ moguće je za svaki Breakpoint pratiti i pregledati.

Važno za praksu: razdvajajte UI-blokove dovoljno krupno. Ako premještate 30 pojedinačnih kontrola, sama lista ruta postat će izvor pogrešaka. Bolji su kontejneri poput layFilterBar, layNavigation, layResultList, layDetails.

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

Sljedeći kod zamišljen je kao pomoćna jedinica koju možete koristiti u FMX-formama. Izračunava breakpoint (XS/SM/MD/LG/XL) i premješta definirane kontrole u definirane slot-kontejnere. Važni detalji:

  • Debounce preko TThread.ForceQueue: više Resize-događaja se objedini u jedno ažuriranje (manje treperenja UI-ja, manje reflow-petlji).
  • Re-Entrancy-Schutz: ažuriranje rasporeda često samo ponovno pokreće Resize/Layout.
  • Opcionalno: orijentacija (Portrait/Landscape) može utjecati na logiku breakpointa.
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);

  // Mapiranje: koji Control treba biti u kojem slotu (kontejneru) za određeni breakpoint.
  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; // 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: primijeniti samo jednom po petlji poruka
  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-poredak.
    // 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 FMX ciljne platforme znatno variraju.
  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

Definirate slotove kao TLayout (npr. layTop, layLeft, layContent) i zatim za svaki Breakpoint registrirate gdje se koji blokovi nalaze. Tipično je da se Sidebar i Details-Pane u malim Breakpointima premještaju jedan ispod drugog.

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 „premještanje“ često djeluje stabilnije od prebacivanja svojstva Visible

Uobičajen pristup je držati zasebna stabla layouta za svaku varijantu i samo prebacivati Visible. To u Designeru izgleda praktično, no ima tipične nuspojave:

  • Duplo Binding/Events: Dva slična kontrola moraju se održavati sinkronizirano (npr. dvije filter trake).
  • Tab-Reihenfolge und Fokus: Pri prebacivanju gubi se fokus ili se dospije u nevidljive kontrole ako su TabStop/HitTest nepovoljno postavljeni.
  • State Drift: Pozicije scrolla, stanja selekcije ili uređivani tekstovi divergiraju.

Premještanje zadržava jedinstvenu instancu. Važno je segmentirati layout-blokove tako da se mogu neovisno premještati (npr. „Sidebar“ kao zaseban container umjesto brojnih pojedinačnih kontrola). Upravo se to isplati u održavanju i analizi pogrešaka: debugirate jednu instancu, ne dvije paralelne sjenovite UI-je.

Uobičajene zamke u praksi (i kako ih debugirati)

1) Resize-naleti i Re-Entrancy

FMX triggert OnResize ne samo pri promjeni veličine od strane korisnika, nego i pri promjenama stila, promjenama roditelja i dijelom pri promjenama DPI-a. Bez debounce-a aplikacija zapada u layout-petlje. Router koristi TThread.ForceQueue da pomakne primjenu promjena na sljedeći UI-tick.

Debugging-savjet: logiranje (npr. preko OutputDebugString) zajedno s breakpointom, veličinom i brojačem update-a pomaže pronaći reflow-petlje. Ako dodatno zabilježite trenutak kad ApplyRoutes počinje i završava, brzo ćete vidjeti hoće li pojedinačni resize „kaskadirati“.

2) Z-Order, HitTest und „nevidljivi“ blokatori klika

Promjena roditelja mijenja Z-Order. Ako Overlays (npr. Flyouts) više ne primaju klikove, često je razlog što se iznad nalazi client-aligned kontejner s aktivnim HitTest-om. Rješenje: za overlay-površine namjerno predvidjeti zaseban slot na vrhu i samo tamo postavljati takve kontrole kao Parent. U FMX-u je HitTest (da li kontrola presreće mouse-/touch-događaje) češći uzrok nego sama vidljivost.

3) TGridPanelLayout und prozentuale Größen

TGridPanelLayout može pri postotnim stupcima/redovima u kombinaciji s Align=Client i dinamičkim premještanjem roditelja izazvati neočekivane preračune. Ako morate koristiti Grid, smjestite Grid u slot te premještajte samo cijele Grid-blokove, ne Grid-djecu. To smanjuje kombinatoriku layout-pasa.

4) Fokus, virtuelle Tastatur und „springende“ Eingabefelder

Rubni slučaj koji se javlja u mobilnim FMX-aplikacijama i također na Windows-tabletima: pri premještanju roditelja fokusirano Edit-control može kratko izgubiti parent. To može zatvoriti virtualnu tipkovnicu ili resetirati kursor. Prakticno se pokazalo: prije rutiranja privremeno spremiti trenutni fokus (Focused/IFMXFocusControl), nakon rutiranja (u istom UI-ticku) vratiti fokus. To se posebno isplati kod obrazaca za unos koji se prebacuju između prikaza u dvije kolone (Tablet/PC) i jedne kolone (Phone).

Varianten: Breakpoints nach Formfaktor statt nur nach Breite

U stvarnim multiplatformskim klijentima „širina“ sama po sebi često nije ispravan signal. Korisne varijante:

  • Breite und Höhe: vrlo plitki/niski prozori (npr. blagajnički terminali, podijeljeni zasloni) zahtijevaju drugačija pravila.
  • Orientierung: Landscape na tabletima često je „desktop-ähnlich“, Portrait je češće „mobile-like“.
  • Safe-Area-Nutzfläche: Na iOS/Android efektivno iskoristiva visina može se značajno smanjiti zbog sistemskih traka. Tko promatra samo Height, ponekad routa „prekasno“.

Router je namjerno konstruiran tako da možete zamijeniti funkciju breakpointa. To je posebno korisno u legacy-situacijama kada ista forma radi u više hostova (npr. jednom kao normalan prozor, drugi put u ugrađenom kontejneru).

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

Na većim ekranima problem manje proizlazi iz samih breakpointa, a više iz redoslijeda UI-operacija. Praktičan obrazac je tretirati routing kao transakciju: prvo odlučiti, zatim premjestiti roditelje, potom uredno izvesti nuspojave (vidljivost, fokus, osvježavanje podataka).

Konkretno to znači: izbjegavajte da pojedinačni controls tijekom premještanja roditelja pokreću vlastite evente koji zauzvrat pokreću layout ili pristup podacima. U FMX se to, primjerice, događa kada pri promjeni parenta OnEnter/OnExit zatrepere ili se LiveBinding-izraz ponovno evaluira zbog updatea bounds-a. Ako vidite takve efekte, pomaže centralni „Updating“-prekidač (kao u routeru) plus jasan naknadni korak: tek nakon ApplyRoutes smiju se pokretati skupe stvari (npr. ponovno učitavanje liste, vezanje detaljnog prikaza).

Posebno je relevantno kod klijenata s pristupom REST: nenamjerno reloadanje tijekom promjene veličine može dovesti do nepotrebnih zahtjeva. To u LAN-u možda neće biti vidljivo, ali preko VPN-a ili mobilno odmah jest.

Wann sich der Ansatz lohnt – und wo er Grenzen hat

Layout-Router se isplati kada:

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

Granice postaju vidljive kad ekran mora biti snažno „fluidan“ (mnogo dinamičkih pločica, pravi Masonry-rasporedi). Tada su TFlowLayout/TGridPanelLayout ili vlastite klase rasporeda primjerenije. Ako se također vrlo mnogo pojedinačnih kontrola prebacuje između slotova, održavanje ruta postaje nepregledno – tada 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 „prebacivanje s Breakpointima“ je pragmatičan srednji put: manje nereda u dizajnu, jasna pravila, stabilna stanja. Ne zamjenjuje promišljenu UI-strukturu, ali pruža robustan okvir koji vam omogućuje kontrolirano daljnje razvijanje FMX-klijenata u digitalnim poslovnim rješenjima preko različitih formata uređaja.

Ako u postojećoj Delphi- ili FMX-aplikaciji želite takvu arhitekturu rasporeda uredno provesti, a da pritom ne riskirate UI-regresije u operativnim scenarijima, možete to slobodno tehnički s nama razvrstati: razgovarajte o projektu ili modernizacijskom pothvatu s Net-Base.

U stručnom kontekstu Delphi Fmx Breakpoints i Firemonkey Layout također igraju važnu ulogu kada se integracije, tokovi podataka i daljnji razvoj moraju uredno uskladiti.

Razgovarajte o projektu ili modernizacijskom pothvatu s Net-Base.

Sljedeći korak

Kad se tema pretvori u stvarni projekt, arhitektura, postojeći sustav i operativni rad trebaju se rano sagledati zajedno.

Podržavamo vas ne samo u pojedinačnim pitanjima, već i kada iz isječaka izvornog koda, naslijeđenih sustava ili ideja za portale treba nastati pouzdan poslovni projekt.

  • Postojeće stanje, ciljna slika i tehnički rizici procjenjuju se zajedno.
  • REST, pristup podacima, portali i Rollout neće biti odgođeni kao kasne posljedice.
  • Vidite rano koji je put ekonomski i operativno održiv.

Podijeli objavu

Izravno proslijedite ovu objavu

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

E-pošta

Instagram se otvara u novoj kartici. Link i kratki tekst se prethodno kopiraju u međuspremnik.