Od teme v reviji do projektne prakse
Ustrezne strani storitev in tehnični opisi k prispevku
Kdor mora v Delphi FireMonkey upravljati več formfaktorjev, hitro pristane pri Responsive Layouts FMX – in prav tako hitro pri mešanici Align-kaskad, skritih layout-kontejnerjev in Designer-workaroundov, ki pri naslednji spremembi DPI ali rotacije odpovedo. V zrelih poslovnih programskih odjemalcih je to še posebej neprijetno: UI se nadalje razvija, ekipe se menjajo in nenadoma je logika vezana na vizualne podrobnosti.
Jedro problema: FireMonkey nudi veliko gradnikov (npr. Align, Anchors, TScaledLayout, TFlowLayout, TGridPanelLayout), vendar nima „nativnega“ Breakpoint-Systems kot na spletu. Res je mogoče reagirati na spremembe velikosti, vendar brez jasne arhitekture to pripelje do „if Width < … then …“ razpršenega po številnih Forms.
Ta prispevek predstavlja en Layout-Router: majhno komponento, ki Breakpointe centralno upravlja in Controls (ali cele layout-bloke) premika med pripravljenimi Slots. Cilj: stanja ostanejo ohranjena, koda je vzdržna, in robni primeri, kot so rotacija, gnezdene postavitve in Re-Entrancy, so ublaženi. Poleg tega nekaj manj očitnih prijemov, ki v praksi ločijo „läuft im Demo“ od „läuft stabil im Betrieb“.
Zakaj so Breakpoints v FMX drugačni kot na spletu
V spletnih postavitvah so Breakpoints večinoma deklarativni (CSS Media Queries). V FMX so odločitve o postavitvi tipično imperativne ob izvajanju: pri OnResize se preklopi. Poleg tega so prisotne platformno specifične posebnosti:
- Device-Pixel vs. logische Pixel:
ClientWidth/ClientHeightso v logičnih enotah (odvisno od skaliranja). Spremembe DPI (npr. Windows Per-Monitor-DPI) lahko sprožijo novo postavitev, ne da bi se „fizično“ kaj spremenilo. - Rotacija in Safe Areas: Mobilne platforme zagotavljajo Insets (Notch/Safe Area) – odvisno od OS in Device. „Breakpoint, ki upošteva le širino“ je pogosto preozek pristop, saj je uporabna površina manjša od same velikosti okna.
- Layout-Pass: FireMonkey izračunava postavitve v fazah. Če v napačnem trenutku spremenite Parent/Align, se pojavijo stranski učinki (npr. večkratni Reflow ali utripajoče velikosti).
Layout-Router naslovi to tako, da (1) odklopi „kdaj“ (Resize/Scale/Rotation) od „kako“ (Layout-Regeln) in (2) centralizira pravila na enem mestu. Za tehnične Leads je najpomembnejši učinek: dobijo jasno, preverljivo odločevalno središče namesto številnih lokalnih posebnih primerov.
Arhitektura: Layout-Router s Slots namesto Control-Erzeugung
Čist trik za FMX: ne dinamično Controls znova ustvarjati, temveč obstoječe Controls med Slots prestavljati. Slot je preprosto vsebnik (npr. TLayout), ki predstavlja del UI: Sidebar, Toolbar, Content, Footer, Details-Pane.
Prednosti v prilagojeni poslovni programski opremi:
- Stanja ostanejo ohranjena (Edit-Text, Scrollposition, izbrani elementi), ker se instance ne ustvarjajo znova.
- Manjše tveganje dvojne ožičitve dogodkov, Timerjev ali Bindings.
- Layout-pravila postanejo pregledna: „kateri blok leži v katerem Slotu“ je mogoče slediti in pregledati za vsak Breakpoint.
Pomembno za prakso: razdelite UI-bloke dovolj grobo. Če premikate 30 posameznih kontrolnikov, sam seznam poti postane vir napak. Bolje so kontejnerji, kot so layFilterBar, layNavigation, layResultList, layDetails.
Izsek izvorne kode: Breakpoint-router za odzivne postavitve FMX
Naslednja koda je mišljena kot pomožna enota, ki jo lahko uporabite v FMX-formah. Izračuna breakpoint (XS/SM/MD/LG/XL) in pripne določene kontrolnike v določene slot-kontejnerje. Pomembne podrobnosti:
- Debounce preko
TThread.ForceQueue: več dogodkov Resize se združi v eno posodobitev (manj tresenja uporabniškega vmesnika, manj reflow-zank). - Zaščita pred ponovnim vstopom (Re-Entrancy): posodobitev postavitve pogosto sama ponovno sproži Resize/Layout.
- Neobvezno: orientacija (Portrait/Landscape) se lahko vključi v logiko breakpointov.
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);
// Mapiranje: kateri Control naj se pri posameznem Breakpointu postavi v kateri Slot (kontejner).
TNBRoute = record
Control: TControl;
TargetSlot: TControl; // ponavadi TLayout ali 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; // ročno ponovno izračunanje
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 ne smeta biti 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: uporabiti le enkrat na zanko sporočil
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;
// Pozor: sprememba Parent spreminja Z-Order.
// Če je vrstni red pomemben, pokličite DefineRoute v želenem vrstnem redu.
for LRoute in LList do
begin
if (LRoute.Control.Parent <> LRoute.TargetSlot) then
LRoute.Control.Parent := LRoute.TargetSlot;
// Align nastaviti šele po nastavitvi Parent, sicer se lahko Bounds interpretira drugače.
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 so namerno grobi, saj se ciljna okolja FMX močno razlikujejo.
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.
Kako uporabiti Router v obrazcu
Definirate reže kot TLayout (npr. layTop, layLeft, layContent) in nato za vsak Breakpoint registrirate, kje kateri bloki ležijo. Tipično je, da se Sidebar in podokno s podrobnostmi pri majhnih breakpointih premikata eden pod drugim.
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;Položaj v praksi: Zakaj je »premestitev« pogosto stabilnejša kot preklapljanje Visible
Razširjen pristop je imeti za vsako varianto ločena drevesa postavitve in le preklapljati Visible. V urejevalniku se zdi priročno, vendar ima tipične stranske učinke:
- Dvojno vezanje/dogodki: Dva podobna kontrolnika je treba sinhronizirati (npr. dve vrstici filtrov).
- Vrstni red zavihkov in fokus: Pri preklopu lahko izgubite fokus ali pristane v nevidnih kontrolnikih, če so TabStop/HitTest nepravilno nastavljeni.
- Odstopanje stanja: Položaji drsenja, izbranost ali urejana besedila se lahko razhajajo.
Premestitev ohranja instanco enotno. Pomembno je razdeliti postavitvene bloke tako, da jih je mogoče neodvisno premikati (npr. obravnava „Sidebar“ kot lasten kontejner namesto številnih posameznih kontrolnikov). To se obrestuje pri vzdrževanju in analizi napak: odpravljate napake v eni instanci, ne v dveh vzporednih sencah UI.
Pasti v praksi (in kako jih odpraviti)
1) Nevroje spreminjanja velikosti in re-entrancy
FMX sproži OnResize ne le pri uporabniškem spreminjanju velikosti, temveč tudi pri zamenjavah stilov, spremembah staršev in deloma pri spremembah DPI. Brez odjave (debounce) se aplikacija ujame v zanke postavitve. Router uporablja TThread.ForceQueue, da spremembe prestavi v naslednji UI-tik.
Nasvet za odpravljanje napak: beleženje (npr. z OutputDebugString) z informacijami o breakpoints, velikosti in števcem posodobitev pomaga najti reflow-zanke. Če dodatno zabeležite čas začetka in konca ApplyRoutes, hitro vidite, ali en sam resize sproži kaskado nadaljnjih sprememb.
2) Z-Order, HitTest in „nevidni“ blokatorji klikov
Sprememba starša (Parent-Wechsel) spremeni Z-Order. Če overlayi (npr. Flyouts) ne sprejemajo klikov, je pogosto razlog v tem, da je nad njimi container poravnan kot Client in ima aktiven HitTest. Rešitev: za površine overlaya namenite namenski slot povsem na vrhu in tam parentajte takšne kontrolnike. V FMX je HitTest (ali kontrolnik prestreza miško-/touch-dogodke) pogosteje vzrok kot sama vidljivost.
3) TGridPanelLayout und prozentuale Größen
TGridPanelLayout lahko pri odstotnih stolpcih/vrsticah v kombinaciji z Align=Client in dinamičnim prevešanjem sproži nepričakovane prerazračune. Če morate uporabiti Grid, postavite Grid v slot in prevešajte le cele Grid-bloke, ne Grid-otrok. To zmanjša kombinatoriko layout-potek.
4) Fokus, virtualna tipkovnica und „poskakujoča“ vnosna polja
Gre za robni primer, ki se pojavlja v mobilnih FMX-aplikacijah in tudi na Windows-tablicah: pri prevešanju lahko osredotočeno Edit-Control za kratek čas izgubi Parent. To lahko zapre virtualno tipkovnico ali ponastavi kurzor. Praktično se je izkazalo: pred routanjem začasno shranite trenutni fokus (Focused/IFMXFocusControl), po routanju (v istem UI-tiku) fokus obnovite. To se posebej izplača pri vnosnih maskah, ki prehajajo med „dvostolpnim“ (Tablet/PC) in „enostolpnim“ (Phone) prikazom.
Variacije: Breakpointi po formfaktorju namesto samo po širini
V resničnih večplatformnih klientih sama „širina“ pogosto ni pravi signal. Smiselne različice:
- Širina in višina: zelo plitka okna (npr. blagajniški terminali, deljeni zasloni) potrebujejo drugačna pravila.
- Orientacija:
Landscapena tablicah je pogosto „podobno namizju“, Portrait pa bolj „mobilno“. - Uporabna površina Safe-Area: na iOS/Android se lahko efektivna uporabna višina zaradi sistemskih vrstic občutno skrči. Kdor upošteva le
Height, včasih routa „prepozno“.
Router je namerno zgrajen tako, da lahko zamenjate funkcijo breakpointov. To je tudi koristno v legacy-situacijah, ko ista forma teče v več hostih (npr. enkrat kot navadno okno, drugič v vgrajenem kontejnerju).
Neobičajno čisto: Layout-Routing kot „transakcija“
Pri večjih zaslonih težava manj leži v samih breakpointih kot v vrstnem redu UI-operacij. Praxistip je obravnavati routing kot transakcijo: najprej odločite, nato preklopite, nato stranske učinke (Visibility, Fokus, osvežitev podatkov) urejeno izvedete.
To pomeni v praksi: izogibajte se, da posamezni controli med prevešanjem sprožajo lastne dogodke, ki nato sprožijo layout ali dostop do podatkov. V FMX se to zgodi na primer, ko ob menjavi Parent-a sprožita OnEnter/OnExit ali ko se LiveBinding-izraz ob posodobitvi Bounds ponovno evaluira. Če opažate takšne učinke, pomaga centralni „Updating“-stikalo (kot v Routerju) plus jasen post-korak: šele po ApplyRoutes smejo drage operacije teči (npr. ponovno nalaganje seznama, vezava pogleda podrobnosti).
Posebno pri klientih z dostopom do REST je to pomembno: nezaželen reload med spreminjanjem velikosti lahko sproži nepotrebne zahteve. To v LANu ni očitno, v VPN ali mobilnem okolju pa takoj.
Kdaj se pristop izplača – und kje ima omejitve
Layout-Router se izplača, kadar:
- FMX-aplikacija živi več let in več razvijalcev dela na istih zaslonih,
- UI-bloki se lahko jasno ločijo (Sidebar/Details/Content),
- potrebujete reproducibilna pravila breakpointov namesto ad-hoc prilagajanja Align-lastnosti.
Meje boste začutili, ko mora biti zaslon močno „fluid“ (množica dinamičnih ploščic, resnični Masonry-postavitve). V takih primerih so TFlowLayout/TGridPanelLayout ali lastni razredi za postavitve primernejši. Tudi če se zelo veliko posameznih kontrol premešča med sloti, postane vzdrževanje poti nepregledno – takrat je bolje razrezati večje bloke ali uvesti deklarativno konfiguracijsko plast (npr. JSON-konfiguracijo za dodelitve slotov, ki se naloži ob zagonu).
Zaključek: Za responsive postavitve v FMX je „Umhängen mit Breakpoints“ pragmatična srednja pot: manj kaosa v oblikovalniku, jasna pravila, stabilna stanja. Ne nadomešča premišljene UI-strukture, vendar vam zagotavlja zanesljivo ogrodje za kontroliran razvoj FMX-klientov v digitalnih podjetniških rešitvah prek različnih formfaktorjev.
Če želite v obstoječi Delphi- ali FMX-aplikaciji takšno arhitekturo postavitve dosledno uvesti, brez tveganja UI-regresij v obratovalnih scenarijih, lahko to tehnično ovrednotite z nami: projekt ali modernizacijski načrt razpravljajte z Net-Base.
V strokovnem kontekstu imajo tudi Delphi Fmx Breakpoints in Firemonkey Layout pomembno vlogo, kadar je treba integracije, podatkovne tokove in nadaljnji razvoj uskladiti.
O projektu ali modernizacijskem načrtu se pogovorite z Net-Base.
Naslednji korak
Ko se tema spremeni v dejanski projekt, je treba arhitekturo, obstoječi sistem in obratovanje zgodaj obravnavati skupaj.
Ne podpiramo le pri posameznih vprašanjih, ampak tudi takrat, ko iz izrezkov izvorne kode, legacy-tem ali idej za portale nastane zanesljiv podjetniški projekt.
- Obstoječe stanje, ciljno stanje in tehnična tveganja se ocenjujejo skupaj.
- REST, dostop do podatkov, portali in uvedba niso prestavljeni kot poznejše posledice.
- Zgodaj prepoznate, katera pot je ekonomsko in obratovalno vzdržna.