Net-Base Dergi

03.06.2026

Responsive Düzenler Delphi FMX'de: Designer karmaşası olmadan Breakpoint'ler (Layout-Router ile kaynak kodu örneği)

FMX'de responsive düzenler pratikte hızla kırılganlaşır: yeniden boyutlandırma fırtınaları, DPI değişimleri, rotasyon ve "Visible-Layouts" çifte durumlar ve hata ayıklaması zor yeniden akışlar (reflows) oluşturur. Bu yazı, çalışma zamanında UI bloklarını kontrol eden, breakpoint'lere sahip bir layout yönlendirici gösteriyor...

03.06.2026

Dergi konusundan proje pratiğine

İçeriğe Uygun Hizmet ve Teknik Sayfalar

Birisi Delphi FireMonkey’de birden fazla form faktörünü desteklemek zorunda kalanlar hızla Responsive Layouts FMX‚e yönelir – ve aynı hızla, bir sonraki DPI veya rotasyon değişikliğinde çökecek Align kaskadları, gizlenmiş layout konteynerleri ve tasarımcı geçici çözümlerinin bir karışımıyla karşılaşırlar. Gelişmiş kurumsal yazılım istemcilerinde bu özellikle rahatsız edicidir: UI geliştirilir, ekipler değişir ve aniden mantık görsel detaylara bağlı hale gelir.

Sorunun özü: FireMonkey birçok yapı taşı sunar (z. B. Align, Anchors, TScaledLayout, TFlowLayout, TGridPanelLayout), ancak web’deki gibi yerleşik bir breakpoint sistemi yoktur. Boyut değişikliklerine tepki verilebilir, fakat net bir mimari olmadan bu birçok formda „if Width < … then …“ ifadelerine dağılır.

Bu yazı bir Layout-Router gösteriyor: breakpoint’leri merkezi olarak yöneten ve kontrolleri (veya tüm layout bloklarını) önceden hazırlanmış slotlar arasında yeniden bağlayan küçük bir bileşen. Amaç: durumların korunması, kodun bakımı kolay olması ve rotasyon, iç içe geçmiş layout’lar ve re-entrancy gibi kenar durumların hafifletilmesi. Ayrıca uygulamada „demo’da çalışıyor“ ile „üretimde stabil çalışıyor“ arasındaki farkı yaratan birkaç daha az belirgin numara da yer alır.

FMX’te Breakpoint’ler Web’dekinden Neden Farklıdır

Web layout’larında breakpoint’ler genellikle deklaratiftir (CSS Media Queries). FMX’te layout kararları çalışma zamanında tipik olarak imperatif olur: OnResize sırasında geçiş yapılır. Buna ek platforma özgü ayrıntılar vardır:

  • Cihaz pikseli vs. mantıksal piksel: ClientWidth/ClientHeight ölçeklemeye bağlı olarak mantıksal birimlerdedir. DPI değişiklikleri (ör. Windows Per-Monitor-DPI) layout’ları fiziksel olarak bir şey değişmemiş olsa bile yeniden tetikleyebilir.
  • Dönüş ve Safe Area’lar: Mobil platformlar inset (Notch/Safe Area) değerleri sağlar – işletim sistemi ve cihaza bağlıdır. Sadece genişliğe göre bir ‚breakpoint‘ genellikle eksik kalır, çünkü kullanılabilir alan ham pencere boyutundan daha küçüktür.
  • Layout geçişi: FireMonkey layout’ları aşamalar halinde hesaplar. Yanlış zamanda Parent/Align değiştirirseniz yan etkiler ortaya çıkar (örn. birden fazla reflow veya titreyen boyutlar).

Bir Layout-Router bunu ele alır; (1) ’ne zaman’ı (Resize/Scale/Rotation) ’nasıl’dan (layout kuralları) ayırarak ve (2) kuralları tek bir yerde toplayarak. Teknik liderler için en önemli etki: birçok yerel istisna yerine net ve denetlenebilir bir karar merkezi elde etmeleridir.

Mimari: Kontrol Oluşturmaya Kıyasla Slots ile Layout-Router

FMX için temiz hile: kontrolleri dinamik olarak yeniden oluşturmak değil, bunun yerine mevcut kontrolleri Slots arasında taşımaktır. Bir Slot basitçe UI’nin bir bölümünü temsil eden bir konteynerdir (örn. TLayout): Sidebar, Toolbar, Content, Footer, Details-Pane.

Bireysel kurumsal yazılımlarda avantajlar:

  • Durumlar korunur (edit metni, kaydırma pozisyonu, seçili öğeler), çünkü örnekler yeniden oluşturulmaz.
  • Event’lerin, timer’ların veya binding’lerin çift bağlanma riski daha düşüktür.
  • Layout kuralları görünür hale gelir: ‚hangi blok hangi Slot’ta‘ olduğu her breakpoint için izlenebilir ve incelenebilir.

Uygulama için önemli: UI bloklarını yeterince kaba ayırın. Eğer 30 adet bireysel kontrolü yeniden bağlarsanız, rota listesi kendisi hata kaynağı olur. Daha uygun olan, layFilterBar, layNavigation, layResultList, layDetails gibi konteynerlerdir.

Kod örneği: FMX için Responsive Düzenler için Breakpoint-Router

Aşağıdaki kod, FMX-formlarında kullanabileceğiniz bir yardımcı birim olarak tasarlanmıştır. Bir breakpoint (XS/SM/MD/LG/XL) hesaplar ve tanımlı kontrolleri tanımlı slot-konteynerlerine yeniden bağlar. Önemli detaylar:

  • Debounce TThread.ForceQueue aracılığıyla: birden fazla yeniden boyutlandırma olayı tek bir güncellemeye toplanır (daha az UI titremesi, daha az yeniden akış döngüsü).
  • Re-Entrancy koruması: Yerleşim güncellemesi çoğu kez yeniden boyutlandırma/yerleşim tetikler.
  • Opsiyonel: Yönelim (Dikey/Yatay) breakpoint mantığına dahil edilebilir.
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);

  // Bir eşleme: hangi Control'un belirli bir breakpoint için hangi slot (Container) içinde olması gerektiği.
  TNBRoute = record
    Control: TControl;
    TargetSlot: TControl; // tipik olarak TLayout veya 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; // manuel olarak yeniden hesaplama
    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 nil olmamalıdır');

  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: yalnızca mesaj döngüsü başına bir kez uygula
  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;

    // Dikkat: Parent değişimi Z-Order'ü değiştirir.
    // Sıra önemliyse, DefineRoute'u istenen sırayla çağırın.
    for LRoute in LList do
    begin
      if (LRoute.Control.Parent <> LRoute.TargetSlot) then
        LRoute.Control.Parent := LRoute.TargetSlot;

      // Align'i önce Parent'e atayın, aksi takdirde Bounds farklı yorumlanabilir.
      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;
  // Breakpoint'ler kasıtlı olarak kabaca tanımlandı; FMX hedef platformları büyük ölçüde farklılık gösterir.
  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.

Bir formda Router’ı kullanma

Slotları TLayout olarak tanımlarsınız (ör. layTop, layLeft, layContent) ve sonra her breakpoint için hangi blokların nerede olduğunu kaydedersiniz. Tipik olarak, küçük breakpointlerde Sidebar ve Details-Pane alt alta taşınır.

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;

Sınıflandırma: ‚Parent değiştirme‘ genellikle Visible değiştirmekten neden daha stabildir

Yaygın bir yaklaşım her varyant için ayrı layout ağaçları tutmak ve sadece Visible değerini değiştirmektir. Tasarımcıda pratik görünür, fakat tipik yan etkilere sahiptir:

  • Çift Binding/Etkinlikler: İki benzer kontrol eşzamanlı tutulmak zorunda (ör. iki filtre çubuğu).
  • Tab sıralaması ve odak: Geçişte odak kaybolabilir veya TabStop/HitTest uygun değilse görünmez kontrollerde sonlanabilirsiniz.
  • Durum sapması: Kaydırma pozisyonları, seçim durumları veya düzenlenen metinler farklılaşır.

Parent değiştirme nesnenin tekil kalmasını sağlar. Önemli olan, layout bloklarını bağımsız taşınabilecek şekilde bölmektir (ör. „Sidebar“ı birçok tekil kontrol yerine kendi konteyneri olarak tutmak). Bu, bakım ve hata analizinde kendisini gösterir: bir örneği debuglarsınız, iki paralel gölge UI’yi değil.

Uygulamada sık karşılaşılan tuzaklar (ve nasıl hata ayıklanır)

1) Resize fırtınaları ve yeniden-giriş

FMX OnResize‚i yalnızca kullanıcı yeniden boyutlandırmasında değil, aynı zamanda stil değişikliklerinde, parent değişimlerinde ve kısmen DPI değişimlerinde de tetikler. Debounce olmadan uygulama layout döngülerinde takılır. Router, değişiklikleri bir sonraki UI tick’ine ertelemek için TThread.ForceQueue kullanır.

Hata ayıklama ipucu: Breakpoint, boyut ve bir güncelleme sayacı ile logging (ör. OutputDebugString üzerinden) reflow döngülerini bulmaya yardımcı olur. Ayrıca ApplyRoutes‚un başladığı ve bittiği zamanı loglarsanız, tek bir resize’in „kaskadlandığını“ hızlıca görürsünüz.

2) Z-Order, HitTest ve „görünmez“ tıklama engelleyiciler

Parent değişimi Z-Order’ı değiştirir. Overlays (ör. Flyouts) artık tıklanmıyorsa genellikle nedeni, üzerinde client-aligned bir konteynerin bulunması ve HitTest‚in aktif olmasıdır. Çözüm: Overlay alanları için bilinçli olarak en üstte ayrı bir slot ayırmak ve sadece bu slot’a bu tür kontrolleri parent etmek. FMX’te HitTest (bir kontrolün fare-/dokunma olaylarını yakalayıp yakalamadığı) görünürlükten daha sık sebep olur.

3) TGridPanelLayout ve yüzde tabanlı boyutlar

TGridPanelLayout, yüzde sütun/satır kullanıldığında Align=Client ile birlikte ve dinamik yeniden bağlamada beklenmedik yeniden hesaplamalara yol açabilir. Grid kullanmak zorundaysanız, Grid’i bir slota yerleştirin ve yalnızca tüm Grid bloklarını yeniden bağlayın; Grid çocuklarını yeniden bağlamayın. Bu, layout geçişlerinin kombinasyon sayısını azaltır.

4) Odak, sanal klavye ve ‚zıplayan‘ giriş alanları

Mobil FMX uygulamalarında ve ayrıca Windows tabletlerde görülen bir kenar durum: Yeniden bağlama sırasında odaklanmış bir Edit kontrolü kısa süreliğine Parent’ını kaybedebilir. Bu sanal klavyeyi kapatabilir veya imleci sıfırlayabilir. Pratikte etkili olan yöntem: Yönlendirmeden önce mevcut odağı geçici olarak saklamak (Focused/IFMXFocusControl), yönlendirmeden sonra (aynı UI-Tick içinde) odağı geri yüklemek. Bu, özellikle ‚iki sütunlu‘ (Tablet/PC) ile ‚tek sütunlu‘ (Phone) arasında geçiş yapan giriş formları için önemlidir.

Varyantlar: Sadece genişliğe değil, form faktöre göre Breakpoint’ler

Gerçek çoklu platform istemcilerinde yalnızca „genişlik“ çoğunlukla doğru sinyal değildir. Mantıklı varyantlar:

  • Genişlik ve yükseklik: çok sığ pencereler (ör. POS-Terminaleri, bölünmüş ekranlar) farklı kurallar gerektirir.
  • Yönelim: Landscape tabletlerde genellikle „masaüstü-benzeri“, Portrait ise daha çok „mobil“ gibidir.
  • Safe-Area kullanılabilir alanı: iOS/Android’de efektif kullanılabilir yükseklik sistem çubukları nedeniyle önemli ölçüde kısalabilir. Sadece Height‚e bakanlar bazen „çok geç“ yönlendirme yapar.

Router, Breakpoint işlevini değiştirebilmenize imkan verecek şekilde bilinçli olarak inşa edilmiştir. Bu, aynı formun birden çok hostta çalıştığı legacy durumlarda da yardımcı olur (ör. bir defa normal pencere, bir defa gömülü bir konteyner içinde).

Olağandışı temiz: Layout-yönlendirmesini bir „işlem“ olarak ele almak

Daha büyük ekranlarda sorun, Breakpoint’lerin kendisinden çok UI operasyonlarının sıralamasından kaynaklanır. Pratik bir desen, yönlendirmeyi bir işlem olarak ele almaktır: önce karar verin, sonra yeniden bağlayın, ardından yan etkileri (görünürlük, odak, veri yenileme) düzenli şekilde çalıştırın.

Somut olarak bu şunu ifade eder: Yeniden bağlama sırasında tek tek kontrollerin kendi eventlerini tetleyip bunun sonucunda layout veya veri erişimini başlatmasını engelleyin. FMX’te bunun örneği, Parent değiştiğinde OnEnter/OnExit‚in tetiklenmesi veya bir LiveBinding ifadesinin bir Bounds güncellemesiyle yeniden değerlendirilmesidir. Böyle etkiler görüyorsanız, merkezi bir „Updating“ anahtarı (Router’daki gibi) ve net bir post-adım yardımcı olur: Sadece ApplyRoutes sonrası maliyetli işler çalıştırılmalıdır (ör. listeyi yeniden yüklemek, detay görünümünü bağlamak).

Özellikle REST erişimi olan istemcilerde bu önemlidir: Bir yeniden boyutlandırma sırasında istemeden tetiklenen bir yeniden yükleme gereksiz istekler oluşturabilir. Bu LAN’da fark edilmez, ancak VPN veya mobilde hemen ortaya çıkar.

Yöntemin avantajlı olduğu durumlar — ve sınırları

Layout-yönlendirici şu durumlarda faydalıdır:

  • bir FMX uygulaması yıllarca kullanılmaya devam ediyor ve aynı ekranlar üzerinde birden çok geliştirici çalışıyorsa,
  • UI blokları net şekilde ayrılabiliyorsa (kenar çubuğu / detay / içerik),
  • ad-hoc Align ince ayarları yerine, yeniden üretilebilir Breakpoint kurallarına ihtiyaç duyuyorsanız.

Sınırlar, bir ekranın çok „fluid“ olması gerektiğinde ortaya çıkar (çok sayıda dinamik kart, gerçek Masonry düzenleri). Bu durumda TFlowLayout/TGridPanelLayout veya özel layout sınıfları daha uygun olur. Çok sayıda bireysel kontrol slotlar arasında değişiyorsa, rotaların bakımı da karmaşıklaşır – bu durumda daha büyük bloklara ayırmak veya deklaratif bir yapılandırma katmanı eklemek daha iyidir (ör. başlatmada yüklenen slot atamaları için bir JSON yapılandırması).

Sonuç: FMX için duyarlı düzenlerde „Breakpoint’lerle yeniden yerleştirme“ pragmatik bir orta yol sunar: daha az tasarım aracı kaosu, net kurallar, stabil durumlar. Bu iyi düşünülmüş bir UI yapısının yerini almaz; ancak FMX istemcilerini dijital kurumsal çözümlerde form faktörleri boyunca kontrollü şekilde geliştirmek için dayanıklı bir iskelet sağlar.

Mevcut bir Delphi- veya FMX uygulamasında böyle bir layout mimarisini, işletme senaryolarında UI gerilemelerini riske atmadan titizlikle uygulamak isterseniz, bunu teknik olarak bizimle değerlendirebilirsiniz: Proje veya modernizasyon çalışmasını Net-Base ile görüşün.

Uzmanlık bağlamında, entegrasyonlar, veri akışları ve devam eden geliştirme düzgün bir şekilde birlikte çalışmak zorunda olduğunda, Delphi Fmx Breakpoints ve Firemonkey düzeni de önemli bir rol oynar.

Proje oder Modernisierungsvorhaben mit Net-Base besprechen.

Sonraki adım

Konu gerçek bir projeye dönüştüğünde, mimari, mevcut yapı ve işletme erken aşamada birlikte ele alınmalıdır.

Wir unterstuetzen nicht nur bei Einzelfragen, sondern auch dann, wenn aus Source-Schnipseln, Legacy-Themen oder Portalideen ein belastbares Unternehmensprojekt werden soll.

  • Mevcut durum, hedef durum ve teknik riskler birlikte değerlendirilir.
  • REST, veri erişimi, portallar ve Rollout sonraki işler olarak ertelenmez.
  • Hangi yolun ekonomik ve işletme açısından uygulanabilir olduğunu erken görürsünüz.

Gönderiyi paylaş

Bu gönderiyi doğrudan paylaş

LinkedIn, X, XING, Facebook, WhatsApp ve e-posta hemen kullanılabilir. Instagram için bağlantı ve kısa metni doğrudan hazırlıyoruz.

E-posta

Instagram yeni bir sekmede açılır. Bağlantı ve kısa metin önceden panoya kopyalanır.