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/ClientHeightsu 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.
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.
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:
Landscapena 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.