No žurnāla tēmas līdz projektu praksei
Atbilstošas pakalpojumu un tehniskās lapas rakstam
Ja kādam, kuram Delphi FireMonkey jāapkalpo vairāki formfaktori, viņš ātri nonāk pie Responsive Layouts FMX — un tikpat ātri pie jaukuma no Align-kaskādēm, slēptiem izkārtojuma konteineriem un dizainera pagaidu risinājumiem, kas sabrūk nākamajā DPI vai rotācijas maiņā. Pieaugušos uzņēmumu programmatūras klientos tas ir īpaši nepatīkami: UI tiek turpināta attīstīt, komandas mainās, un pēkšņi loģika ir sasaistīta ar vizuālām detaļām.
Problēmas kodols: FireMonkey piedāvā daudz komponentu (piem., Align, Anchors, TScaledLayout, TFlowLayout, TGridPanelLayout), taču tam nav „dzimtā” breakpoint-sistēma kā tīmeklī. Var reaģēt uz izmēru izmaiņām, bet bez skaidras arhitektūras tas beigsies ar „if Width < … then …” izkliedētu pa daudziem Forms.
Šis raksts demonstrē Layout-Router: nelielu komponenti, kas centrāli pārvalda breakpoints un pārvieto Controls (vai veselas izkārtojuma blokus) starp sagatavotiem Slots. Mērķis: stāvokļi saglabājas, kods paliek uzturams, un maldīšanās gadījumi kā rotācija, iegultie izkārtojumi un re-entrancy tiek amortizēti. Papildus tam ir daži mazāk acīmredzami paņēmieni, kas praksē nosaka atšķirību starp „darbojas demonstrācijā” un „darbojas stabilā ekspluatācijā”.
Kāpēc Breakpoints FMX atšķiras no tīmekļa
Tīmekļa izkārtojumos breakpoints parasti ir deklaratīvi (CSS Media Queries). FMX gadījumā izkārtojuma lēmumi parasti tiek pieņemti izpildlaikā imperatīvi: OnResize tiek izmantots, lai pārslēgtu izkārtojumu. Tam klāt nāk platformu īpatnības:
- Device-Pixel vs. loģiskie pikseļi:
ClientWidth/ClientHeightir loģiskajās vienībās (atkarīgs no mērogošanas). DPI izmaiņas (piem., Windows Per-Monitor-DPI) var izraisīt izkārtojuma pārrēķinu, neradot „fiziskas” izmaiņas. - Rotācija un Safe Areas: Mobilās platformas piegādā Insets (Notch/Safe Area) — atkarībā no OS un ierīces. Breakpoint, kas balstīts tikai uz platumu, bieži ir pārāk šaurs skatījums, jo izmantojamā virsma var būt mazāka par loga tīro izmēru.
- Layout-pieeja: FireMonkey izskaitļo izkārtojumus posmos. Ja Parent/Align maina nepareizā brīdī, rodas blakusparādības (piem., vairākkārtējs reflow vai mirgojoši izmēri).
Layout-Router risina šo problēmu, (1) atdalot „kad“ (Resize/Scale/Rotation) no „kā“ (izkārtojuma noteikumi) un (2) koncentrējot noteikumus vienā vietā. Tehniskajiem līderiem svarīgākais efekts: viņi iegūst skaidru, pārbaudāmu lēmumu centru, nevis daudzus lokālus izņēmuma gadījumus.
Arhitektūra: Layout-Router ar Slots, nevis kontrolu izveide
Tīrs paņēmiens FMX: ne dinamiski atjaunot Controls, bet pārkārtot esošos Controls starp Slots. Slot ir vienkārši konteiners (piem., TLayout), kas reprezentē UI daļu: sānjosla, rīkjosla, saturs, kājene, detaļu panelis.
Priekšrocības individuālai uzņēmumu programmatūrai:
- Stāvokļi tiek saglabāti (Edit-Text, ritinājuma pozīcija, atlasītie elementi), jo instances netiek pārbūvētas.
- Mazāks risks dubultai pieslēgšanai notikumiem, taimeriem vai sasaistēm.
- Izkārtojuma noteikumi kļūst redzami: „kurš bloks atrodas kurā Slotā” ir izsekojams katram breakpointam un to var pārskatīt.
Svarīgi praksē: nošķiriet UI blokus pietiekami rupji. Ja pārvietojat 30 atsevišķus kontrolu elementus, maršruta saraksts pats kļūst par kļūdas avotu. Labāk izmantot konteinerus kā layFilterBar, layNavigation, layResultList, layDetails.
Koda fragments: Breakpoint-Router responsīviem FMX izkārtojumiem
Šāds kods domāts kā palīgvienība, ko varat izmantot FMX formās. Tas aprēķina Breakpoint (XS/SM/MD/LG/XL) un pārvieto definētus kontrolu elementus uz definētiem slot-konteineriem. Svarīgas detaļas:
- Debounce — izmantojot
TThread.ForceQueue: vairāki Resize notikumi tiek apvienoti vienā atjauninājumā (mazāk UI trīcēšanas, mazāk reflow cilpu). - Re-entrancy aizsardzība: Layout-atjauninājums bieži pats izsauc Resize/Layout.
- Neobligāti: orientācija (portrets/ainava) var tikt iekļauta Breakpoint loģikā.
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);
// Kartējums: kurš Control jānovieto kurā slotā (konteinerī) konkrētam Breakpoint.
TNBRoute = record
Control: TControl;
TargetSlot: TControl; // parasti TLayout vai 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; // manuāli pārrēķināt
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 nedrīkst būt 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: piemērot tikai vienu reizi katrā Message-Loop 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;
// Uzmanību: Parent maiņa maina Z-kārtību.
// Ja secība ir svarīga, izsauciet DefineRoute vēlamajā secībā.
for LRoute in LList do
begin
if (LRoute.Control.Parent <> LRoute.TargetSlot) then
LRoute.Control.Parent := LRoute.TargetSlot;
// Align iestatīt tikai pēc Parent iestatīšanas, citādi Bounds var tikt interpretēti savādāk.
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 apzināti aptuveni, jo FMX mērķplatformas būtiski atšķiras.
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.
Kā izmantot Router formā
Jūs definējat slotus kā TLayout (piem., layTop, layLeft, layContent) un pēc tam reģistrējat katram Breakpoint, kur kuri bloki atrodas. Parasti sānjosla un detaļu panelis mazos Breakpoint pārvietojas viens zem otra.
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;Konteksts: Kāpēc „pārparentēšana“ bieži ir stabilāka nekā Visible pārslēgšana
Izplatīts paņēmiens ir katrai variācijai saglabāt atsevišķus izkārtojuma kokus un tikai pārslēgt Visible. Tas dizainerī izskatās ērti, taču tam ir tipiskas blaknes:
- Dubultā Binding/Events: Diviem līdzīgiem kontrolēm jābūt sinhronizētām (piem., divām filtru joslām).
- Tab secība un fokuss: Pārslēdzoties fokuss var tikt pazaudēts vai nonākt neredzamās kontrolēs, ja TabStop/HitTest ir nevēlami iestatīti.
- State Drift: Ritināšanas pozīcijas, atlases stāvokļi vai rediģētie teksti divergē.
Pārparentēšana saglabā instanci viennozīmīgu. Svarīgi ir izgriezt izkārtojuma blokus tā, lai tos var pārvietot neatkarīgi (piem., sānjosla kā atsevišķs konteiners, nevis daudzi atsevišķi kontroles elementi). Tieši tas atmaksājas uzturēšanā un kļūdu analīzē: jūs atkļūdojat vienu instanci, nevis divas paralēlas ēnu UI.
Biežākās kļūdas praksē (un kā tās atkļūdot)
1) Resize vētras un re-entrancy
FMX neizsauc OnResize tikai lietotāja Resize gadījumā, bet arī stilu maiņu, Parent izmaiņu un daļēji DPI izmaiņu laikā. Bez debouncing aplikācija var iestrēgt izkārtojuma cilpās. Router izmanto TThread.ForceQueue, lai pārceltu izmaiņas uz nākamo UI ciklu.
Atkļūdošanas padoms: žurnāšanas ieraksti (piem., ar OutputDebugString) ar Breakpoint, izmēru un atjauninājumu skaitītāju palīdz atrast reflow cilpas. Ja papildus ierakstāt laiku, kad ApplyRoutes sākas un kad beidzas, ātri redzēsiet, vai viens Resize kaskadējas.
2) Z-Order, HitTest und „unsichtbare“ Klick-Blocker
Parent maiņa maina Z-Order. Ja Overlays (piem., Flyouts) vairs nav klikšķējami, bieži iemesls ir tas, ka virs tiem atrodas client-aligned konteiners ar aktīvu HitTest. Variants: Overlay laukumiem apzināti paredzēt atsevišķu slotu pavisam augšā un tikai tur parentēt šādus controls. FMX gadījumā HitTest (vai kontrolis uztver peli/taustīšanas notikumus) biežāk ir cēlonis nekā redzamība.
3) TGridPanelLayout und prozentuale Größen
TGridPanelLayout var izraisīt negaidītas pārrēķināšanas, ja tiek lietotas procentuālas kolonnas/rindas kopā ar Align=Client un dinamisku pārkārtošanu. Ja jāizmanto Grid, ievietojiet Grid slotā un pārkārtojiet tikai pilnus Grid blokus, nevis Grid bērnelementus. Tas samazina izkārtojuma pāreju kombinatoriku.
4) Fokus, virtuelle Tastatur und „springende“ Eingabefelder
Starpieraksts, kas parādās mobilajās FMX-Apps un arī uz Windows-planšetēm: pārkārtojot, fokusēts Edit-Control īslaicīgi var zaudēt vecāku. Tas var aizvērt virtuālo tastatūru vai atiestatīt kursora pozīciju. Praktiski ir sevi attaisnojis risinājums: pirms maršrutēšanas saglabāt pašreizējo fokusu (Focused/IFMXFocusControl), pēc maršrutēšanas (tajā pašā UI-tick) atjaunot fokusu. Tas ir īpaši lietderīgi ievades veidlapām, kas pārslēdzas starp „zweispaltig“ (Tablet/PC) un „einspaltig“ (Phone) izkārtojumu.
Varianten: Breakpoints nach Formfaktor statt nur nach Breite
Reālos multiplatformu klientos „Breite“ vien ne vienmēr ir atbilstošs rādītājs. Jēdzīgi varianti:
- Breite und Höhe: ļoti seklas loga formas (piem., Kassen-Terminals, geteilte Bildschirme) prasa citas noteikšanas.
- Orientierung:
Landscapeuz planšetēm bieži ir „desktop-ähnlich“, Portrait drīzāk „mobile-like“. - Safe-Area-Nutzfläche: uz iOS/Android efektīvi izmantojama augstuma daļa var ievērojami sarukt sistēmas joslu dēļ. Ja skatās tikai
Height, maršrutēšana dažkārt notiek „zu spät“.
Der Router ist bewusst so gebaut, dass Sie die Breakpoint-Funktion austauschen können. Das ist auch hilfreich in Legacy-Situationen, wenn dieselbe Form in mehreren Hosts läuft (z. B. einmal als normales Fenster, einmal in einem eingebetteten Container).
Ungewöhnlich sauber: Layout-Routing als „Transaktion“
Lielākos ekrānos problēma retāk slēpjas pašos Breakpointos, vairāk — UI operāciju secībā. Praxistaugliches Muster ir traktēt Routing kā Transaktion: vispirms izlemt, tad pārkārtot, pēc tam sakārtoti izpildīt blakusparādības (Visibility, Fokus, datu atsvaidzināšana).
Konkrēti tas nozīmē: izvairieties no tā, lai atsevišķi Controls pārkārtošanās laikā izsauktu savus eventus, kas savukārt startē layout vai datu piekļuvi. FMX tas notiek, piemēram, ja Parent maiņas laikā OnEnter/OnExit izšaujas vai LiveBinding izteiksme tiek no jauna novērtēta Bounds atjauninājuma dēļ. Ja redzat šādus efektus, palīdz centrāls „Updating“ slēdzis (kā Router) plus skaidrs post‑steps: tikai pēc ApplyRoutes drīkst darboties dārgas operācijas (piem., saraksta pārlāde, detailskata piesaiste).
Īpaši klientiem ar REST piekļuvi tas ir relevant: nevēlama pārlāde Resize laikā var radīt liekus requestus. LAN to var nepamanīt, bet VPN vai mobilajā tīklā — uzreiz.
Wann sich der Ansatz lohnt – und wo er Grenzen hat
Layout-Router ir lietderīgs, ja:
- FMX‑lietošana dzīvo vairākus gadus un pie tiem pašiem ekrāniem strādā vairāki izstrādātāji,
- UI blokus iespējams skaidri atdalīt (Sidebar/Details/Content),
- Jums nepieciešamas reproducējamas Breakpoint‑noteikšanas, nevis ad‑hoc Align regulēšana.
Ierobežojumus redzat, ja ekrānam jābūt ļoti „fluid“ (daudzas dinamiskas flīzes, īsti Masonry-izkārtojumi). Tad piemērotāki ir TFlowLayout/TGridPanelLayout vai pašu izstrādātas izkārtojumu klases. Arī gadījumos, kad ļoti daudzi atsevišķie kontroles elementi pārvietojas starp slotiem, maršrutu uzturēšana kļūst pārāk sarežģīta – tad labāk sadalīt lielākus blokus vai ieviest deklaratīvu konfigurācijas slāni (piem., JSON konfigurācija slotu piešķīršanai, kas tiek ielādēta startēšanas laikā).
Secinājums: FMX responsīvajiem izkārtojumiem „pārslēgšanās, izmantojot Breakpoints“ ir pragmatisks vidusceļš: mazāks dizaina haoss, skaidri noteikumi, stabilas stāvokļu pārejas. Tas neaizstāj pārdomātu UI struktūru, bet nodrošina uzticamu karkasu, lai kontrolēti turpinātu attīstīt FMX klientus digitālajos uzņēmumu risinājumos, neatkarīgi no formfaktoriem.
Ja vēlaties esošā Delphi vai FMX lietotnē rūpīgi ieviest šādu izkārtojumu arhitektūru, neriskējot ar UI regresijām ražošanas scenārijos, varam to tehniski novērtēt kopā ar jums: apspriest projektu vai modernizācijas iniciatīvu ar Net-Base.
Profesionālajā kontekstā arī Delphi Fmx Breakpoints un Firemonkey izkārtojums spēlē nozīmīgu lomu, ja integrācijām, datu plūsmām un turpmākajai attīstībai jādarbojas saskaņoti.
Apspriest projektu vai modernizācijas iniciatīvu ar Net-Base.
Nākamais solis
Ja no tēmas rodas reāls projekts, arhitektūra, esošais stāvoklis un ekspluatācija būtu jāizskata kopā jau agri.
Mēs atbalstām ne tikai atsevišķu jautājumu risināšanā, bet arī tad, kad no avota koda fragmentiem, mantojuma sistēmu jautājumiem vai portāla idejām jāizveido stabils uzņēmuma līmeņa projekts.
- Esošais stāvoklis, mērķa stāvoklis un tehniskie riski tiek kopīgi vērtēti.
- REST, datu piekļuve, portāli un izvēršana netiek atlikti kā vēlākas sekas.
- Jūs savlaicīgi redzat, kurš ceļš ir ekonomiski un darbības ziņā dzīvotspējīgs.