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

Þ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.

Deila færslu

Deila þessari færslu beint

LinkedIn, X, XING, Facebook, WhatsApp og tölvupóstur eru strax tiltækir. Fyrir Instagram undirbúum við hlekk og stuttan texta beint.

Tölvupóstur

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