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.ForceQueuearacı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.
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.
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/HitTestuygun 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:
Landscapetabletlerde 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.
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.