Nuo žurnalo temos iki projekto įgyvendinimo
Tinkami puslapiai apie paslaugas ir techninę informaciją šiam įrašui
Jei Delphi FireMonkey reikia aptarnauti kelis formatus, greitai pasirenkama Responsive Layouts FMX – ir taip pat greitai susiduriama su Align-kaskadomis, paslėptais išdėstymo konteineriais ir dizainerio apėjimais, kurie sutrinka prie kito DPI ar sukimosi pasikeitimo. Brandžiuose verslo programinės įrangos klientuose tai ypač nepatogu: vartotojo sąsaja toliau vystoma, komandos keičiasi, ir staiga logika priklauso nuo vizualinių detalių.
Problemos esmė: FireMonkey siūlo daug komponentų (pvz. Align, Anchors, TScaledLayout, TFlowLayout, TGridPanelLayout), bet neturi „natyvios“ Breakpoint-sistemos kaip žiniatinklyje. Nors galima reaguoti į dydžio pasikeitimus, be aiškios architektūros tai baigiasi „if Width < … then …“ sąlygomis, pasklidusiomis per daugelį formų.
Šis įrašas pristato Layout-Router: nedidelę komponentę, kuri centralizuotai valdo Breakpoint’us ir perkelia valdiklius (arba visus išdėstymo blokus) tarp paruoštų Slots. Tikslas: būsenos išlieka, kodas yra prižiūrimas, o kraštutiniai atvejai, tokie kaip rotacija, įdėtiniai išdėstymai ir re-entrancy, yra amortizuojami. Be to pateikiamos kelios mažiau akivaizdžios gudrybės, kurios praktikoje lemia skirtumą tarp „veikia demonstracijoje“ ir „veikia stabiliai eksploatacijoje“.
Kodėl Breakpoint’ai FMX skiriasi nuo žiniatinklio
Žiniatinklio išdėstymuose Breakpoint’ai dažniausiai deklaratyvūs (CSS Media Queries). FMX atveju išdėstymo sprendimai vykdomi imperatyviai paleidimo metu: perjungimas vyksta OnResize įvykyje. Be to, yra platformai būdingų ypatybių:
- Įrenginio pikseliai vs. loginiai pikseliai:
ClientWidth/ClientHeightyra loginiais vienetais (priklauso nuo mastelio). DPI pasikeitimai (pvz. Windows Per-Monitor-DPI) gali iš naujo suaktyvinti išdėstymus, nors „fiziškai“ niekas nepasikeičia. - Sukimas ir saugios zonos: Mobilios platformos pateikia Insets (Notch/Safe Area) – priklausomai nuo OS ir įrenginio. „Tik pagal plotį“ nustatytas Breakpoint dažnai yra per siauras požiūris, nes naudotina erdvė gali būti mažesnė už gryną lango dydį.
- Išdėstymo praėjimas: FireMonkey skaičiuoja išdėstymus etapais. Jei netinkamu metu pakeičiama Parent/Align, atsiranda šalutiniai efektai (pvz. kelkartinis reflow arba mirgėjantys dydžiai).
Layout-Router sprendžia tai, atskirdamas (1) „kada“ (Resize/Scale/Rotation) nuo „kaip“ (išdėstymo taisyklės) ir (2) sutelkiant taisykles vienoje vietoje. Technikos vadovams svarbiausias efektas: jie gauna aiškų, patikrinamą sprendimų centrą vietoje daugybės lokalių išimčių.
Architektūra: Layout-Router su Slots vietoje valdiklių kūrimo
Paprastas triukas FMX: ne dinamiškai kurti valdiklių iš naujo, o perkabinti esamus valdiklius tarp Slots. Slotas yra paprastas konteineris (pvz. TLayout), kuris reprezentuoja UI sritį: šoninė juosta, įrankių juosta, turinys, poraštė, detalių sritis.
Privalumai individualioje įmonės programinėje įrangoje:
- Būsenos išlieka (redagavimo lauko turinys, slinkties pozicija, pažymėti elementai), nes instancijos nėra kuriamos iš naujo.
- Mažesnė rizika dvigubam prijungimui įvykių, laikmačių ar bindingų atžvilgiu.
- Išdėstymo taisyklės tampa matomos: „kuris blokas yra kuriame Slote“ galima sekti ir peržiūrėti kiekvienam Breakpoint’ui.
Svarbu praktikoje: dalykite UI blokus pakankamai grubiai. Jei perkelsite 30 atskirų valdiklių, pati maršrutų (route) sąrašas taps klaidų šaltiniu. Geriau naudoti konteinerius, pavyzdžiui layFilterBar, layNavigation, layResultList, layDetails.
Kodo fragmentas: Breakpoint maršrutizatorius reaguojamiems FMX išdėstymams
Toliau pateiktas kodas skirtas pagalbinei vienetei, kurią galite naudoti FMX formose. Jis apskaičiuoja breakpoint’ą (XS/SM/MD/LG/XL) ir perkelia nurodytus valdiklius į nurodytus slot konteinerius. Svarbios detalės:
- Debounce per
TThread.ForceQueue: keli Resize-įvykiai sujungiami į vieną atnaujinimą (mažiau UI drebėjimo, mažiau reflow ciklų). - Re-Entrancy apsauga: išdėstymo atnaujinimas dažnai pats iššaukia vėl Resize/Layout.
- Pasirinktinai: orientacija (portrait/landscape) gali būti įtraukta į breakpoint logiką.
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);
// Atvaizdavimas: kuris Control turi būti priskirtas kuriam slot'ui (konteineriui) tam tikram Breakpoint'ui.
TNBRoute = record
Control: TControl;
TargetSlot: TControl; // dažniausiai TLayout arba 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; // rankiniu būdu perskaičiuoti
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 negali būti 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: taikyti tik vieną kartą per pranešimų ciklą
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;
// Dėmesio: Parent pakeitimas keičia Z-Order.
// Jei eiliškumas svarbus, kvieskite DefineRoute norima tvarka.
for LRoute in LList do
begin
if (LRoute.Control.Parent <> LRoute.TargetSlot) then
LRoute.Control.Parent := LRoute.TargetSlot;
// Align nustatyti tik po Parent priskyrimo, kitaip Bounds gali būti interpretuojami kitaip.
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 sąmoningai grubūs, nes FMX tikslinės platformos labai skiriasi.
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.
Kaip formoje naudoti maršrutizatorių
Jūs apibrėžiate slotus kaip TLayout (pvz. layTop, layLeft, layContent) ir tuomet kiekvienam Breakpoint registruojate, kur kurie blokai turi būti. Įprasta, kad Sidebar ir Details-Pane mažose Breakpointuose persikelia vienas po kito.
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;Kontekstas: kodėl „Umhängen“ dažnai yra stabilesnis už Visible perjungimą
Įprastas požiūris — kiekvienai variantei turėti atskirus layout medžius ir tik perjunginėti Visible. Dizaineriuje tai atrodo patogu, tačiau turi tipinių nepageidaujamų pasekmių:
- Dvigubas Binding/Events: Dvi panašios valdiklių kopijos turi būti palaikomos sinchroniškai (pvz., dvi filtrų juostos).
- Tab-eiliškumas ir fokusas: Perjungiant prarandamas fokusas arba jis užstringa nematomuose valdikliuose, jei TabStop/HitTest nustatymai nėra tinkami.
- State Drift: Slinkties pozicijos, atrankos būsenos arba redaguoti tekstai gali išsiskirti.
Perkabinimas („Umhängen“) palaiko vienareikšmę instanciją. Svarbu išdėstyti layout blokus taip, kad juos būtų galima nepriklausomai perkelti (pvz., „Sidebar“ kaip atskiras konteineris vietoje daugelio atskirų valdiklių). Tai atsiperka priežiūroje ir klaidų analizėje: jūs derinate vieną instanciją, o ne dvi paralelines šešėlines UI.
Praktinės kliūtys (ir kaip jas derinti)
1) Resize audros ir re-entrancy
FMX iškviečia OnResize ne tik dėl vartotojo dydžio keitimo, bet ir dėl stiliaus pakeitimų, Parent pakeitimų ir dalinai DPI pokyčių. Be debounc’o programa gali įstrigti išdėstymo cikluose. Router naudoja TThread.ForceQueue, kad pakeitimai būtų perkelti į kitą UI taktą.
Derinimo patarimas: logavimas (pvz., per OutputDebugString) kartu su breakpoint, dydžiu ir atnaujinimų skaitikliu padeda rasti reflow ciklus. Jei taip pat užregistruosite laiką, kada ApplyRoutes prasideda ir baigiasi, greitai pamatysite, ar vienas Resize „kaskaduoja“.
2) Z-Order, HitTest und „unsichtbare“ Klick-Blocker
Parent pakeitimai keičia Z-eiliškumą. Jei overlay’ai (pvz., Flyouts) nebepriima paspaudimų, dažnai priežastis yra client-aligned konteineris viršuje su aktyviu HitTest. Sprendimas: skirtoms overlay sritims numatykite atskirą slotą viršuje ir tik ten priskirkite tokius valdiklius kaip parent. FMX atveju HitTest (ar valdiklis pagauna pelės/lietimo įvykius) dažniau yra problema nei pats matomumas.
3) TGridPanelLayout ir procentiniai dydžiai
TGridPanelLayout gali sukelti netikėtus perskaičiavimus, kai procentinės stulpelių/eilučių reikšmės derinamos su Align=Client ir dinamišku perkabinimu. Jei privalote naudoti Grid, talpinkite Grid į slotą ir perkelkite tik pilnus Grid blokus, o ne atskirus Grid vaikų elementus. Tai sumažina išdėstymo žingsnių kombinatoriškumą.
4) Fokusas, virtuali klaviatūra ir „šokinėjantys“ įvedimo laukai
Tai kraštinis atvejis, pasitaikantis mobiliuose FMX app’uose ir taip pat Windows planšetėse: perkabinimo metu fokusuotas Edit kontrolės elementas gali trumpam prarasti Parent. Tai gali uždaryti virtualią klaviatūrą arba nustatyti kursorių iš naujo. Praktikoje pasitvirtino: prieš Routing laikinai išsaugoti esamą fokusą (Focused/IFMXFocusControl), o po Routing (to paties UI ciklo metu) fokusą atstatyti. Tai ypač naudinga įvedimo formoms, kurios pereina tarp „dviejų stulpelių“ (Tablet/PC) ir „vieno stulpelio“ (Phone) išdėstymo.
Variacijos: Breakpoints pagal formos faktorių, o ne tik pagal plotį
Realiuose multiplatform klientuose vien tik „plotis“ dažnai nėra tinkamas signalas. Naudingi variantai:
- Plotis ir aukštis: labai plokšti langai (pvz., kasos terminalai, padalinti ekranai) reikalauja kitokių taisyklių.
- Orientacija:
Landscapeplanšetėse dažnai elgiasi kaip darbalaukio tipo įrenginys, o Portrait labiau kaip mobilus. - Safe-Area naudinga sritis: iOS/Android efektyviai prieinama aukštis gali smarkiai sumažėti dėl sistemos juostų. Tie, kurie žiūri tik į
Height, kartais per vėlai keičia išdėstymą.
Routeris sąmoningai sukurtas taip, kad galite pakeisti Breakpoint funkciją. Tai taip pat naudinga legacy situacijose, kai ta pati forma veikia keliuose hostuose (pvz., vieną kartą kaip įprastas langas, kitą kartą įdėtame konteineryje).
Neįprastai tvarkinga: Layout-Routing kaip „transakcija“
Didesniuose ekranuose problema dažniau kyla ne dėl pačių Breakpoint’ų, o dėl UI operacijų eiliškumo. Praktinis modelis — traktuoti routing kaip transakciją: pirmiausia apsispręsti, paskui perkelti komponentus, o tuomet tvarkingai atlikti šalutinius veiksmus (matomumas, fokusas, duomenų atnaujinimas).
Konkrečiai tai reiškia: venkite situacijų, kai atskiri valdikliai perkabinimo metu sukelia savo įvykius, kurie vėl pradeda layout ar duomenų užklausas. FMX tai vyksta, pavyzdžiui, kai keičiant Parent suveikia OnEnter/OnExit arba LiveBinding išraiška perskaičiuojama po Bounds atnaujinimo. Jei pastebite tokius efektus, padeda centrinis „Updating“ jungiklis (kaip Router’yje) ir aiškus post žingsnis: tik po ApplyRoutes leidžiama vykdyti brangias operacijas (pvz., pakartotinai užkrauti sąrašą, pririšti detalių vaizdą).
Ypač aktualu klientams su REST prieiga: nepageidaujamas perkrovimas keičiant dydį gali sukelti nereikalingų užklausų. LAN to gali nepastebėti, bet per VPN ar mobiliojo ryšio atveju tai pasireiškia iškart.
Kada šis požiūris apsimoka – ir kokios jo ribos
Layout-Routeris apsimoka, kai:
- FMX taikymas gyvuoja metų metus ir keli kūrėjai dirba su tais pačiais ekranais,
- UI blokai gali būti aiškiai atskirti (Sidebar/Details/Content),
- reikalingos atkartojamos Breakpoint taisyklės, o ne ad-hoc Align sureguliavimas.
Ribotumai pasireiškia, kai ekranas turi būti labai „fluid“ (daug dinamiškų plytelių, tikri Masonry-išdėstymai). Tada tinkamesni yra TFlowLayout/TGridPanelLayout arba savos išdėstymo klasės. Taip pat, jei labai daug atskirų valdiklių kinta tarp slotų, maršrutų priežiūra tampa neperžvelgiama – geriau tuomet skaidyti į didesnius blokus arba įvesti deklaratyvią konfigūracijos sluoksnį (pvz. JSON konfigūracija slotų priskyrimams, kuri užkraunama paleidimo metu).
Išvada: Reaguojančių išdėstymų FMX atveju „perkabinimas su Breakpoints“ yra pragmatiškas kompromisas: mažiau dizainerio chaoso, aiškios taisyklės, stabilios būsenos. Tai nepakeičia apgalvotos UI struktūros, bet suteikia jums patikimą karkasą, leidžiantį kontroliuojamai tobulinti FMX klientus skaitmeninėse įmonių sprendimuose per formos faktorius.
Jei norite tokį išdėstymo architektūrą tvarkingai perkelti į esamą Delphi arba FMX programą, nekeliančią UI regresijų eksploatacijos scenarijuose, galite tai techniniu atžvilgiu su mumis įvertinti: aptarkite projektą arba modernizavimo užmojį su Net-Base.
Profesiniame kontekste taip pat svarbų vaidmenį atlieka Delphi Fmx Breakpoints ir Firemonkey Layout, kai integracijos, duomenų srautai ir tolimesnė plėtra turi sklandžiai veikti kartu.
Kitas žingsnis
Kai tema virsta realiu projektu, architektūra, esami sprendimai ir eksploatavimas turėtų būti nagrinėjami kartu nuo pat pradžių.
Wir unterstuetzen nicht nur bei Einzelfragen, sondern auch dann, wenn aus Source-Schnipseln, Legacy-Themen oder Portalideen ein belastbares Unternehmensprojekt werden soll.
- Esama padėtis, tikslinis vaizdas ir techninės rizikos vertinami kartu.
- REST, duomenų prieiga, portalai ir rollout nebus perkelti į vėlesnį etapą kaip vėlyvos pasekmės.
- Jūs anksti matote, kuris kelias yra ekonomiškai ir operaciniškai tvarus.