Vom Magazinthema zur Projektpraxis
Passende Leistungs- und Technikseiten zum Beitrag
Wer in Delphi FireMonkey mehrere Formfaktoren bedienen muss, landet schnell bei Responsive Layouts FMX – und ebenso schnell bei einer Mischung aus Align-Kaskaden, versteckten Layout-Containern und Designer-Workarounds, die beim nächsten DPI- oder Rotationswechsel kippen. In gewachsenen Business-Software-Clients ist das besonders unangenehm: Die UI wird weiterentwickelt, Teams wechseln, und plötzlich hängt Logik an visuellen Details.
Der Kern des Problems: FireMonkey bietet viele Bausteine (z. B. Align, Anchors, TScaledLayout, TFlowLayout, TGridPanelLayout), aber kein „natives“ Breakpoint-System wie im Web. Man kann zwar über Größenänderungen reagieren, aber ohne klare Architektur endet das in „if Width < … then …“ über viele Forms verteilt.
Dieser Beitrag zeigt einen Layout-Router: eine kleine Komponente, die Breakpoints zentral verwaltet und Controls (oder ganze Layout-Blöcke) zwischen vorbereiteten Slots umhängt. Ziel: Zustände bleiben erhalten, der Code ist wartbar, und Randfälle wie Rotation, verschachtelte Layouts und Re-Entrancy werden abgefedert. Dazu kommen ein paar weniger offensichtliche Kniffe, die in der Praxis den Unterschied zwischen „läuft im Demo“ und „läuft stabil im Betrieb“ ausmachen.
Warum Breakpoints in FMX anders sind als im Web
In Web-Layouts sind Breakpoints meist deklarativ (CSS Media Queries). In FMX sind Layoutentscheidungen zur Laufzeit typischerweise imperativ: Beim OnResize wird umgestellt. Dazu kommen plattformspezifische Eigenheiten:
- Device-Pixel vs. logische Pixel:
ClientWidth/ClientHeightsind in logischen Einheiten (abhängig von Skalierung). DPI-Wechsel (z. B. Windows Per-Monitor-DPI) können Layouts neu triggern, ohne dass sich „physisch“ etwas ändert. - Rotation und Safe Areas: Mobile Plattformen liefern Insets (Notch/Safe Area) – abhängig von OS und Device. Ein „Breakpoint nur nach Breite“ ist oft zu kurz gedacht, weil die nutzbare Fläche kleiner ist als die reine Fenstergröße.
- Layout-Pass: FireMonkey berechnet Layouts in Phasen. Wenn man im falschen Moment Parent/Align ändert, entstehen Nebenwirkungen (z. B. mehrfaches Reflow oder flackernde Größen).
Ein Layout-Router adressiert das, indem er (1) das „Wann“ (Resize/Scale/Rotation) entkoppelt vom „Wie“ (Layout-Regeln) und (2) die Regeln an einer Stelle konzentriert. Für technische Leads ist der wichtigste Effekt: Sie bekommen ein klares, überprüfbares Entscheidungszentrum statt vieler lokaler Sonderfälle.
Architektur: Layout-Router mit Slots statt Control-Erzeugung
Der saubere Trick für FMX: nicht dynamisch Controls neu erzeugen, sondern bestehende Controls zwischen Slots umhängen. Ein Slot ist einfach ein Container (z. B. TLayout), der einen Bereich des UI repräsentiert: Sidebar, Toolbar, Content, Footer, Details-Pane.
Vorteile in individueller Unternehmenssoftware:
- Zustände bleiben erhalten (Edit-Text, Scrollposition, selektierte Items), weil Instanzen nicht neu gebaut werden.
- Weniger Risiko für doppelte Verdrahtung von Events, Timern oder Bindings.
- Layout-Regeln werden sichtbar: „welcher Block liegt in welchem Slot“ lässt sich pro Breakpoint nachvollziehen und reviewen.
Wichtig für die Praxis: Schneiden Sie UI-Blöcke grob genug. Wenn Sie 30 Einzelcontrols umhängen, wird die Route-Liste selbst zur Fehlerquelle. Besser sind Container wie layFilterBar, layNavigation, layResultList, layDetails.
Source-Schnipsel: Breakpoint-Router für Responsive Layouts FMX
Der folgende Code ist als Hilfseinheit gedacht, die Sie in FMX-Forms verwenden können. Er berechnet einen Breakpoint (XS/SM/MD/LG/XL) und hängt definierte Controls in definierte Slot-Container um. Wichtige Details:
- Debounce über
TThread.ForceQueue: mehrere Resize-Events werden zu einem Update zusammengefasst (weniger UI-Zittern, weniger Reflow-Schleifen). - Re-Entrancy-Schutz: Layout-Update triggert oft selbst wieder Resize/Layout.
- Optional: Orientierung (Portrait/Landscape) kann in die Breakpoint-Logik einfließen.
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);
// Ein Mapping: welches Control soll in welchen Slot (Container) für einen 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; // manuell neu berechnen
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 dürfen nicht nil sein');
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: nur einmal pro Message-Loop anwenden
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;
// Achtung: Parent-Wechsel verändert Z-Order.
// Wenn Reihenfolge relevant ist, DefineRoute in gewünschter Reihenfolge aufrufen.
for LRoute in LList do
begin
if (LRoute.Control.Parent <> LRoute.TargetSlot) then
LRoute.Control.Parent := LRoute.TargetSlot;
// Align erst nach Parent setzen, sonst werden Bounds ggf. anders interpretiert.
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 bewusst grob, da FMX Zielplattformen stark variieren.
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.
So verwenden Sie den Router in einer Form
Sie definieren Slots als TLayout (z. B. layTop, layLeft, layContent) und registrieren dann pro Breakpoint, wo welche Blöcke liegen. Typisch ist, dass Sidebar und Details-Pane in kleinen Breakpoints untereinander wandern.
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;Einordnung: Warum „Umhängen“ oft stabiler ist als Visible-Schalten
Ein verbreiteter Ansatz ist, für jede Variante separate Layoutbäume vorzuhalten und nur Visible zu toggeln. Das wirkt im Designer bequem, hat aber typische Nebenwirkungen:
- Doppeltes Binding/Events: Zwei ähnliche Controls müssen synchron gehalten werden (z. B. zwei Filterleisten).
- Tab-Reihenfolge und Fokus: Beim Umschalten verliert man Fokus oder landet in unsichtbaren Controls, wenn TabStop/HitTest ungünstig stehen.
- State Drift: Scrollpositionen, Selektionszustände oder editierte Texte divergieren.
Das Umhängen hält die Instanz eindeutig. Wichtig ist, Layout-Blöcke so zu schneiden, dass sie unabhängig verschoben werden können (z. B. „Sidebar“ als eigener Container statt viele Einzelcontrols). Genau das zahlt sich in Wartung und Fehleranalyse aus: Sie debuggen eine Instanz, nicht zwei parallele Schatten-UIs.
Stolperfallen in der Praxis (und wie man sie debuggt)
1) Resize-Stürme und Re-Entrancy
FMX triggert OnResize nicht nur bei User-Resize, sondern auch bei Style-Wechseln, Parent-Änderungen und teils bei DPI-Änderungen. Ohne Debounce hängt die App in Layout-Schleifen. Der Router nutzt TThread.ForceQueue, um die Änderungen in den nächsten UI-Tick zu schieben.
Debugging-Tipp: Logging (z. B. über OutputDebugString) mit Breakpoint, Größe und einem Update-Counter hilft, Reflow-Schleifen zu finden. Wenn Sie zusätzlich den Zeitpunkt loggen, an dem ApplyRoutes startet und endet, sehen Sie schnell, ob ein einzelner Resize „kaskadiert“.
2) Z-Order, HitTest und „unsichtbare“ Klick-Blocker
Parent-Wechsel verändert die Z-Order. Wenn Overlays (z. B. Flyouts) nicht mehr klicken, liegt es oft daran, dass ein Client-aligned Container darüber liegt und HitTest aktiv ist. Variante: Für Overlay-Flächen bewusst einen separaten Slot ganz oben vorsehen und nur dort solche Controls parenten. In FMX ist HitTest (ob ein Control Maus-/Touch-Ereignisse abfängt) häufiger die Ursache als die Sichtbarkeit.
3) TGridPanelLayout und prozentuale Größen
TGridPanelLayout kann bei prozentualen Spalten/Zeilen in Kombination mit Align=Client und dynamischem Umhängen unerwartete Reberechnungen auslösen. Wenn Sie Grid einsetzen müssen, legen Sie den Grid in einen Slot, und hängen Sie nur ganze Grid-Blöcke um, nicht die Grid-Kinder. Das reduziert die Kombinatorik der Layout-Pässe.
4) Fokus, virtuelle Tastatur und „springende“ Eingabefelder
Ein Randfall, der in mobilen FMX-Apps und auch auf Windows-Tablets auftritt: Beim Umhängen kann ein fokussiertes Edit-Control kurzzeitig den Parent verlieren. Das kann die virtuelle Tastatur schließen oder den Cursor zurücksetzen. Praktisch bewährt hat sich: Vor dem Routing den aktuellen Fokus zwischenspeichern (Focused/IFMXFocusControl), nach dem Routing (im selben UI-Tick) den Fokus wiederherstellen. Das lohnt sich vor allem bei Eingabemasken, die zwischen „zweispaltig“ (Tablet/PC) und „einspaltig“ (Phone) wechseln.
Varianten: Breakpoints nach Formfaktor statt nur nach Breite
In realen Multiplattform-Clients ist „Breite“ allein oft nicht das richtige Signal. Sinnvolle Varianten:
- Breite und Höhe: sehr flache Fenster (z. B. Kassen-Terminals, geteilte Bildschirme) brauchen andere Regeln.
- Orientierung:
Landscapeauf Tablets ist oft „desktop-ähnlich“, Portrait eher „mobile-like“. - Safe-Area-Nutzfläche: Auf iOS/Android kann die effektiv nutzbare Höhe durch Systemleisten deutlich schrumpfen. Wer nur
Heightbetrachtet, routet manchmal „zu spät“.
Der Router ist bewusst so gebaut, dass Sie die Breakpoint-Funktion austauschen können. Das ist auch hilfreich in Legacy-Situationen, wenn dieselbe Form in mehreren Hosts läuft (z. B. einmal als normales Fenster, einmal in einem eingebetteten Container).
Ungewöhnlich sauber: Layout-Routing als „Transaktion“
In größeren Screens kippt das Thema weniger an den Breakpoints selbst, sondern an der Reihenfolge der UI-Operationen. Ein praxistaugliches Muster ist, das Routing als Transaktion zu behandeln: erst entscheiden, dann umhängen, dann Nebenwirkungen (Visibility, Fokus, Datenrefresh) geordnet ausführen.
Konkret heißt das: Vermeiden Sie, dass einzelne Controls während des Umhängens eigene Events auslösen, die wiederum Layout oder Datenzugriff starten. In FMX passiert das etwa, wenn beim Parent-Wechsel OnEnter/OnExit feuert oder ein LiveBinding-Ausdruck durch ein Bounds-Update neu evaluiert wird. Wenn Sie solche Effekte sehen, hilft ein zentraler „Updating“-Schalter (wie im Router) plus ein klarer Post-Step: Erst nach ApplyRoutes dürfen teure Dinge laufen (z. B. Liste neu laden, Detailansicht binden).
Gerade bei Clients mit REST-Zugriff ist das relevant: Ein ungewollter Reload während eines Resize kann zu unnötigen Requests führen. Das fällt im LAN nicht auf, aber im VPN oder mobil sofort.
Wann sich der Ansatz lohnt – und wo er Grenzen hat
Der Layout-Router lohnt sich, wenn:
- eine FMX-Anwendung über Jahre weiterlebt und mehrere Entwickler an denselben Screens arbeiten,
- UI-Blöcke klar getrennt werden können (Sidebar/Details/Content),
- Sie reproduzierbare Breakpoint-Regeln brauchen, statt ad-hoc Align-Tuning.
Grenzen sehen Sie, wenn ein Screen stark „fluid“ sein muss (viele dynamische Kacheln, echte Masonry-Layouts). Dann sind TFlowLayout/TGridPanelLayout oder eigene Layout-Klassen geeigneter. Auch wenn sehr viele Einzelcontrols zwischen Slots wechseln, wird die Wartung der Routen unübersichtlich – dann besser größere Blöcke schneiden oder eine deklarative Konfigurationsschicht einziehen (z. B. eine JSON-Konfiguration für Slot-Zuordnungen, die beim Start geladen wird).
Fazit: Für Responsive Layouts FMX ist „Umhängen mit Breakpoints“ ein pragmatischer Mittelweg: weniger Designer-Chaos, klare Regeln, stabile Zustände. Er ersetzt keine durchdachte UI-Struktur, aber er gibt Ihnen ein belastbares Gerüst, um FMX-Clients in digitalen Unternehmenslösungen über Formfaktoren hinweg kontrolliert weiterzuentwickeln.
Wenn Sie in einer bestehenden Delphi- oder FMX-Anwendung eine solche Layout-Architektur sauber nachziehen möchten, ohne dabei UI-Regressionen in Betriebsszenarien zu riskieren, können Sie das gern technisch mit uns einordnen: Projekt oder Modernisierungsvorhaben mit Net-Base besprechen.
Im fachlichen Umfeld spielen auch Delphi Fmx Breakpoints und Firemonkey Layout eine wichtige Rolle, wenn Integrationen, Datenflüsse und Weiterentwicklung sauber zusammenspielen müssen.
Projekt oder Modernisierungsvorhaben mit Net-Base besprechen.
Nächster Schritt
Wenn aus dem Thema ein reales Projekt wird, sollten Architektur, Bestand und Betrieb frueh zusammen betrachtet werden.
Wir unterstuetzen nicht nur bei Einzelfragen, sondern auch dann, wenn aus Source-Schnipseln, Legacy-Themen oder Portalideen ein belastbares Unternehmensprojekt werden soll.
- Bestand, Zielbild und technische Risiken werden zusammen bewertet.
- REST, Datenzugriff, Portale und Rollout werden nicht als Spaetfolgen verschoben.
- Sie sehen frueh, welcher Weg wirtschaftlich und betrieblich tragfähig ist.