Vom Magazinthema zur Projektpraxis
Passende Leistungs- und Technikseiten zum Beitrag
Die Szene ist vertraut: Ein FMX-Client für eine individuelle Unternehmenssoftware läuft am Entwicklerrechner sauber – und zerlegt sich beim ersten echten Einsatz. Auf dem Surface im Dock passt alles, im Tablet-Modus kippt die Sidebar über die Detailansicht, und auf einem 125%-DPI-Monitor beginnt das UI beim Fensterskalieren zu „zittern“. Spätestens dann kommt die Frage, die niemand gern beantwortet: „Wie bauen wir Responsive Layouts FMX, ohne dass der Designer in fünf Varianten explodiert?“
FireMonkey bringt viele Layout-Bausteine mit (Align/Anchors, TFlowLayout, TGridPanelLayout, TScaledLayout). Was fehlt, ist ein deklaratives Breakpoint-System wie im Web. In der Praxis endet das schnell in verteilten if Width < ...-Blöcken, schwer reproduzierbaren Resize-Kaskaden und parallel gepflegten Layoutbäumen, die sich über die Zeit auseinanderentwickeln.
Dieser Refresh setzt auf ein Muster, das in gewachsenen Clients stabiler ist: Ein Layout-Router, der Breakpoints zentral berechnet und bestehende UI-Blöcke zwischen vorbereiteten Slots umhängt. Das ist kein „UI-Framework im Framework“, sondern eine kleine, testbare Schicht, die genau die Randfälle adressiert, an denen FMX in der Praxis kippt: Resize-Stürme, DPI-Wechsel, Rotation, Fokus-/HitTest-Nebenwirkungen.
Szenarioanalyse: Was genau bricht bei Responsive Layouts FMX?
Konstruiertes, aber realistisches Beispiel: Ein Screen „Aufträge“ hat oben eine Filterleiste, links eine Navigation (Status, Zeiträume), in der Mitte die Ergebnisliste, rechts eine Detailansicht. Auf Desktop soll das dreispaltig sein, auf Tablet zweispaltig (Liste + Details, Navigation als Drawer), auf Phone einspaltig (Navigation → Liste → Details als Stack).
Die typische Evolution im Team sieht so aus:
- Man baut zwei Layoutbäume im Designer (Desktop vs. Mobile) und schaltet
Visible. - Dann kommen LiveBindings, Timer, „Refresh bei Fokus“ – plötzlich existiert Logik doppelt.
- Später folgt „nur kurz“ ein dritter Formfaktor (Split-Screen/kleines Notebook), und das Ganze wird unreviewbar.
Der Kernfehler ist nicht Align oder Anchors. Der Kernfehler ist duplizierter Zustand: zwei Filterleisten, zwei Listen, zwei Selektionszustände. Sobald Zustände auseinanderlaufen, wird Debugging zum Ratespiel.
Warum FMX-Breakpoints nicht wie CSS funktionieren
Im Web sind Breakpoints deklarativ (Media Queries), die Rendering-Pipeline ist darauf ausgelegt. In FMX sind Layout-Entscheidungen meist imperativ und eventgetrieben: OnResize feuert, Code schaltet um. Dazu kommen FMX-spezifische Trigger:
- Logische Einheiten statt Device-Pixel:
Width/ClientWidthsind in „FMX-Punkten“, beeinflusst durch Skalierung. Per-Monitor-DPI auf Windows kann Layout-Entscheidungen verändern, ohne dass sich die physische Fenstergröße „gefühlt“ ändert. - Layout-Pässe und Re-Entrancy: Parent/Align zu ändern, kann weitere Resize/Layout-Events auslösen. Ohne Guard entsteht eine Schleife.
- Z-Order/HitTest als Nebenwirkung: Ein Parent-Wechsel verändert Z-Order; ein unsichtbarer Client-Container mit
HitTest=Truekann Klicks abfangen.
Die Konsequenz: Wenn Sie Breakpoints in FMX sauber wollen, müssen Sie das „Wann“ (Debounce, Guard, Reihenfolge) genauso ernst nehmen wie das „Was“ (welcher Block wohin).
Architektur: Slots + Umhängen statt Visible-Schalten
Die robuste Grundeinheit ist der Slot: ein Container (TLayout, manchmal TPresentedControl), der im Layout eine Rolle hat, z. B. slotTop, slotLeft, slotCenter, slotRight, slotOverlay. UI-Blöcke (Toolbar, Sidebar, Liste, Details) werden je Breakpoint einem Slot zugewiesen und zur Laufzeit umgehängt.
Warum Umhängen in Business-Software-Clients so oft die bessere Wette ist:
- Eine Instanz, ein Zustand: Text in Edits, Scrollpositionen, Auswahl in Listen bleiben erhalten.
- Weniger Verdrahtungsrisiko: Events, Actions, Bindings hängen an genau einem Control-Baum.
- Reviewbarkeit: Breakpoint-Regeln sind an einer Stelle lesbar (Router-Konfiguration) statt über Forms verteilt.
Source-Schnipsel: Layout-Router mit Breakpoints, Debounce und „Transaction“-Apply
Der folgende Router ist absichtlich klein, aber nicht naiv: Er fasst Resize-Events zusammen (Debounce), schützt vor Re-Entrancy und erlaubt eine austauschbare Breakpoint-Logik. Wichtig: Er arbeitet mit dem Umhängen bestehender Instanzen. Genau das verhindert den typischen „State Drift“ aus parallelen Layoutbäumen.
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);
TNBRoute = record
Control: TControl;
TargetSlot: TControl;
Align: TAlignLayout;
end;
TNBRouteList = TList<TNBRoute>;
// Callback, damit Breakpoints nicht „hart“ an Breite gekoppelt sind.
TNBGetBreakpointEvent = reference to function(const AClientSize: TSizeF): TNBLayoutBreakpoint;
// Optional: Hook nach Apply, um Fokus wiederherzustellen oder datengetriebene Aktionen zu entkoppeln.
TNBApplyEvent = reference to procedure(const ABp: TNBLayoutBreakpoint);
TNBLayoutRouter = class(TComponent)
private
FRoot: TControl;
FPending: Boolean;
FUpdating: Boolean;
FCurrent: TNBLayoutBreakpoint;
FOnGetBreakpoint: TNBGetBreakpointEvent;
FOnAfterApply: TNBApplyEvent;
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;
property Current: TNBLayoutBreakpoint read FCurrent;
property OnGetBreakpoint: TNBGetBreakpointEvent read FOnGetBreakpoint write FOnGetBreakpoint;
property OnAfterApply: TNBApplyEvent read FOnAfterApply write FOnAfterApply;
end;
implementation
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: mehrere Resize-Events zu einem Apply bündeln.
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) or (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;
if Assigned(FOnAfterApply) then
FOnAfterApply(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;
// Parent-Wechsel beeinflusst Z-Order.
// Reihenfolge wird durch DefineRoute-Aufrufe festgelegt.
for LRoute in LList do
begin
if LRoute.Control.Parent <> LRoute.TargetSlot then
LRoute.Control.Parent := LRoute.TargetSlot;
// Align nach Parent setzen.
LRoute.Control.Align := LRoute.Align;
LRoute.Control.Visible := True;
end;
finally
FUpdating := False;
end;
end;
function TNBLayoutRouter.DefaultBreakpoint(const AClientSize: TSizeF): TNBLayoutBreakpoint;
var
W, H: Single;
begin
W := AClientSize.cx;
H := AClientSize.cy;
// Kombiniert: sehr geringe Höhe behandelt wie „kleiner“, z. B. Split-Screen oder Kiosk.
if (W < 480) or (H < 360) then Exit(bpXS);
if (W < 768) or (H < 480) then Exit(bpSM);
if W < 1024 then Exit(bpMD);
if W < 1440 then Exit(bpLG);
Result := bpXL;
end;
end.
Verwendung in der Form: Slots definieren, Blöcke routen
Sie brauchen im Designer nur die Slots und die Blöcke. Der Router hängt die Blöcke passend um. Das reduziert Designer-Chaos und hält die Regeln in Delphi-Code reviewbar.
procedure TFrmOrders.FormCreate(Sender: TObject);
var
LPrevFocus: IControl;
begin
FRouter := TNBLayoutRouter.Create(Self);
FRouter.AttachRoot(layRoot); // TLayout Align=Client
// Breakpoint XS: Stack
FRouter.DefineRoute(bpXS, layToolbar, slotTop, TAlignLayout.Top);
FRouter.DefineRoute(bpXS, layNav, slotContent, TAlignLayout.Top);
FRouter.DefineRoute(bpXS, layList, slotContent, TAlignLayout.Client);
FRouter.DefineRoute(bpXS, layDetails, slotContent, TAlignLayout.Bottom);
// Breakpoint MD: 3-Spalten
FRouter.DefineRoute(bpMD, layToolbar, slotTop, TAlignLayout.Top);
FRouter.DefineRoute(bpMD, layNav, slotLeft, TAlignLayout.Client);
FRouter.DefineRoute(bpMD, layList, slotCenter, TAlignLayout.Client);
FRouter.DefineRoute(bpMD, layDetails, slotRight, TAlignLayout.Client);
// Fokus-Falle entschärfen: vor/nach Apply restaurieren.
FRouter.OnAfterApply :=
procedure(const ABp: TNBLayoutBreakpoint)
begin
// Beispiel: Fokus wiederherstellen, wenn FMX ihn beim Parent-Wechsel verliert.
if Assigned(Screen) and Assigned(Screen.Focused) then
LPrevFocus := Screen.Focused
else
LPrevFocus := nil;
if (LPrevFocus <> nil) and (LPrevFocus.GetObject is TFmxObject) then
(LPrevFocus.GetObject as TControl).SetFocus;
end;
// Optional: eigene Logik, wenn Sie Portrait/Landscape härter trennen wollen.
FRouter.OnGetBreakpoint :=
function(const S: TSizeF): TNBLayoutBreakpoint
begin
if (S.cx < 820) or (S.cy < 500) then
Result := bpSM
else
Result := bpMD;
end;
end;Stolperfallen: Wo der Ansatz kippen kann
Resize-Stürme: warum Debounce in FMX nicht optional ist
FMX feuert OnResize nicht nur bei Benutzeraktionen, sondern auch durch Parent-/Align-Änderungen oder Skalierungswechsel. Ohne Debounce routen Sie im schlimmsten Fall mehrfach pro Frame. Der Router bündelt deshalb via TThread.ForceQueue auf „ein Apply pro Message-Loop“. Das ist ein pragmatischer Kompromiss: Sie bekommen Stabilität, ohne Timer-Logik oder künstliche Delays.
Z-Order und HitTest: unsichtbare Klick-Blocker
Parent-Wechsel verändert die Z-Order. Wenn sich nach einem Breakpoint-Wechsel Buttons nicht mehr klicken lassen, ist häufig nicht „Visible falsch“, sondern ein Container darüber fängt Events ab. In FMX ist HitTest die entscheidende Eigenschaft: Ein transparentes Layout mit HitTest=True blockiert Maus/Touch. Planen Sie für Overlays (Drawer, Flyouts) einen eigenen slotOverlay, der bewusst „oben“ liegt, und prüfen Sie HitTest-Defaults nach Style-Wechseln.
TGridPanelLayout und Prozent-Spalten: Reflow-Verstärker
Wenn Sie TGridPanelLayout mit Prozentwerten nutzen, hängen Sie besser ganze Grid-Blöcke um, nicht einzelne Grid-Kinder. Prozent-Reberechnungen + Align-Ketten + Parent-Wechsel sind eine typische Quelle für instabile Zwischenzustände. Eine TGridPanelLayout Alternative ist in manchen Screens tatsächlich ein einfaches Slot-System mit festen Breiten für Sidebar/Details, weil es deterministischer bleibt.
Vergleich: „Visible-Layouts“ vs. Umhängen per Router
| Kriterium | Mehrere Layoutbäume (Visible schalten) | Layout-Router (Controls umhängen) |
|---|---|---|
| Zustand (Edit/Scroll/Selection) | Drift-Risiko, weil Instanzen doppelt | Stabil, weil eine Instanz pro Block |
| Debugging | Fehler nur in „Variante B“ sichtbar | Ein Control-Baum, reproduzierbarer Apply |
| Wartung | Designer komfortabel, später teuer | Code reviewbar, Regeln zentral |
| FMX-Randfälle (Resize/DPI) | Oft Flicker und Timing-Probleme | Debounce/Guard steuerbar |
Einordnung mit Quellen: warum die „UI-Tick“-Bündelung fachlich sinnvoll ist
Das Pattern „teure UI-Updates bündeln und kontrolliert ausführen“ ist nicht nur bei Layout relevant, sondern auch bei allem, was auf UI-Events stürmisch reagiert (Requests, Logs, Bindings). Wer in einem Client neben Layout auch Netzwerk- oder Debug-Telemetrie sauber führen will, profitiert von der gleichen Disziplin: klare Trigger, klare Gateways, keine ungebremste Ereignisflut. In diesem Sinne passt das Debounce/Guard-Prinzip gut zu robusten Betriebsansätzen wie sie auch in Net-Base-Schnipseln zu Overload-Gates beschrieben werden (Parallelität kontrollieren statt „alles gleichzeitig“) – siehe den Beitrag zum Concurrency-Gate im Delphi-REST-Server: High Performance REST Server in Delphi: Concurrency-Gate, 429/503 und sauberes Overload-Verhalten.
Und wenn Sie im FMX-Client zusätzlich eine eingebettete Web-Oberfläche (z. B. für Auth, Dokumentenviewer oder Portalelemente) verwenden, ist Lifecycle-Disziplin genauso entscheidend: Initialisierung, Dispose, Event-Unhooking. Das ist konzeptionell verwandt mit „Parent-Wechsel sauber abschirmen“. Ein praxisnaher Referenzpunkt ist hier: Delphi WebView2 in FMX: Entscheidungspfad für Lifecycle, Bridge, Downloads und stabile Dispose-Logik.
Genau eine redaktionelle Einschätzung
Aus Redaktionssicht ist der Layout-Router kein „Nice-to-have“, sondern eine sinnvolle Investition, sobald ein Screen mehr als zwei Formfaktoren sauber bedienen muss: Er zwingt zu einer expliziten Layout-Entscheidung und macht Zustandsfragen sichtbar. Der Preis ist, dass Sie UI-Blöcke bewusst schneiden und Slot-Strukturen sauber halten müssen – wer das nicht tut, baut sich lediglich eine neue Schicht Chaos.
Fazit: Wann sich Responsive Layouts FMX per Router lohnen – und Einsatzgrenzen
Der Router lohnt sich besonders, wenn Sie eine FMX-Anwendung langfristig modernisieren, mehrere Geräteklassen bedienen und dabei Zustände stabil halten müssen (Eingaben, Selektionen, Scrollpositionen). Er ist auch in Legacy-Situationen hilfreich, weil er Breakpoint-Regeln zentralisiert und das Risiko verteilt eingebauter Sonderfälle reduziert.
Grenzen: Wenn Sie ein sehr „fluides“ Layout benötigen (Kachelwände, Masonry, dynamische Karten), sind spezialisierte Layout-Klassen oder ein eigenes Layouting sinnvoller als Slot-Routing. Und wenn ein Screen aus sehr vielen Einzelcontrols besteht, wird die Route-Liste unübersichtlich: Dann erst refaktorieren (gröbere Blöcke), dann routen.
Wenn Sie in einer bestehenden Delphi/FMXLösung das Layout über mehrere Formfaktoren stabil bekommen müssen, ohne dabei Betriebsprobleme (Fokus, HitTest, Resize-Stürme) in den Alltag zu tragen, lässt sich das in einem technischen Review oft schnell eingrenzen: Kontakt aufnehmen.
Für dieses Thema sind auch Delphi Fmx Breakpoints und Firemonkey Layout wichtig. Der Beitrag ordnet diese Aspekte verständlich ein und zeigt, worauf es im Alltag ankommt.
Projekt oder Modernisierungsvorhaben mit Net-Base besprechen.
nächster Schritt
Wenn aus dem Thema ein reales Projekt wird, sollten Architektur, Bestand und Betrieb früh 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 Spätfolgen verschoben.
- Sie sehen früh, welcher Weg wirtschaftlich und betrieblich tragfähig ist.