Ajakirjateemast projektipraktikasse
Sobivad teenuse- ja tehnilised lehed postituse jaoks
Kes peab Delphi FireMonkey mitme vormifaktoriga toime tulema, satub kiiresti tugevasti Responsive Layouts FMX juurde – ja sama kiiresti ka Align-kaskaadide, peidetud paigutuskonteinerite ja designer-workaround’ide segusse, mis järgmise DPI- või rotatsioonimuutuse korral ära kukuvad. Kasvavates ärirakendustes on see eriti ebameeldiv: kasutajaliidest arendatakse edasi, meeskonnad vahetuvad ja äkitselt on äriloogika seotud visuaalsete detailidega.
Probleemi tuum: FireMonkey pakub palju baasmooduleid (nt Align, Anchors, TScaledLayout, TFlowLayout, TGridPanelLayout), kuid puudub „natiivne“ breakpoint-süsteem nagu veebis. Kuigi saab suurusemuutustele reageerida, lõpeb see ilma selge arhitektuurita laialt levinud „if Width < … then …“-loogikaga erinevates vormides.
See artikkel tutvustab Layout-Routerit: väikest komponenti, mis haldab breakpoint’e tsentraalselt ja riputab Controls (või terviklikud paigutusplokid) ettevalmistatud Slot’ide vahel ümber. Eesmärk: olekud säilivad, kood on hooldatav ja servjuhtumid nagu rotatsioon, pesastatud paigutused ja re-entrancy saavad leevenduse. Lisaks on mõned vähem ilmse nüansid, mis praktikas tähendavad vahet demo-käivituse ja stabiilse tootmiskäivituske vahel.
Miks on breakpoint’id FMX-is erinevad kui veebis
Veebipaigutustes on breakpoint’id enamasti deklaratiivsed (CSS Media Queries). FMX-is tehakse paigutusotsused tavaliselt jooksvalt imperatiivselt: muutus tehakse OnResize-is. Sellele lisanduvad platvormispetsiifilised eripärad:
- Seadme-pikslid vs loogilised pikslid:
ClientWidth/ClientHeighton loogilistes ühikutes (sõltuvalt skaala sättest). DPI-vahetused (nt Windows Per-Monitor-DPI) võivad käivitada paigutuse ümberarvutuse, ilma et „füüsiliselt“ midagi muutuks. - Rotatsioon ja safe areas: Mobiilsed platvormid annavad Insets (Notch/Safe Area) – sõltuvalt opsüsteemist ja seadmest. „Breakpoint ainult laiuse järgi“ on sageli liiga piiratud, sest kasutatav pind võib olla väiksem kui akna puhas suurus.
- Layout-pass: FireMonkey arvutab paigutusi etappide kaupa. Kui muuta Parent/Align valel hetkel, tekivad kõrvalmõjud (nt mitmekordne reflow või vilkuvad suurused).
Layout-Router käsitleb seda, eraldades (1) «millal» (Resize/Scale/Rotation) «kuidas» (paigutusreeglid) ning (2) koondades reeglid ühte kohta. Tehniliste juhtide jaoks on olulisem efekt see, et nad saavad selge, kontrollitava otsustuskeskuse, mitte hulga lokaalsete erandite kogumi.
Arhitektuur: Layout-Router Slot’idega, mitte Controlide taasloomine
Puhas nipp FMX-ile: mitte dünaamiliselt Controls’e uuesti luua, vaid riputada olemasolevad Controls ümber Slotide vahel. Slot on lihtne konteiner (nt TLayout), mis esindab UI piirkonda: külgriba, tööriistariba, sisu, jalus, üksikasjade paneel.
Eelised individuaalse ärisoftware puhul:
- Olekud säilivad (nt edit-tekst, kerimispositsioon, valitud üksused), sest eksemplaarid ei pea uuesti ehitatama.
- Vähenenud risk topeltühenduste tekkeks sündmuste, taimerite või binding’ute puhul.
- Paigutusreeglid muutuvad nähtavaks: „milline blokk asub millises slot’is“ on iga breakpoint’i puhul jälgitav ja ülevaadatav.
Praktilise tähtsusega: jaotage kasutajaliidese plokid piisavalt suurtel tükkidel. Kui ümber tõstate 30 üksikut kontrolli, muutub marsruutide nimekiri ise vigade allikaks. Paremad on konteinerid nagu layFilterBar, layNavigation, layResultList, layDetails.
Koodinäide: Breakpoint-ruuter responsiivsete FMX-paigutuste jaoks
Järgmine kood on mõeldud abikomponendina, mida saate FMX-Forms’is kasutada. See arvutab Breakpointi (XS/SM/MD/LG/XL) ja paigutab määratletud kontrollid määratletud slot-konteineritesse. Tähtsad detailid:
- Debounce kasutades
TThread.ForceQueue: mitu Resize-sündmust koondatakse üheks uuenduseks (vähem kasutajaliidese värinat, vähem reflow-tsükleid). - Taassisenemise kaitse: paigutuse uuendus käivitab sageli ise Resize/Layout sündmusi.
- Valikuline: orientatsioon (portree/maastik) võib Breakpoint-loogikasse sisse mängida.
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);
// Kaardistus: milline Control paigutatakse millisesse sloti (konteinerisse) konkreetse breakpointi jaoks.
TNBRoute = record
Control: TControl;
TargetSlot: TControl; // tüüpiliselt TLayout või 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; // käsitsi uuesti arvutada
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 ei tohi olla 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: rakendada ainult üks kord iga sõnumitsükli jooksul
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;
// Tähelepanu: Parent'i vahetus muudab Z-järjestust.
// Kui järjekord on oluline, kutsuge DefineRoute soovitud järjekorras.
for LRoute in LList do
begin
if (LRoute.Control.Parent <> LRoute.TargetSlot) then
LRoute.Control.Parent := LRoute.TargetSlot;
// Align määrata alles pärast Parent'i määramist, muidu võidakse Bounds teisiti tõlgendada.
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;
// Breakpointid on teadlikult jämedad, kuna FMX-i sihtplatvormid võivad tugevalt erineda.
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.
Kuidas vormis routerit kasutada
Te määratlete slotid kui TLayout (nt layTop, layLeft, layContent) ja registreerite seejärel iga breakpointi kohta, kus millised plokid asuvad. Tavaliselt liiguvad väikestes breakpoint’ides külgriba ja detailivaade üksteise alla.
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;Kontekst: miks ümberpaigutamine sageli stabiilsem on kui Visible-i lülitamine
Levinud lähenemine on hoida iga variandi jaoks eraldi layout-puud ja lihtsalt Visible ümber lülitada. See näib designer’is mugav, kuid põhjustab tüüpilisi kõrvalmõjusid:
- Kahekordne sidumine/sündmused: Kaks sarnast komponenti tuleb sünkroonis hoida (nt kaks filtririba).
- Tabi järjestus ja fookus: Lülitamisel kaotatakse fookus või satutakse nähtamatutesse komponentidesse, kui TabStop/HitTest ebasoodsalt seatud on.
- Oleku hälbed: Kerimispositsioonid, valikuseisundid või redigeeritud tekstid võivad erineda.
Ümberpaigutamine hoiab instantsi üheselt. Oluline on jagada layout-plokid nii, et neid saaks iseseisvalt liigutada (nt „külgriba“ eraldi konteinerina, mitte paljude üksiktelementidena). See tasub end hoolduses ja veaotsingus ära: te debugite ühte instantsi, mitte kahte paralleelset varju-UI-d.
Praktilised lõksud (ja kuidas neid debugida)
1) Resize-lainetused ja re-entrancy
FMX käivitab OnResize mitte ainult kasutaja suuruse muutmisel, vaid ka stiili vahetusel, parent-muutustel ja mõnel juhul DPI-muutustel. Ilma debounce-mehhanismita võib rakendus jääda layout-tsüklitesse. Router kasutab TThread.ForceQueue, et lükata muudatused järgmisse UI-tikki.
Debugimisnõuanne: logimine (nt OutputDebugString) koos breakpointi, mõõtude ja uuenduste loenduriga aitab leida reflow-tsükleid. Kui logite ka ajad, millal ApplyRoutes käivitub ja lõpeb, näete kiiresti, kas üksik Resize kaskadeerub.
2) Z-Order, HitTest ja „nähtamatud“ klikkide blokeerijad
Parent’i vahetus muudab Z-Orderit. Kui overlay’d (nt Flyouts) ei reageeri enam klikkidele, on põhjus tihti see, et nende kohal paikneb client-aligned konteiner ja HitTest on aktiivne. Lahendus: reserveerida overlay-pindade jaoks teadlikult eraldi slot alati kõige peale ja parentida sellised komponendid ainult sinna. FMX-is on HitTest (kas komponent tabab hiire-/puute-sündmusi) sagedamini probleemiks kui nähtavus.
3) TGridPanelLayout ja protsendipõhised suurused
TGridPanelLayout võib protsentuaalsete veergude/ridade puhul koos Align=Client ja dünaamilise ümberpaigutusega põhjustada ettearvamatuid ümberarvutusi. Kui peate Grid kasutama, paigutage Grid slot’i ja ümberhangige ainult terviklikke Grid-blokke, mitte Grid-lapsi. See vähendab paigutuse läbipääsude kombinatoorikat.
4) Fookus, virtuaalne klaviatuur ja „hüppavad“ sisendväljad
Piirjuhtum, mis esineb mobiilsetes FMX-rakendustes ja ka Windows-tahvelarvutitel: ümberhangimisel võib fookuses olev Edit-Control lühiajaliselt oma vanema kaotada. See võib sulgeda virtuaalse klaviatuuri või lähtestada kursori positsiooni. Praktikas on tõestunud lähenemine: enne routingut vahemällu salvestada praegune fookus (Focused/IFMXFocusControl) ja pärast routingut (samas UI-tikis) fookus taastada. See on eriti oluline sisendvormide puhul, mis vahetavad „kaheveerulise“ (Tablet/PC) ja „üheveerulise“ (Phone) vahel.
Variandid: Breakpoints nach Formfaktor statt nur nach Breite
Reaalses multiplatvorm-kliendis ei ole „laius“ sageli ainus õige signaal. Mõistlikud variandid:
- Laius ja kõrgus: väga madalad aknad (nt kassaterminalid, jagatud ekraanid) vajavad teistsuguseid reegleid.
- Orientatsioon:
Landscapetahvelarvutitel on sageli „lauaarvutile sarnane“, portree pigem „mobiilne“. - Safe-Area kasutuspind: iOS/Android puhul võib süsteemiribade tõttu efektiivne kasutuskõrgus oluliselt väheneda. Kes vaatab ainult
Height, teeb mõnikord ruutimise otsuse liiga hilja.
Router on teadlikult üles ehitatud nii, et saate breakpoint-funktsiooni välja vahetada. See on kasulik ka legacy-situatsioonides, kui sama vorm jookseb mitmes hostis (nt kord normaalse aknana, kord manustatud konteineris).
Ebatavaliselt puhas: paigutuse routing kui „transaktsioon“
Suuremate ekraanide puhul ei sõltu probleem niivõrd breakpointidest kui UI-operatsioonide järjekorrast. Töökindel muster on käsitleda routingut kui transaktsiooni: esmalt otsustada, siis ümberhangida, seejärel kõrvalmõjud (nähtavus, fookus, andmete värskendus) järjekindlalt käivitada.
Konkreetselt tähendab see: vältige, et üksikud kontrollid ümberhangimisel käivitaksid oma sündmusi, mis omakorda alustavad paigutuse muutmist või andmejuurdepääsu. FMX-is juhtub see näiteks siis, kui vanema vahetusel käivituvad OnEnter/OnExit või kui LiveBinding-väljend hinnatakse bounds-uuenduse tõttu uuesti. Kui näete selliseid efekte, aitab keskne „Updating“-lüliti (nagu routeris) koos selge post-sammuga: alles pärast ApplyRoutes tohivad kulukad toimingud käivituda (nt loendi uuesti laadimine, detailvaate sidumine).
Eriti klientide puhul, kellel on REST-ligipääs, on see oluline: soovimatu uuellaadimine resize’i ajal võib tekitada liigseid päringuid. LAN-is seda ei pruugi märgata, aga VPN-is või mobiilselt ilmneb see kohe.
Millal lähenemine tasub end ära – ja kus on piirid
Layout-ruuter tasub end ära, kui:
- FMX-rakendus elab aastaid ja mitu arendajat töötavad samade ekraanide kallal,
- UI-blokid on selgelt eraldatavad (Sidebar/Details/Content),
- teil on vaja reprodutseeritavaid breakpoint-reegleid, mitte ad-hoc Align-täppimist.
Piirid ilmnevad, kui ekraan peab olema tugevalt „fluid“ (palju dünaamilisi plaate, tõelised Masonry-paigutused). Siis sobivad paremini TFlowLayout/TGridPanelLayout või kohandatud Layout‑klassid. Kui väga paljud eraldiseisvad komponendid vahetavad slotide vahel aset, muutub marsruutide hooldus keeruliseks – siis on mõistlikum lõigata suuremaid plokke või rakendada deklaratiivne konfiguratsioonikiht (nt JSON‑konfiguratsioon slotide määrangute jaoks, mis laetakse käivitamisel).
Kokkuvõte: FMX‑i jaoks reageerivate paigutuste puhul on „Breakpoints’idega ümberlülitamine“ pragmaatiline kesktee: vähem disaineri‑kaost, selged reeglid, stabiilsed olekud. See ei asenda läbimõeldud UI‑struktuuri, kuid annab teile usaldusväärse raamistiku FMX‑klientide kontrollitud edasiarendamiseks digitaalsetes ettevõttelahendustes erinevate vormitegurite ulatuses.
Kui soovite olemasolevas Delphi‑ või FMX‑rakenduses sellist paigutus‑arhitektuuri korrektselt järele teha, ilma et riskiksite käitusstsenaariumites UI‑regressioonidega, saame seda tehniliselt koos läbi vaadata: arutage projekti või moderniseerimishanke kasutades Net-Base.
Tehnilises kontekstis mängivad ka Delphi Fmx Breakpoints ja Firemonkey Layout olulist rolli, kui integratsioonid, andmevood ja edasiarendus peavad korrektselt koos töötama.
Projekti või moderniseerimishanke arutamiseks kasutage Net-Base.
Järgmine samm
Kui teema muutub reaalseks projektiks, tuleks arhitektuuri, olemasolevat süsteemi ja käitust varakult ühiselt vaadelda.
Me ei toeta ainult üksikute küsimuste lahendamist, vaid ka siis, kui lähtekoodilõikudest, pärandsüsteemidest või portaalikontseptsioonidest peab saama usaldusväärne ettevõtteprojekt.
- Olemasolev olukord, sihtpilt ja tehnilised riskid hinnatakse üheskoos.
- REST, andmete juurdepääs, portaalid ja juurutamine ei lükata hilisemaks.
- Te näete varakult, milline tee on majanduslikult ja operatiivselt jätkusuutlik.