Frá tímaritsþema til verkefnaframkvæmdar
Viðeigandi þjónustu- og tæknisíður fyrir greinina
Hver sem þarf að styðja marga formþætti í Delphi FireMonkey lendir fljótt við Responsive Layouts FMX – og jafnframt í blöndu af Align-kasköðum, faldum layout-gámum og Designer-Workarounds sem víkja við næsta DPI- eða snúningsbreytingu. Í rótgrónum Business-Software-Clients er þetta sérstaklega óþægilegt: Viðmótið þróast áfram, teymi skiptast út, og skyndilega hangir rökfræði á sjónrænum smáatriðum.
Kjarni vandamálsins: FireMonkey býður upp á marga byggingarþætti (t.d. Align, Anchors, TScaledLayout, TFlowLayout, TGridPanelLayout), en ekkert „innbyggt“ Breakpoint-System eins og á vefnum. Það er vissulega hægt að bregðast við með stærðarbreytingum, en án skýrra arkitektúra endar það í „if Width < … then …“ dreift yfir mörg Forms.
Þessi grein sýnir einn Layout-Router: lítinn íhlut sem stýrir Breakpoints miðlægt og hengir Controls (eða heila layout-blokka) milli fyrirfram skilgreindra Slots. Markmið: Ástand haldast óbreytt, kóðinn er viðhaldshæfur, og jaðarmál eins og snúningur, innfelld útlit og Re-Entrancy eru mýkt. Auk þess eru nokkur minna augljós ráð sem í reynd greina á milli „keyrir í demo“ og „keyrir stöðugt í rekstri“.
Af hverju eru Breakpoints í FMX öðruvísi en á vefnum
Í vef-útlitum eru Breakpoints oft lýsandi (CSS Media Queries). Í FMX eru útlitsákvarðanir yfirleitt skipandi í keyrslutíma: Í OnResize er skipt um. Við bætast síðan vettvangstengdir eiginleikar:
- Device-Pixel vs. logische Pixel:
ClientWidth/ClientHeighteru í röklegum einingum (háð skalun). DPI-breytingar (t.d. Windows Per-Monitor-DPI) geta kveikt endurreiknun á útliti án þess að „líkamlega“ eitthvað breytist. - Rotation und Safe Areas: Farsíma-vettvangar skila Insets (Notch/Safe Area) – fer eftir stýrikerfi og tæki. „Breakpoint sem byggir aðeins á breidd“ er oft of stuttíghugsað, því nothæft svæði getur verið minna en heildarglugga-stærðin.
- Layout-Pass: FireMonkey reiknar útlit í stigum. Ef breytt er Parent/Align á röngum tímapunkti koma fram aukaverkanir (t.d. margföld endurröðun eða flöktandi stærðir).
Layout-Router mætir þessu með því að (1) aftengja „hvenær“ (Resize/Scale/Rotation) frá „hvernig“ (layout-reglum) og (2) samræma reglurnar á einum stað. Fyrir tæknilega leiðtoga er helsti ávinningurinn sá að þeir fá skýra, yfirfærða ákvörðunarmiðstöð í stað margra staðbundinna undantekninga.
Architektur: Layout-Router mit Slots statt Control-Erzeugung
Hreini bragurinn fyrir FMX: ekki að búa til Controls á nýju heldur hengja til staðarverandi Controls milli Slots. Slot er einfaldur gámi (t.d. TLayout) sem táknar svæði í viðmótinu: Sidebar, Toolbar, Content, Footer, Details-Pane.
Kostir í sérsmíðuðum fyrirtækjaforritum:
- Ástand haldast óbreytt (Edit-Text, Scrollposition, valin atriði), því eintök eru ekki endurgerð.
- Minna hætta á tvöfaldri tengingu atburða, tímamæla eða Bindings.
- Útlitsreglur verða sýnilegar: „hvort blokk liggur í hvaða Slot“ má rekja fyrir hvern Breakpoint og yfirfara.
Mikilvægt í framkvæmd: Skerið UI-blokka nægilega gróflega. Ef þið flytjið 30 einstaka stjórnþætti um, getur leiðalistinn sjálfur orðið orsök villna. Betri eru gámar eins og layFilterBar, layNavigation, layResultList, layDetails.
Kóðasneið: Breakpoint-Router fyrir móttækileg FMX-útlit
Eftirfarandi kóði er hugsaður sem hjálpareining sem þið getið notað í FMX-Forms. Hann reiknar Breakpoint (XS/SM/MD/LG/XL) og flytur skilgreinda stjórnþætti yfir í skilgreinda slot‑gáma. Mikilvægar upplýsingar:
- Debounce með
TThread.ForceQueue: mörg Resize-atvik eru sameinuð í eina uppfærslu (minna titringur í UI, færri reflow‑lykkjur). - Vernd gegn endurkomu: Uppfærsla á útliti kallar oft aftur á Resize/Layout.
- Valfrjálst: Stefna (Portrait/Landscape) getur flætt inn í Breakpoint‑rökfræði.
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);
// Kortlagning: hvaða Control á að vera í hvaða Slot (container) fyrir ákveðinn Breakpoint.
TNBRoute = record
Control: TControl;
TargetSlot: TControl; // typischerweise TLayout oder 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; // handvirkt endurreikna
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 mega ekki vera 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: aðeins beitt einu sinni í hverri skilaboðalykkju
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;
// Athugið: Parent-breyting breytir Z-röð.
// Ef röð skiptir máli, kalla DefineRoute í þeirri röð sem óskað er.
for LRoute in LList do
begin
if (LRoute.Control.Parent <> LRoute.TargetSlot) then
LRoute.Control.Parent := LRoute.TargetSlot;
// Stilltu Align aðeins eftir að Parent hefur verið sett, annars geta Bounds verið túlkuð öðruvísi.
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 eru með viljandi grófum mörkum, þar sem FMX markpallar geta verulega verið misjafnir.
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.
Hvernig á að nota Routerinn í formi
Þú skilgreinir slots sem TLayout (t.d. layTop, layLeft, layContent) og skráir svo fyrir hvern Breakpoint hvar hverjir blokkir eiga að vera. Algengt er að hliðarpanel („Sidebar“) og smáatriða-glugginn færist undir hvor annan í litlum Breakpoints.
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;Staðsetning: Af hverju er að færa milli foreldra oft stöðugra en að breyta Visible
Einn algengur aðgangur er að hafa aðskilda layout-tré fyrir hverja útgáfu og kveikja aðeins á Visible. Það er þægilegt í Designer-inu en hefur þó einkenni aukaverkana:
- Tvöfalt Binding/Events: Tvö svipuð Controls verða að haldast samstillt (t.d. tvær filter-línur).
- Tab-röð og fókus: Við skipti getur fókus farið forgörðum eða endað á ósýnilegum Controls ef TabStop/HitTest eru óhagstæð.
- State Drift: Rennustöður, valstöður eða ritunartextar geta farið úr takt.
Að færa Controls milli foreldra heldur eintakinu einu. Mikilvægt er að skipta layout-blokkum þannig að hægt sé að færa þær óháð (t.d. „Sidebar“ sem eigin gagnagrind í stað margra einstakra Controls). Þetta greiðir fyrir viðhald og bilanaleit: þú kembir eina instans, ekki tvö samsíða skugga-UI.
Algengar gildrur í framkvæmd (og hvernig á að debugga þær)
1) Stærðarstormar og endurkomu (Re-Entrancy)
FMX kallar OnResize ekki aðeins við notendaflísbreytingu heldur einnig við stílskipti, Parent-breytingar og stundum við DPI-breytingar. Án Debounce festist forritið í layout-syklusum. Routerinn notar TThread.ForceQueue til að ýta breytingum yfir í næsta UI-tick.
Debugging-tips: Logging (t.d. með OutputDebugString) með breakpoint, stærð og uppfærslu-teljara hjálpar að finna reflow-slynga. Ef þú skráir aukalega tímann þegar ApplyRoutes hefst og lýkur sérðu fljótt hvort ein stærðarbreyting „kaskadi“.
2) Z-röð, HitTest og „ósýnileg“ smelliblokkar
Breytur á Parent hafa áhrif á Z-röð. Ef overlays (t.d. Flyouts) hætta að taka við smellum stafar það oft af því að Client-aligned container er ofan á og HitTest er virkt. Lausn: vista sérstakan slot efst fyrir overlay-svæði og parenta slík Controls eingöngu þar. Í FMX er HitTest (þ.e. hvort Control grípur mús-/snertiatburði) oftar orsökin en sýnileiki.
3) TGridPanelLayout og hlutfallslegar stærðir
TGridPanelLayout getur valdið óvæntum endurreikningum þegar dálkar/röður eru skilgreindir í prósentum og það er samspil við Align=Client og dynamíska endurfesting (Umhängen). Ef þú þarft að nota Grid, settu Grid-inn í Slot og flyttu aðeins heila Grid-blokka, ekki Grid-börn. Þetta minnkar fjölda mögulegra layout-fasa.
4) Fókus, sýndarlyklaborð og inntaksreitir sem færa sig óvænt
Það er jaðartilvik sem kemur upp í farsíma-FMX-forritum og einnig á Windows-spjaldtölvum: við umhängen getur Edit-control sem er í fókus misst foreldrið tímabundið. Það getur lokað sýndarlyklaborðinu eða endurstilla bendilinn. Í framkvæmd hefur reynst gagnlegt að vista núverandi fókus fyrir routing (Focused/IFMXFocusControl) og endurheimta hann eftir routing (í sama UI-tick). Þetta borgar sig sérstaklega í inntaksformum sem skiptast á milli „tvídálks“ (Tablet/PC) og „eindálks“ (Phone).
Varianten: Breakpoints nach Formfaktor statt nur nach Breite
Í raunverulegum fjölpallsklientum er „breidd“ ein og sér oft ekki rétta merkið. Nýtilegar útgáfur:
- Breidd og hæð: mjög flatar/þunnar gluggar (t.d. kassatermínalar, deildir skjáa) þurfa aðra reglur.
- Stefna:
Landscapeá spjaldtölvum er oft „líkt desktop“, portrait frekar „farsímalegt“. - Safe-Area nýtanlegur flötur: Á iOS/Android getur áhrifarík nýtanleg hæð skerst verulega af kerfissjónarrömmum. Sá sem horfir aðeins á
Heightskiptir stundum um útlit „of seint“.
Routerinn er meðvitað hannaður þannig að þú getur skipt út breakpoints-fallinu. Þetta er einnig hjálplegt í legacy-situm þar sem sama form keyrir í fleiri en einum host (t.d. einu sinni sem venjulegur gluggi, einu sinni í innbyggðum container).
Óvenju hreint: Layout-Routing als „Transaktion“
Í stærri skjám snýst málið sjaldnast um breakpoints sjálf heldur um röð UI-aðgerða. Praktískt mynstur er að meðhöndla routing sem transaktion: ákveða fyrst, færa síðan, og framkvæma síðan aukaverkanir (Visibility, fókus, data-refresh) í skipulegri röð.
Nánar sagt: forðastu að einstök Controls kalli sín eigin viðburði meðan á umhengi stendur sem svo kveiki á frekari layout- eða gagnaaðgerðum. Í FMX gerist þetta t.d. þegar OnEnter/OnExit fer af stað við foreldrabreytingu eða þegar LiveBinding-útreikningur ræsist upp á nýtt vegna bounds-uppfærslu. Ef þú sérð slíkar aukaverkanir hjálpar miðlægur „Updating“-rofi (svo sem í Router) og skýr eftirfylgniskref: aðeins eftir ApplyRoutes mega dýr verkefni keyra (t.d. endurhlaða lista, binda smáatriðasýn).
Sérstaklega hjá klientum með REST-aðgang er þetta mikilvægt: óviljug endurhleðsla í miðri stærðarbreytingu getur kallað á óþarfa requests. Það kemur ekki vel fram í LAN en sést strax í VPN eða á farsímaneti.
Hvenær aðferðin borgar sig – og hvar hún hefur takmörk
Layout-Routerinn borgar sig þegar:
- FMX-forrit lifir yfir mörg ár og fleiri en einn þróunaraðili vinna á sömu skjám,
- UI-blokkir er hægt að aðskilja skýrt (sidebar/details/content),
- þú þarft áreiðanlegar, endurtekningarhæfar breakpoint-reglur frekar en ad-hoc Align-fínstillingu.
Takmarkanir koma í ljós þegar skjár þarf að vera mjög „fluid“ (margar dýnamískar flísar, raunveruleg Masonry-útlit). Þá henta TFlowLayout/TGridPanelLayout eða sérhæfðir layout-klasar betur. Ef mjög mörg einstök Controls flakka á milli Slots verður viðhald leiða óyfirstíganlegt – þá er betra að skera stærri blokka eða leggja inn deklaratíft stillingarlag (t.d. JSON-stilling fyrir Slot-úthlutanir sem er hlaðin við ræsingu).
Niðurstaða: Für Responsive-Layouts FMX er „Umhängen mit Breakpoints“ ein pragmatischer Mittelweg: minna hönnuðaróreiðu, skýr reglur, stöðug ástand. Það kemur ekki í stað vel útfærðrar UI-strúktúru, en það veitir ykkur áreiðanlegt grindverk til að þróa FMX-Clients í stafrænum fyrirtækjalausnum yfir mismunandi formþætti á stjórnaðan hátt.
Ef þið viljið í fyrirliggjandi Delphi- eða FMX-forriti útfæra slíka uppsetningararkítektúr án þess að hætta á UI-regressíum í rekstrarsenarium, getið þið metið það tæknilega með okkur: ræðum verkefni eða moderniseringarverkefni með Net-Base.
Á faglegu sviði gegna einnig Delphi FMX Breakpoints og Firemonkey-útlit mikilvægu hlutverki þegar samþættingar, gagnastreymar og áframhaldandi þróun þurfa að vinna vel saman.
Næsta skref
Þegar úr málinu verður raunverulegt verkefni ber að skoða arkitektúr, núverandi kerfi og rekstur snemma saman.
Við styðjum ekki aðeins við einstakar spurningar, heldur einnig þegar úr kóðabútum, eldri kerfum eða gáttahugmyndum þarf að verða traust fyrirtækjaverkefni.
- Núverandi staða, markmynd og tæknileg áhætta eru metin saman.
- REST, gagnaaðgangur, gáttir og innleiðing eru ekki skildir eftir til síðar.
- Það sést snemma hvaða leið er fjárhagslega og rekstrarlega sjálfbær.