Net-Base Revista

03.06.2026

Responsive Layouts in Delphi FMX: Breakpoints ohne Designer-Chaos (Layout-Router als Source-Schnipsel)

Responsive Layouts FMX werden in echten Clients schnell fragil: Resize-Stürme, DPI-Wechsel und doppelte Layoutbäume erzeugen Drift. Dieser Refresh zeigt einen Layout-Router mit Breakpoints, Slots und Debounce – inklusive Delphi-Source und Praxisfallen.

03.06.2026

Do tema da revista à prática do projeto

Páginas de serviços e técnicas correspondentes ao artigo

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:

  1. Man baut zwei Layoutbäume im Designer (Desktop vs. Mobile) und schaltet Visible.
  2. Dann kommen LiveBindings, Timer, „Refresh bei Fokus“ – plötzlich existiert Logik doppelt.
  3. 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/ClientWidth sind 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=True kann 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.

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);

  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.

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

Próximo passo

Quando o tema se tornar um projeto real, arquitetura, ambiente existente e operação devem ser considerados em conjunto desde o início.

Não apenas apoiamos questões pontuais, mas também quando fragmentos de código-fonte, temas legados ou ideias de portais precisam evoluir para um projeto empresarial robusto.

  • Estado atual, estado-alvo e riscos técnicos são avaliados em conjunto.
  • REST, o acesso a dados, os portais e o Rollout não são adiados para fases posteriores.
  • Você identifica cedo qual caminho é viável econômica e operacionalmente.

Partilhar publicação

Compartilhar esta publicação diretamente

LinkedIn, X, XING, Facebook, WhatsApp e E‑Mail estão disponíveis imediatamente. Para o Instagram, preparamos diretamente o link e o texto curto.

E-mail

O Instagram abre numa nova aba. O link e o texto curto são copiados previamente para a área de transferência.

Escrever comentário

Deine E-Mail-Adresse wird nicht veröffentlicht. Erforderliche Felder sind mit * markiert