Net-Base Revistë

03.06.2026

Layout-e responsivë në Delphi FMX: Breakpoints pa kaosin e Designer-it (me Layout-Router si fragment kodi burimor)

Responsive Layouts FMX bëhen shpejt të brishta në praktikë: stuhi ndryshimesh madhësie (resize), ndryshime DPI, rotacion dhe „Visible-Layouts“ krijojnë një gjendje të dyfishtë dhe reflows që janë të vështirë për t’u debugguar. Ky artikull tregon një Layout-Router me Breakpoints, i cili kontrollon blloqet UI në kohë ekzekutimi...

03.06.2026

Nga tema e revistës në praktikën e projektit

Faqe shërbimi dhe teknike të përshtatshme për artikullin

Kushdo që në Delphi duhet të mbulojë disa form-faktora në FireMonkey, shkon shpejt te Layout-e responsive FMX – dhe po aq shpejt përfundon me një përzierje kaskadash Align, kontejnerësh layout të fshehur dhe punoaround-e të designer-it që shemben në ndryshimin e ardhshëm të DPI-së ose rotacionit. Në klientë të software-it të rritur për biznes kjo është veçanërisht e pakëndshme: UI-ja zhvillohet më tej, ekipet ndryshojnë dhe papritmas logjika ndërlidhet me detaje vizuale.

Thelbi i problemit: FireMonkey ofron shumë blloqe ndërtimi (p.sh. Align, Anchors, TScaledLayout, TFlowLayout, TGridPanelLayout), por nuk ka një sistem „natyror“ Breakpoint si në web. Mund të reagosh ndaj ndryshimeve të përmasave, por pa një arkitekturë të qartë kjo përfundon në „if Width < … then …“ të shpërndara mbi shumë Forms.

Kënga tregon një ruteri i layoutit: një komponent i vogël që menaxhon centralisht Breakpoints dhe bën rishpërndarjen e Controls (ose blloqeve të plota layout) midis Slots të përgatitura. Qëllimi: gjendjet mbeten të paprekura, kodi është i mirëmbajtur, dhe rastet skajore si rotacioni, layout-et e zgjatura dhe Re-Entrancy amortizohen. Shtohen edhe disa truket më pak të dukshme që në praktikë bëjnë diferencën midis „funksionon në demo“ dhe „funksionon në mënyrë të qëndrueshme në prodhim“.

Pse Breakpoints në FMX janë të ndryshme nga në web

Në layout-et web Breakpoints janë zakonisht deklarative (CSS Media Queries). Në FMX vendimet e layout-it përgjithësisht merren në kohë ekzekutimi në mënyrë imperative: në OnResize bëhet kalimi. Këtu shtohen veçori specifike për platformën:

  • Device-Pixel vs. pixele logjike: ClientWidth/ClientHeight janë në njësi logjike (varet nga skalimi). Ndryshimet e DPI-së (p.sh. Windows Per-Monitor-DPI) mund të trigger-ojnë layout-et përsëri, pa ndonjë ndryshim „fizik“.
  • Rotacioni dhe Safe Areas: Platformat mobile ofrojnë Insets (Notch/Safe Area) – varësisht nga OS dhe Device. Një „Breakpoint vetëm sipas gjerësisë“ shpesh është i pamjaftueshëm, sepse sipërfaqja e përdorshme është më e vogël se madhësia e pastër e dritares.
  • Kalimi i layout-it: FireMonkey llogarit layout-et në faza. Nëse ndryshon Parent/Align në momentin e gabuar, krijohen efekte anësore (p.sh. reflow i shumëfishtë ose madhësi që pulsojnë).

Një ruteri i layoutit adreson këto duke (1) shkëputur „kur“ (Resize/Scale/Rotation) nga „si“ (rregullat e layout-it) dhe (2) duke përqendruar rregullat në një vend. Për drejtuesit teknikë efekti më i rëndësishëm është: marrin një qendër vendimmarrjeje të qartë dhe të verifikueshme në vend të shumë rasteve lokale të veçanta.

Arkitektura: Ruteri i layoutit me Slots në vend të krijimit të komponentëve

Truku i pastër për FMX është: jo të krijosh dinamikisht Controls të reja, por të varësh instancat ekzistuese midis Slots. Një Slot është thjesht një container (p.sh. TLayout) që përfaqëson një zonë të UI-së: Sidebar, Toolbar, Content, Footer, Details-Pane.

Avantazhet në software-in e përshtatur për ndërmarrje:

  • Gjendjet mbeten të paprekura (fusha teksti, pozicioni i scroll-it, elemente të përzgjedhura), sepse instancat nuk rindërtohen.
  • Më pak rrezik për lidhje të dyfishta të ngjarjeve, timer-ave ose bindings.
  • Rregullat e layout-it bëhen të dukshme: „cilin bllok ndodhet në cilin Slot“ mund të ndiqet dhe rishikohet për çdo Breakpoint.

E rëndësishme për praktikën: Pritini blloqet e UI mjaftë të mëdha. Nëse lëvizni 30 kontrolle të veçanta, lista e rutave vetë bëhet burim gabimesh. Më të përshtatshëm janë kontejnerë si layFilterBar, layNavigation, layResultList, layDetails.

Shembull burimi: Breakpoint-Router për layout-e responsive FMX

Kodi vijues është menduar si një njësie ndihmëse që mund ta përdorni në FMX-Forms. Ai llogarit një breakpoint (XS/SM/MD/LG/XL) dhe vendos kontrollet e përcaktuara në kontejnerët e slot-it të përcaktuar. Detaje të rëndësishme:

  • Debounce përmes TThread.ForceQueue: disa ngjarje Resize përmbledhen në një përditësim (më pak dridhje të UI-së, më pak cikle reflow).
  • Mbrojtje kundër re-entrancy: Përditësimi i layout-it shpesh shkakton përsëri Resize/Layout.
  • Opsionale: Orientimi (Portrait/Landscape) mund të përfshihet në logjikën e breakpoint-it.
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);

  // Një mapim: cili Control duhet të vendoset në cilin slot (container) për një breakpoint.
  TNBRoute = record
    Control: TControl;
    TargetSlot: TControl; // zakonisht TLayout ose 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; // rikalkulo manualisht
    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 nuk duhet të jenë 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: apliko vetëm një herë për 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;

    // Kujdes: ndryshimi i Parent ndryshon Z-Order.
    // Nëse rendi është i rëndësishëm, thërrisni DefineRoute në rendin e dëshiruar.
    for LRoute in LList do
    begin
      if (LRoute.Control.Parent <> LRoute.TargetSlot) then
        LRoute.Control.Parent := LRoute.TargetSlot;

      // Vendosni Align vetëm pasi të vendosni Parent; përndryshe Bounds mund të interpretohen ndryshe.
      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 qëllimisht të përgjithshme, sepse platformat e synuara të FMX ndryshojnë shumë.
  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.

Si të përdorni Router-in në një formë

Përcaktoni slot-et si TLayout (p.sh. layTop, layLeft, layContent) dhe regjistroni pastaj për çdo Breakpoint se cilat blloqe vendosen ku. Tipike është që Sidebar dhe Details-Pane në Breakpoint-et e vogla vendosen njëri nën tjetrin.

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;

Klasifikim: Pse „Umhängen“ shpesh është më i qëndrueshëm se Visible-Schalten

Një qasje e përhapur është të mbani pemë layout-i të ndara për çdo variant dhe të vetëm të toggl-oni Visible. Në dizajner kjo duket e rehatshme, por ka efekte anësore tipike:

  • Binding/Events të dyfishta: Dy kontrolle të ngjashme duhet të mbahen sinkron (p.sh. dy shirita filtrash).
  • Renditja e Tab-it dhe fokus: Gjatë ndërrimit humbni fokusin ose përfundoni në kontrolle të padukshme, nëse TabStop/HitTest janë të vendosura paaftësisht.
  • State Drift: Pozicionet e scroll-it, gjendjet e seleksionit ose tekstet e redaktuara divergojnë.

Umhängen ruan një instancë të vetme të qartë. E rëndësishme është të prisni blloqet e layout-it në mënyrë që të mund të zhvendosen në mënyrë të pavarur (p.sh. „Sidebar“ si një kontejner i veçantë në vend të shumë kontrolleve individuale). Kjo paguan në mirëmbajtje dhe analizë të gabimeve: ju debug-oni një instancë, jo dy UI paralela hije.

Rreziqet në praktikë (dhe si t’i debug-oni)

1) Stuhitë e Resize-it dhe Re-Entrancy

FMX triggert OnResize jo vetëm gjatë Resize-it nga përdoruesi, por edhe gjatë ndryshimeve të stileve, ndryshimeve të Parent-it dhe herë pas here gjatë ndryshimeve të DPI-së. Pa debounce aplikacioni ngec në cikle layout-i. Router-i përdor TThread.ForceQueue për të shtyrë ndryshimet në tick-un e ardhshëm të UI-së.

Sugjerim për debug: Logging (p.sh. përmes OutputDebugString) me breakpoint, madhësi dhe një counter për update ndihmon të gjeni ciklet e reflow-it. Nëse regjistroni gjithashtu kohën kur nis dhe përfundon ApplyRoutes, shihni shpejt nëse një resize i vetëm po „kaskadizohet“.

2) Z-Order, HitTest dhe „të padukshëm“ bllokues klikimesh

Ndërrimi i Parent-it ndryshon Z-Order. Nëse overlays (p.sh. Flyouts) nuk janë më të klikueshëm, shpesh shkaku është që mbi to ndodhet një kontejner Client-aligned dhe HitTest është aktiv. Variantë: për sipërfaqet overlay planifikoni qëllimisht një slot të veçantë krejt në krye dhe bëni parentimin e atyre kontrolleve vetëm aty. Në FMX HitTest (nëse një kontroll kap ngjarjet e mausit/prekjes) është shpesh shkaku më i shpeshtë sesa dukshmëria.

3) TGridPanelLayout dhe madhësi përqindore

TGridPanelLayout mund të shkaktojë ricalktime të papritura kur përdoren kolonat/rreshtat me përqindje në kombinim me Align=Client dhe ri-ngjitje dinamike. Nëse duhet të përdorni Grid, vendosni Grid-in në një slot dhe ri-ngjisni vetëm blloqet e plota të Grid-it, jo fëmijët e Grid-it. Kjo redukton kombinatorikën e paseve të layout-it.

4) Fokus, virtuelle Tastatur und „springende“ Eingabefelder

Një rast kufitar që shfaqet në aplikacionet FMX mobile dhe edhe në tableta Windows: gjatë ri-ngjitjes një Edit-Control i fokusuar mund të humbasë përkohësisht parent-in. Kjo mund të mbyllë tastierën virtuale ose të rivendosë kursorin. Praktikisht ka funksionuar mirë: ruani përkohësisht fokusin aktual para routing-ut (Focused/IFMXFocusControl) dhe riktheni fokusin pas routing-ut (në të njëjtin UI-Tick). Kjo ia vlen sidomos për forma inputi që kalojnë midis “dypjesë” (Tablet/PC) dhe “njëpjesë” (Phone).

Varianten: Breakpoints nach Formfaktor statt nur nach Breite

Në klientët multiplatform realë, vetëm “gjerësia” shpesh nuk është sinjali i duhur. Variantet e arsyeshme:

  • Gjerësia dhe lartësia: dritare shumë të sheshta (p.sh. terminale kasash, ekrane të ndara) kërkojnë rregulla të ndryshme.
  • Orientierung: Landscape në tableta shpesh është i ngjashëm me desktop, Portrait më tepër si në pajisje mobile.
  • Safe-Area-Nutzfläche: Në iOS/Android lartësia efektive e përdorshme mund të zvogëlohet ndjeshëm nga shirita sistemor. Kush merr parasysh vetëm Height, ndonjëherë routon “tepër vonë”.

Router-i është qëllimisht ndërtuar në mënyrë që të mund të zëvendësoni funksionin e Breakpoint-it. Kjo është e dobishme edhe në situata legacy, kur e njëjta formë ekzekutohet në disa host-e (p.sh. njëherë si dritare normale, njëherë në një container të ngulitur).

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

Në ekranet më të mëdha problemi zakonisht vjen jo nga Breakpoint-et vetë, por nga rendi i operacioneve UI. Një model praktik është të trajtoni routing-un si një transaksion: vendosni vendimin, pastaj ri-ngjisni, dhe më pas kryeni në mënyrë të rregullt efektet anësore (shfaqjen, fokusin, rifreskimin e të dhënave).

Konkrisht: shmangni që kontrollet individuale gjatë ri-ngjitjes të aktivizojnë event-e që nga ana tjetër nxisin layout ose akses në të dhëna. Në FMX kjo ndodh, për shembull, kur në ndryshim të parent-it triggohet OnEnter/OnExit ose një shprehje LiveBinding rievallohet për shkak të një update të bounds. Nëse vëreni efekte të tilla, ndihmon një çelës qendror “Updating” (si në Router) plus një hap i qartë pasues: vetëm pas ApplyRoutes lejohet që gjërat e shtrenjta të ekzekutohen (p.sh. rifillim i listës, lidhje e view-it të detajeve).

Veçanërisht në klientë me akses REST kjo është relevante: një reload i paqëllimshëm gjatë një resize-i mund të shkaktojë kërkesa të panevojshme. Në LAN nuk vërehet, por në VPN ose në lëvizje shfaqet menjëherë.

Wann sich der Ansatz lohnt – und wo er Grenzen hat

Layout-Router-i ia vlen kur:

  • një aplikacion FMX zhvillohet dhe mbahet për vite dhe disa zhvillues punojnë mbi të njëjtat ekrane,
  • blloqet UI mund të ndahen qartë (Sidebar/Details/Content),
  • keni nevojë për rregulla Breakpoint të riprodhueshme, në vend të tuning-ut ad-hoc të Align.

Kufizimet shfaqen kur një ekran duhet të jetë shumë „fluid“ (shumë kllika dinamike, Masonry-Layouts të vërteta). Atëherë TFlowLayout/TGridPanelLayout ose klasat e veta të layout-it janë më të përshtatshme. Edhe kur shumë komponentë të veçantë kalojnë midis slot-eve, mirëmbajtja e rrugëve bëhet e paqartë – atëherë më mirë të ndahen blloqe më të mëdha ose të futet një shtresë konfiguruese deklarative (p.sh. një konfigurim JSON për përcaktimet e slot-eve, i cili ngarkohet gjatë fillimit).

Përfundim: Për Responsive Layouts në FMX, „Umhängen mit Breakpoints“ është një zgjidhje pragmatike mesatare: më pak kaos në dizajn, rregulla të qarta, shtete të qëndrueshme. Ai nuk zëvendëson një strukturë UI të menduar mirë, por ju jep një kornizë të besueshme për të zhvilluar në mënyrë të kontrolluar klientët FMX brenda zgjidhjeve dixhitale të ndërmarrjeve përtej formateve të pajisjeve.

Nëse dëshironi të implementoni në mënyrë të pastër një arkitekturë të tillë layout në një aplikacion ekzistues Delphi- ose FMX, pa rrezikuar regresione UI në skenarë operativë, mund ta vlerësojmë teknikisht me ju: diskutoni projektin ose përpjekjen për modernizim me Net-Base.

Në kontekstin profesional, edhe Delphi Fmx Breakpoints dhe Firemonkey Layout luajnë një rol të rëndësishëm kur integrimet, rrjedhat e të dhënave dhe zhvillimi i mëtejshëm duhet të bashkëveprojnë në mënyrë të pastër.

Diskutoni projektin ose përpjekjen për modernizim me Net-Base.

Hapi tjetër

Kur nga një temë bëhet një projekt real, arkitektura, sistemi ekzistues dhe operimi duhet të merren në konsideratë së bashku që në fazat e hershme.

Wir unterstuetzen nicht nur bei Einzelfragen, sondern auch dann, wenn aus Source-Schnipseln, Legacy-Themen oder Portalideen ein belastbares Unternehmensprojekt werden soll.

  • Gjendja ekzistuese, imazhi i synuar dhe rreziqet teknike vlerësohen së bashku.
  • REST, akses në të dhëna, portalet dhe Rollout nuk shtyhen si pasoja të mëvonshme.
  • Ju e shihni herët se cila rrugë është e qëndrueshme ekonomikisht dhe operativisht.

Ndaje postimin

Shpërndaj këtë postim drejtpërdrejt

LinkedIn, X, XING, Facebook, WhatsApp dhe E‑Mail janë menjëherë të disponueshme. Për Instagram po përgatitim menjëherë lidhjen dhe tekstin e shkurtër.

Postë elektronike

Instagram hapet në një skedë të re. Linku dhe teksti i shkurtër kopjohen më parë në memorjen e kopjimit.