Net-Base Revija

03.06.2026

Odzivne postavitve v Delphi FMX: breakpointi brez kaosa v Designerju (z Layout-Routerjem kot primerom izvorne kode)

Odzivne postavitve v FMX se v praksi hitro izkažejo za krhke: sunki spreminjanja velikosti, spremembe DPI, rotacija in „Visible-Layouts“ ustvarjajo dvojno stanje in težko razhroščljive reflowe. Ta prispevek prikazuje Layout-Router z Breakpoints, ki med izvajanjem nadzoruje UI-bloke...

03.06.2026

Od teme v reviji do projektne prakse

Ustrezne strani storitev in tehnični opisi k prispevku

Kdor mora v Delphi FireMonkey upravljati več formfaktorjev, hitro pristane pri Responsive Layouts FMX – in prav tako hitro pri mešanici Align-kaskad, skritih layout-kontejnerjev in Designer-workaroundov, ki pri naslednji spremembi DPI ali rotacije odpovedo. V zrelih poslovnih programskih odjemalcih je to še posebej neprijetno: UI se nadalje razvija, ekipe se menjajo in nenadoma je logika vezana na vizualne podrobnosti.

Jedro problema: FireMonkey nudi veliko gradnikov (npr. Align, Anchors, TScaledLayout, TFlowLayout, TGridPanelLayout), vendar nima „nativnega“ Breakpoint-Systems kot na spletu. Res je mogoče reagirati na spremembe velikosti, vendar brez jasne arhitekture to pripelje do „if Width < … then …“ razpršenega po številnih Forms.

Ta prispevek predstavlja en Layout-Router: majhno komponento, ki Breakpointe centralno upravlja in Controls (ali cele layout-bloke) premika med pripravljenimi Slots. Cilj: stanja ostanejo ohranjena, koda je vzdržna, in robni primeri, kot so rotacija, gnezdene postavitve in Re-Entrancy, so ublaženi. Poleg tega nekaj manj očitnih prijemov, ki v praksi ločijo „läuft im Demo“ od „läuft stabil im Betrieb“.

Zakaj so Breakpoints v FMX drugačni kot na spletu

V spletnih postavitvah so Breakpoints večinoma deklarativni (CSS Media Queries). V FMX so odločitve o postavitvi tipično imperativne ob izvajanju: pri OnResize se preklopi. Poleg tega so prisotne platformno specifične posebnosti:

  • Device-Pixel vs. logische Pixel: ClientWidth/ClientHeight so v logičnih enotah (odvisno od skaliranja). Spremembe DPI (npr. Windows Per-Monitor-DPI) lahko sprožijo novo postavitev, ne da bi se „fizično“ kaj spremenilo.
  • Rotacija in Safe Areas: Mobilne platforme zagotavljajo Insets (Notch/Safe Area) – odvisno od OS in Device. „Breakpoint, ki upošteva le širino“ je pogosto preozek pristop, saj je uporabna površina manjša od same velikosti okna.
  • Layout-Pass: FireMonkey izračunava postavitve v fazah. Če v napačnem trenutku spremenite Parent/Align, se pojavijo stranski učinki (npr. večkratni Reflow ali utripajoče velikosti).

Layout-Router naslovi to tako, da (1) odklopi „kdaj“ (Resize/Scale/Rotation) od „kako“ (Layout-Regeln) in (2) centralizira pravila na enem mestu. Za tehnične Leads je najpomembnejši učinek: dobijo jasno, preverljivo odločevalno središče namesto številnih lokalnih posebnih primerov.

Arhitektura: Layout-Router s Slots namesto Control-Erzeugung

Čist trik za FMX: ne dinamično Controls znova ustvarjati, temveč obstoječe Controls med Slots prestavljati. Slot je preprosto vsebnik (npr. TLayout), ki predstavlja del UI: Sidebar, Toolbar, Content, Footer, Details-Pane.

Prednosti v prilagojeni poslovni programski opremi:

  • Stanja ostanejo ohranjena (Edit-Text, Scrollposition, izbrani elementi), ker se instance ne ustvarjajo znova.
  • Manjše tveganje dvojne ožičitve dogodkov, Timerjev ali Bindings.
  • Layout-pravila postanejo pregledna: „kateri blok leži v katerem Slotu“ je mogoče slediti in pregledati za vsak Breakpoint.

Pomembno za prakso: razdelite UI-bloke dovolj grobo. Če premikate 30 posameznih kontrolnikov, sam seznam poti postane vir napak. Bolje so kontejnerji, kot so layFilterBar, layNavigation, layResultList, layDetails.

Izsek izvorne kode: Breakpoint-router za odzivne postavitve FMX

Naslednja koda je mišljena kot pomožna enota, ki jo lahko uporabite v FMX-formah. Izračuna breakpoint (XS/SM/MD/LG/XL) in pripne določene kontrolnike v določene slot-kontejnerje. Pomembne podrobnosti:

  • Debounce preko TThread.ForceQueue: več dogodkov Resize se združi v eno posodobitev (manj tresenja uporabniškega vmesnika, manj reflow-zank).
  • Zaščita pred ponovnim vstopom (Re-Entrancy): posodobitev postavitve pogosto sama ponovno sproži Resize/Layout.
  • Neobvezno: orientacija (Portrait/Landscape) se lahko vključi v logiko breakpointov.

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: kateri Control naj se pri posameznem Breakpointu postavi v kateri Slot (kontejner).
TNBRoute = record
Control: TControl;
TargetSlot: TControl; // ponavadi TLayout ali 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; // ročno ponovno izračunanje
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 smeta 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: uporabiti le enkrat na zanko sporočil
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: sprememba Parent spreminja Z-Order.
// Če je vrstni red pomemben, pokličite DefineRoute v želenem vrstnem redu.
for LRoute in LList do
begin
if (LRoute.Control.Parent <> LRoute.TargetSlot) then
LRoute.Control.Parent := LRoute.TargetSlot;

// Align nastaviti šele po nastavitvi Parent, sicer se lahko Bounds interpretira drugače.
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 so namerno grobi, saj se ciljna okolja FMX močno razlikujejo.
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 uporabiti Router v obrazcu

Definirate reže kot TLayout (npr. layTop, layLeft, layContent) in nato za vsak Breakpoint registrirate, kje kateri bloki ležijo. Tipično je, da se Sidebar in podokno s podrobnostmi pri majhnih breakpointih premikata eden pod drugim.

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;

Položaj v praksi: Zakaj je »premestitev« pogosto stabilnejša kot preklapljanje Visible

Razširjen pristop je imeti za vsako varianto ločena drevesa postavitve in le preklapljati Visible. V urejevalniku se zdi priročno, vendar ima tipične stranske učinke:

  • Dvojno vezanje/dogodki: Dva podobna kontrolnika je treba sinhronizirati (npr. dve vrstici filtrov).
  • Vrstni red zavihkov in fokus: Pri preklopu lahko izgubite fokus ali pristane v nevidnih kontrolnikih, če so TabStop/HitTest nepravilno nastavljeni.
  • Odstopanje stanja: Položaji drsenja, izbranost ali urejana besedila se lahko razhajajo.

Premestitev ohranja instanco enotno. Pomembno je razdeliti postavitvene bloke tako, da jih je mogoče neodvisno premikati (npr. obravnava „Sidebar“ kot lasten kontejner namesto številnih posameznih kontrolnikov). To se obrestuje pri vzdrževanju in analizi napak: odpravljate napake v eni instanci, ne v dveh vzporednih sencah UI.

Pasti v praksi (in kako jih odpraviti)

1) Nevroje spreminjanja velikosti in re-entrancy

FMX sproži OnResize ne le pri uporabniškem spreminjanju velikosti, temveč tudi pri zamenjavah stilov, spremembah staršev in deloma pri spremembah DPI. Brez odjave (debounce) se aplikacija ujame v zanke postavitve. Router uporablja TThread.ForceQueue, da spremembe prestavi v naslednji UI-tik.

Nasvet za odpravljanje napak: beleženje (npr. z OutputDebugString) z informacijami o breakpoints, velikosti in števcem posodobitev pomaga najti reflow-zanke. Če dodatno zabeležite čas začetka in konca ApplyRoutes, hitro vidite, ali en sam resize sproži kaskado nadaljnjih sprememb.

2) Z-Order, HitTest in „nevidni“ blokatorji klikov

Sprememba starša (Parent-Wechsel) spremeni Z-Order. Če overlayi (npr. Flyouts) ne sprejemajo klikov, je pogosto razlog v tem, da je nad njimi container poravnan kot Client in ima aktiven HitTest. Rešitev: za površine overlaya namenite namenski slot povsem na vrhu in tam parentajte takšne kontrolnike. V FMX je HitTest (ali kontrolnik prestreza miško-/touch-dogodke) pogosteje vzrok kot sama vidljivost.

3) TGridPanelLayout und prozentuale Größen

TGridPanelLayout lahko pri odstotnih stolpcih/vrsticah v kombinaciji z Align=Client in dinamičnim prevešanjem sproži nepričakovane prerazračune. Če morate uporabiti Grid, postavite Grid v slot in prevešajte le cele Grid-bloke, ne Grid-otrok. To zmanjša kombinatoriko layout-potek.

4) Fokus, virtualna tipkovnica und „poskakujoča“ vnosna polja

Gre za robni primer, ki se pojavlja v mobilnih FMX-aplikacijah in tudi na Windows-tablicah: pri prevešanju lahko osredotočeno Edit-Control za kratek čas izgubi Parent. To lahko zapre virtualno tipkovnico ali ponastavi kurzor. Praktično se je izkazalo: pred routanjem začasno shranite trenutni fokus (Focused/IFMXFocusControl), po routanju (v istem UI-tiku) fokus obnovite. To se posebej izplača pri vnosnih maskah, ki prehajajo med „dvostolpnim“ (Tablet/PC) in „enostolpnim“ (Phone) prikazom.

Variacije: Breakpointi po formfaktorju namesto samo po širini

V resničnih večplatformnih klientih sama „širina“ pogosto ni pravi signal. Smiselne različice:

  • Širina in višina: zelo plitka okna (npr. blagajniški terminali, deljeni zasloni) potrebujejo drugačna pravila.
  • Orientacija: Landscape na tablicah je pogosto „podobno namizju“, Portrait pa bolj „mobilno“.
  • Uporabna površina Safe-Area: na iOS/Android se lahko efektivna uporabna višina zaradi sistemskih vrstic občutno skrči. Kdor upošteva le Height, včasih routa „prepozno“.

Router je namerno zgrajen tako, da lahko zamenjate funkcijo breakpointov. To je tudi koristno v legacy-situacijah, ko ista forma teče v več hostih (npr. enkrat kot navadno okno, drugič v vgrajenem kontejnerju).

Neobičajno čisto: Layout-Routing kot „transakcija“

Pri večjih zaslonih težava manj leži v samih breakpointih kot v vrstnem redu UI-operacij. Praxistip je obravnavati routing kot transakcijo: najprej odločite, nato preklopite, nato stranske učinke (Visibility, Fokus, osvežitev podatkov) urejeno izvedete.

To pomeni v praksi: izogibajte se, da posamezni controli med prevešanjem sprožajo lastne dogodke, ki nato sprožijo layout ali dostop do podatkov. V FMX se to zgodi na primer, ko ob menjavi Parent-a sprožita OnEnter/OnExit ali ko se LiveBinding-izraz ob posodobitvi Bounds ponovno evaluira. Če opažate takšne učinke, pomaga centralni „Updating“-stikalo (kot v Routerju) plus jasen post-korak: šele po ApplyRoutes smejo drage operacije teči (npr. ponovno nalaganje seznama, vezava pogleda podrobnosti).

Posebno pri klientih z dostopom do REST je to pomembno: nezaželen reload med spreminjanjem velikosti lahko sproži nepotrebne zahteve. To v LANu ni očitno, v VPN ali mobilnem okolju pa takoj.

Kdaj se pristop izplača – und kje ima omejitve

Layout-Router se izplača, kadar:

  • FMX-aplikacija živi več let in več razvijalcev dela na istih zaslonih,
  • UI-bloki se lahko jasno ločijo (Sidebar/Details/Content),
  • potrebujete reproducibilna pravila breakpointov namesto ad-hoc prilagajanja Align-lastnosti.

Meje boste začutili, ko mora biti zaslon močno „fluid“ (množica dinamičnih ploščic, resnični Masonry-postavitve). V takih primerih so TFlowLayout/TGridPanelLayout ali lastni razredi za postavitve primernejši. Tudi če se zelo veliko posameznih kontrol premešča med sloti, postane vzdrževanje poti nepregledno – takrat je bolje razrezati večje bloke ali uvesti deklarativno konfiguracijsko plast (npr. JSON-konfiguracijo za dodelitve slotov, ki se naloži ob zagonu).

Zaključek: Za responsive postavitve v FMX je „Umhängen mit Breakpoints“ pragmatična srednja pot: manj kaosa v oblikovalniku, jasna pravila, stabilna stanja. Ne nadomešča premišljene UI-strukture, vendar vam zagotavlja zanesljivo ogrodje za kontroliran razvoj FMX-klientov v digitalnih podjetniških rešitvah prek različnih formfaktorjev.

Če želite v obstoječi Delphi- ali FMX-aplikaciji takšno arhitekturo postavitve dosledno uvesti, brez tveganja UI-regresij v obratovalnih scenarijih, lahko to tehnično ovrednotite z nami: projekt ali modernizacijski načrt razpravljajte z Net-Base.

V strokovnem kontekstu imajo tudi Delphi Fmx Breakpoints in Firemonkey Layout pomembno vlogo, kadar je treba integracije, podatkovne tokove in nadaljnji razvoj uskladiti.

O projektu ali modernizacijskem načrtu se pogovorite z Net-Base.

Naslednji korak

Ko se tema spremeni v dejanski projekt, je treba arhitekturo, obstoječi sistem in obratovanje zgodaj obravnavati skupaj.

Ne podpiramo le pri posameznih vprašanjih, ampak tudi takrat, ko iz izrezkov izvorne kode, legacy-tem ali idej za portale nastane zanesljiv podjetniški projekt.

  • Obstoječe stanje, ciljno stanje in tehnična tveganja se ocenjujejo skupaj.
  • REST, dostop do podatkov, portali in uvedba niso prestavljeni kot poznejše posledice.
  • Zgodaj prepoznate, katera pot je ekonomsko in obratovalno vzdržna.

Deli objavo

Deli ta prispevek neposredno

LinkedIn, X, XING, Facebook, WhatsApp in e-pošta so takoj na voljo. Za Instagram bomo neposredno pripravili povezavo in kratek opis.

E-pošta

Instagram se odpre v novem zavihku. Povezava in kratek opis se pred tem kopirata v odložišče.