Net-Base Tímarit

03.06.2026

Aðlögunarhæf skipulag í Delphi FMX: Breakpoints án Designer-óreiðu (með Layout-Router sem kóðasýni)

Responsive Layouts FMX verða í framkvæmd fljótt brothætt: hröðar, endurteknar stærðarbreytingar, DPI-breytingar, skjásnúningur og „Visible-Layouts“ skapa tvöfalt ástand og endurskipulagningar (reflows) sem erfitt er að kemba. Þessi grein sýnir Layout-Router með Breakpoints sem stýrir UI-blokkum á keyrslutíma.

03.06.2026

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/ClientHeight eru í 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.
Delphi
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.

Delphi
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 á Height skiptir 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.

Ræddu verkefni eða moderniseringarverkefni með Net-Base.

Næsta skref

Ef efnið verður að raunverulegu verkefni, ætti snemma að skoða kerfisarkitektúr, núverandi kerfi og rekstur í sameiningu.

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, aðgangur að gögnum, gáttir og innleiðing verða ekki flutt til síðari tíma sem afleiðingar.
  • Þú sérð snemma hvaða leið er efnahagslega og rekstrarlega framkvæmanleg.

Deila færslu

Deila þessari færslu beint

LinkedIn, X, XING, Facebook, WhatsApp og tölvupóstur eru strax í boði. Fyrir Instagram undirbúum við tengil og stuttan texta strax.

Tölvupóstur

Instagram opnast í nýjum flipa. Tengill og stuttur texti eru afritaðir í klippiborðið á undan.