De la tema din revistă la practica în proiecte
Pagini relevante de servicii și pagini tehnice pentru articol
Cine trebuie să deservească mai mulți form-factori în Delphi FireMonkey ajunge rapid la Responsive Layouts FMX – și la fel de repede la un amestec de Align-Kaskaden, containere de layout ascunse și Designer-Workarounds, care se prăbușesc la următoarea schimbare de DPI sau rotație. În clienți de business software maturi acest lucru este deosebit de neplăcut: UI-ul este dezvoltat în continuare, echipele se schimbă și, dintr-o dată, logica depinde de detalii vizuale.
Nucleul problemei: FireMonkey oferă multe componente (z. B. Align, Anchors, TScaledLayout, TFlowLayout, TGridPanelLayout), dar nu un sistem „nativ” de Breakpoints ca în web. Se poate reacționa la schimbările de dimensiune, dar fără o arhitectură clară asta se sfârșește în „if Width < … then …” împrăștiat peste multe formulare.
Acest articol prezintă un Layout-Router: o componentă mică care gestionează central Breakpoints și reatașează Controls (sau blocuri întregi de layout) între Slots pregătite. Scop: stările rămân păstrate, codul este întreținut, iar cazurile-limită precum rotația, layout-urile închise și Re-Entrancy sunt amortizate. În plus, câteva trucuri mai puțin evidente care, în practică, fac diferența între „rulează în demo” și „rulează stabil în producție”.
De ce Breakpoints în FMX sunt diferite față de web
În layout-urile web Breakpoints sunt de obicei declarative (CSS Media Queries). În FMX deciziile de layout sunt, de regulă, imperative la runtime: în OnResize se face comutarea. La acestea se adaugă particularități specifice platformei:
- Device-Pixel vs. pixeli logici:
ClientWidth/ClientHeightsunt în unități logice (dependente de scalare). Schimbările de DPI (z. B. Windows Per-Monitor-DPI) pot retrigera layout-urile, fără ca ceva „fizic” să se schimbe. - Rotație și Safe Areas: Platformele mobile furnizează inseturi (Notch/Safe Area) – în funcție de OS și Device. Un „Breakpoint doar pe bază de lățime” este adesea prea limitat, pentru că suprafața utilizabilă este mai mică decât dimensiunea pură a ferestrei.
- Layout-Pass: FireMonkey calculează layout-urile în faze. Dacă schimbi Parent/Align în momentul nepotrivit apar efecte secundare (z. B. reflow multiplu sau dimensiuni care clipesc).
Un Layout-Router abordează aceasta prin (1) decuplarea „Când” (Resize/Scale/Rotation) de „Cum” (reguli de layout) și (2) concentrarea regulilor într-un singur loc. Pentru liderii tehnici efectul cel mai important: obțin un centru de decizie clar și verificabil în locul multor cazuri particulare locale.
Arhitectură: Layout-Router cu Slots în locul creării de Controls
Trucul curat pentru FMX: nu recrea dinamic Controls, ci reatașează instanțele existente între Slots. Un Slot este pur și simplu un container (z. B. TLayout) care reprezintă o zonă a UI: Sidebar, Toolbar, Content, Footer, Details-Pane.
Avantaje în software personalizat pentru întreprinderi:
- Stările rămân păstrate (Edit-Text, Scrollposition, elemente selectate), pentru că instanțele nu sunt reconstruite.
- Mai puțin risc de dublă legare a evenimentelor, timer-elor sau binding-urilor.
- Regulile de layout devin vizibile: „care bloc se află în ce Slot” poate fi urmărit și revizuit pentru fiecare Breakpoint.
Important în practică: împărțiți blocurile UI suficient de grosier. Dacă reatașați 30 de controale individuale, lista de rute însăși devine o sursă de erori. Mai potrivite sunt containere precum layFilterBar, layNavigation, layResultList, layDetails.
Fragment de cod sursă: Breakpoint-Router pentru layout-uri responsive FMX
Codul următor este conceput ca o unitate de asistență pe care o puteți folosi în formulare FMX. Calculează un breakpoint (XS/SM/MD/LG/XL) și reașează controalele definite în containere-slot definite. Detalii importante:
- Debounce prin
TThread.ForceQueue: mai multe evenimente de redimensionare sunt grupate într-o singură actualizare (mai puțină instabilitate a UI-ului, mai puține cicluri de reflow). - Protecție împotriva re-entrării: actualizarea layout-ului declanșează adesea din nou Resize/Layout.
- Opțional: Orientare (Portret/Peisaj) poate intra în logica breakpoint-urilor.
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);
// Mapare: care Control trebuie plasat în ce slot (container) pentru un breakpoint.
TNBRoute = record
Control: TControl;
TargetSlot: TControl; // de obicei TLayout sau 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; // recalculare manuală
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 nu trebuie să fie 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: doar o dată pe bucla de mesaje
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;
// Atenție: schimbarea Parent modifică ordinea Z.
// Dacă ordinea este relevantă, apelați DefineRoute în ordinea dorită.
for LRoute in LList do
begin
if (LRoute.Control.Parent <> LRoute.TargetSlot) then
LRoute.Control.Parent := LRoute.TargetSlot;
// Align trebuie setat după Parent; altfel, Bounds pot fi interpretate diferit.
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;
// Breakpoint-urile sunt intenționat grosiere, deoarece platformele țintă FMX variază puternic.
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.
Cum utilizați routerul într-un formular
Definiți sloturi ca TLayout (de ex. layTop, layLeft, layContent) și înregistrați apoi pentru fiecare breakpoint unde se află blocurile. Este tipic ca bara laterală și panoul de detalii să se alinieze unul sub altul la breakpoints mici.
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;Context: De ce „reatasarea“ este adesea mai stabilă decât comutarea Visible
O abordare comună este să păstrați câte un arbore de layout separat pentru fiecare variantă și să comutați doar Visible. Pare comod în Designer, dar are efecte secundare tipice:
- Binding/Events dublate: Două controale similare trebuie menținute sincron (de ex. două bare de filtrare).
- Ordinea de tabulare și focus: La comutare se poate pierde focusul sau se poate ajunge pe controale invizibile dacă TabStop/HitTest sunt setate nefavorabil.
- Deriva de stare: Pozițiile de defilare, stările de selecție sau textele editate pot diverge.
Reatașarea păstrează o singură instanță. Important este să fragmentați blocurile de layout astfel încât să poată fi mutate independent (de ex. „Sidebar“ ca un container propriu în loc de multe controale individuale). Exact aceasta se dovedește avantajoasă în mentenanță și în analiza erorilor: depanați o singură instanță, nu două UI-uri „umbră” paralele.
Capcane practice (și cum să le depanați)
1) Valuri de redimensionare și re-entrancy
FMX declanșează OnResize nu numai la redimensionarea de către utilizator, ci și la schimbări de stil, la modificări ale părintelui și uneori la schimbări DPI. Fără debounce, aplicația poate rămâne blocată în bucle de layout. Routerul folosește TThread.ForceQueue pentru a amâna schimbările până la următorul tick UI.
Sfat de debugging: Logging (de ex. prin OutputDebugString) cu breakpoint, dimensiune și un contor de update ajută la identificarea buclelor de reflow. Dacă logați și momentul în care ApplyRoutes pornește și se termină, vedeți rapid dacă o singură redimensionare provoacă o cascadă.
2) Z-Order, HitTest și blocanți de click „invizibili”
Schimbarea părintelui modifică Z-Order. Dacă overlay-urile (de ex. Flyouts) nu mai primesc click, de multe ori cauza este prezența unui container client-aligned deasupra cu HitTest activ. Variantă: alocați intenționat un slot separat, plasat în vârf, pentru suprafețele de overlay și parentați astfel de controale doar acolo. În FMX HitTest (dacă un control interceptează evenimente mouse/touch) este frecvent cauza, mai des decât vizibilitatea.
3) TGridPanelLayout și dimensiuni procentuale
TGridPanelLayout poate declanșa recalculări neașteptate în cazul coloanelor/linilor procentuale în combinație cu Align=Client și reatașarea dinamică. Dacă trebuie să folosiți Grid, plasați Grid-ul într-un slot și reatașați doar blocuri întregi de Grid, nu copiii Grid-ului. Aceasta reduce combinatorica trecerilor de layout.
4) Focalizare, tastatura virtuală și câmpuri de introducere „sărind”
Un caz marginal, care apare în aplicațiile FMX mobile și și pe Windows-tablete: la reatașare un Edit-Control focalizat poate pierde temporar parent-ul. Asta poate închide tastatura virtuală sau reseta cursorul. S-a dovedit practic: înainte de rutare salvați temporar focalizarea curentă (Focused/IFMXFocusControl), după rutare (în același UI-Tick) restaurați focalizarea. Merită mai ales pentru formulare de intrare care trec între „pe două coloane” (Tablet/PC) și „pe o coloană” (Phone).
Variante: Breakpoints nach Formfaktor statt nur nach Breite
În clienți multiplatformă reali, „lățimea” singură adesea nu este semnalul corect. Variante utile:
- Lățime și înălțime: ferestre foarte plate (de ex. terminale POS, ecrane împărțite) necesită reguli diferite.
- Orientare:
Landscapepe tablete este adesea „asemănător desktopului”, Portrait mai degrabă „de tip mobil”. - Aria utilă (Safe-Area): pe iOS/Android înălțimea efectiv utilizabilă poate scădea semnificativ din cauza barelor de sistem. Cine ia în calcul doar
Heightuneori face rutare „prea târziu”.
Routerul este conceput intenționat astfel încât să puteți înlocui funcția de breakpoint. Acest lucru este util și în situații legacy, când același formular rulează în mai multe gazde (de ex. o dată ca fereastră normală, o dată într-un container încorporat).
Neobișnuit de curat: Layout-Routing ca „Tranzacție”
Pe ecrane mai mari problema nu cade atât pe breakpoints în sine, cât pe ordinea operațiunilor UI. Un model practic este tratarea rutării ca pe o tranzacție: mai întâi decideți, apoi reatașați, apoi executați în mod ordonat efectele secundare (vizibilitate, focalizare, reîmprospătare a datelor).
Concret înseamnă: evitați ca controale individuale să declanșeze în timpul reatașării evenimente proprii care, la rândul lor, pornesc layout sau acces la date. În FMX aceasta se întâmplă, de exemplu, când la schimbarea parent-ului OnEnter/OnExit declanșează sau când o expresie LiveBinding este re-evaluată din cauza unui update de bounds. Dacă observați astfel de efecte, ajută un comutator central „Updating” (ca în Router) plus un pas post clar: abia după ApplyRoutes ar trebui să ruleze lucruri costisitoare (de ex. reîncărcarea listei, legarea unei vederi detaliu).
Mai ales la clienți cu acces REST asta este relevant: un reload nedorit în timpul unui redimensionări poate duce la cereri inutile. În LAN nu se observă, dar în VPN sau mobil imediat.
Când merită abordarea – și unde are limite
Routerul de layout merită folosit când:
- o aplicație FMX rămâne în exploatare ani de zile și mai mulți dezvoltatori lucrează la aceleași ecrane,
- blocuri UI pot fi separate clar (Sidebar/Details/Content),
- aveți nevoie de reguli de breakpoint reproductibile, în loc de ajustări
Alignad-hoc.
Veți întâlni limite atunci când un ecran trebuie să fie foarte „fluid” (multe plăci dinamice, layout-uri Masonry reale). Atunci sunt mai potrivite TFlowLayout/TGridPanelLayout sau clase de layout proprii. De asemenea, dacă foarte multe controale individuale schimbă între slot-uri, mentenanța rutelor devine neclară – în acest caz e mai bine să împărțiți în blocuri mai mari sau să introduceți un strat de configurare declarativă (de ex. o configurație JSON pentru asocieri de slot-uri, care se încarcă la pornire).
Concluzie: Pentru layout-uri responsive FMX, „Umhängen mit Breakpoints” este un compromis pragmatic: mai puțin haos în designer, reguli clare, stări stabile. Nu înlocuiește o structură UI bine gândită, dar vă oferă un cadru solid pentru a dezvolta controlat clienții FMX în soluții digitale pentru întreprinderi peste factorii de formă.
Dacă doriți să implementați corect o astfel de arhitectură de layout într-o aplicație existentă Delphi sau FMX, fără a risca regresii UI în scenarii de operare, puteți să o evaluăm tehnic împreună: discutați proiectul sau demersul de modernizare cu Net-Base.
În contextul profesional, și Delphi Fmx Breakpoints și Firemonkey Layout joacă un rol important, atunci când integrările, fluxurile de date și dezvoltarea ulterioară trebuie să funcționeze coerent.
Discutați proiectul sau demersul de modernizare cu Net-Base.
Următorul pas
Când o temă devine un proiect real, arhitectura, infrastructura existentă și operarea trebuie analizate împreună de la început.
Nu oferim sprijin doar pentru întrebări punctuale, ci și atunci când fragmente de cod sursă, probleme legacy sau idei de portal trebuie transformate într-un proiect robust la nivel de companie.
- Situația curentă, starea țintă și riscurile tehnice sunt evaluate împreună.
- REST, accesul la date, portalurile și Rollout nu sunt amânate ca consecințe ulterioare.
- Veți vedea din timp ce cale este viabilă din punct de vedere economic și operațional.