Net-Base Magazine

03.06.2026

Responsive Layouts in Delphi FMX: Breakpoints without Designer Chaos (with Layout-Router as source snippet)

Responsive FMX layouts quickly become fragile in practice: resize storms, DPI changes, rotation and “Visible-Layouts” create duplicated state and hard-to-debug reflows. This article presents a layout router with breakpoints that controls UI blocks at runtime.

03.06.2026

From magazine topic to project implementation

Relevant service and technical pages for this post

Anyone who has to support multiple form factors in Delphi FireMonkey quickly ends up with Responsive Layouts FMX — and just as quickly with a mix of Align cascades, hidden layout containers and designer workarounds that break on the next DPI or rotation change. In mature business software clients this is particularly problematic: the UI continues to evolve, teams change, and suddenly logic is tied to visual details.

The core of the problem: FireMonkey provides many building blocks (e.g. Align, Anchors, TScaledLayout, TFlowLayout, TGridPanelLayout), but no “native” breakpoint system like on the web. You can react to size changes, but without a clear architecture this ends up as “if Width < … then …” scattered across many forms.

This article presents a Layout Router: a small component that centrally manages breakpoints and reassigns controls (or entire layout blocks) between predefined slots. Goal: states are preserved, the code is maintainable, and edge cases such as rotation, nested layouts and re-entrancy are absorbed. There are also a few less obvious tricks that in practice make the difference between “works in the demo” and “runs reliably in production”.

Why breakpoints in FMX are different from the web

In web layouts breakpoints are usually declarative (CSS media queries). In FMX, layout decisions at runtime are typically imperative: changes are made in the OnResize. On top of that there are platform-specific peculiarities:

  • Device pixels vs. logical pixels: ClientWidth/ClientHeight are in logical units (dependent on scaling). DPI changes (e.g. Windows Per-Monitor-DPI) can retrigger layouts without anything ‚physical‘ changing.
  • Rotation and safe areas: Mobile platforms provide insets (notch/safe area) — depending on OS and device. A ‚breakpoint by width only‘ is often too simplistic because the usable area can be smaller than the raw window size.
  • Layout pass: FireMonkey performs layout in phases. If you change Parent/Align at the wrong moment, side effects occur (e.g. multiple reflows or flickering sizes).

A layout router addresses this by (1) decoupling the “when” (resize/scale/rotation) from the “how” (layout rules) and (2) concentrating the rules in one place. For technical leads the most important effect is: they get a clear, verifiable decision center instead of many local special cases.

Architecture: Layout Router with slots instead of control creation

The neat trick for FMX: do not dynamically recreate controls, but reparent existing controls between slots. A slot is simply a container (e.g. TLayout) that represents an area of the UI: sidebar, toolbar, content, footer, details pane.

Advantages in custom enterprise software:

  • States are preserved (edit text, scroll position, selected items), because instances are not rebuilt.
  • Less risk of duplicate wiring of events, timers or bindings.
  • Layout rules become visible: ‚which block is in which slot‘ can be traced and reviewed per breakpoint.

Important in practice: keep UI blocks coarse enough. If you reparent 30 individual controls, the route list itself becomes a source of errors. Use containers such as layFilterBar, layNavigation, layResultList, layDetails.

Source snippet: Breakpoint router for FMX responsive layouts

The following code is intended as a helper unit you can use in FMX forms. It calculates a breakpoint (XS/SM/MD/LG/XL) and reparents defined controls into defined slot containers. Important details:

  • Debounce via TThread.ForceQueue: multiple resize events are consolidated into a single update (less UI jitter, fewer reflow cycles).
  • Re-entrancy protection: a layout update often triggers resize/layout again.
  • Optional: orientation (portrait/landscape) can be incorporated into the breakpoint logic.
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);

  // A mapping: which control should be placed into which slot (container) for a breakpoint.
  TNBRoute = record
    Control: TControl;
    TargetSlot: TControl; // typically TLayout or 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; // manually recompute
    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 must not be nil');

  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: apply only once per message loop
  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;

    // Note: changing Parent alters Z-order.
    // If order matters, call DefineRoute in the desired order.
    for LRoute in LList do
    begin
      if (LRoute.Control.Parent <> LRoute.TargetSlot) then
        LRoute.Control.Parent := LRoute.TargetSlot;

      // Set Align only after Parent, otherwise Bounds may be interpreted differently.
      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 intentionally coarse, as FMX target platforms vary widely.
  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.

Using the router in a form

You define slots as TLayout (e.g. layTop, layLeft, layContent) and then register, per breakpoint, where each block should reside. Typically the sidebar and details pane stack vertically at small breakpoints.

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;

Context: Why reparenting is often more stable than toggling Visible

A common approach is to maintain separate layout trees for each variant and only toggle Visible. That seems convenient in the designer, but has typical side effects:

  • Duplicate bindings/events: Two similar controls must be kept in sync (e.g. two filter bars).
  • Tab order and focus: When switching you can lose focus or end up in invisible controls if TabStop/HitTest are set unfavorably.
  • State drift: Scroll positions, selection states, or edited texts diverge.

Reparenting keeps the instance unique. It’s important to cut layout blocks so they can be moved independently (e.g. „Sidebar“ as its own container instead of many individual controls). That pays off in maintenance and debugging: you debug one instance, not two parallel shadow UIs.

Pitfalls in practice (and how to debug them)

1) Resize storms and re-entrancy

FMX triggers OnResize not only on user resize but also on style changes, parent changes, and sometimes on DPI changes. Without debounce the app can get stuck in layout loops. The router uses TThread.ForceQueue to push changes into the next UI tick.

Debugging tip: Logging (e.g. via OutputDebugString) with a breakpoint, size and an update counter helps find reflow loops. If you also log the time when ApplyRoutes starts and ends, you’ll quickly see whether a single resize cascades.

2) Z-order, HitTest and „invisible“ click blockers

Parent changes alter the Z-order. If overlays (e.g. flyouts) no longer receive clicks, it’s often because a client-aligned container lies on top and HitTest is active. Variant: reserve a dedicated slot at the very top for overlay areas and parent such controls only there. In FMX, HitTest (whether a control intercepts mouse/touch events) is more often the cause than visibility.

3) TGridPanelLayout and percentage sizes

TGridPanelLayout can trigger unexpected recomputations with percentage columns/rows in combination with Align=Client and dynamic reparenting. If you must use a Grid, place the Grid into a slot, and reparent only whole Grid blocks, not the Grid children. That reduces the combinatorics of the layout passes.

4) Focus, virtual keyboard and „jumping“ input fields

A corner case that occurs in mobile FMX apps and also on Windows-tablets: during reparenting a focused Edit-Control can temporarily lose its parent. That can close the virtual keyboard or reset the cursor. A practical approach has proven effective: store the current focus before routing (Focused/IFMXFocusControl) and restore the focus after routing (in the same UI tick). This is especially worthwhile for input forms that switch between „two-column“ (Tablet/PC) and „single-column“ (Phone).

Variants: Breakpoints by form factor instead of width alone

In real-world multiplatform clients, „width“ alone is often not the right signal. Useful variants:

  • Width and height: very shallow windows (e.g. POS terminals, split screens) require different rules.
  • Orientation: Landscape on tablets is often „desktop-like“, portrait rather „mobile-like“.
  • Safe-area usable space: On iOS/Android the effectively usable height can shrink significantly due to system bars. Those who consider only Height sometimes route „too late“.

The router is deliberately designed so you can swap out the breakpoint function. This is also helpful in legacy situations when the same form runs in multiple hosts (e.g. once as a normal window, once inside an embedded container).

Unusually clean: Layout-Routing as a „transaction“

On larger screens the issue is less about the breakpoints themselves and more about the order of UI operations. A practical pattern is to treat routing as a transaction: decide first, then reparent, then execute side effects (visibility, focus, data refresh) in a controlled order.

Concretely this means: avoid having individual controls trigger their own events during reparenting that in turn start layout or data access. In FMX this happens, for example, when OnEnter/OnExit fires on a parent change or when a LiveBinding expression is re-evaluated due to a bounds update. If you observe such effects, a central „Updating“ switch (as in the router) plus a clear post-step helps: only after ApplyRoutes may expensive operations run (e.g. reload a list, bind a detail view).

This is particularly relevant for clients with REST access: an unintended reload during a resize can lead to unnecessary requests. That may go unnoticed on a LAN, but will be apparent immediately over VPN or mobile.

When the approach is worthwhile — and where it has limits

The layout router is worthwhile when:

  • an FMX application lives on for years and multiple developers work on the same screens,
  • UI blocks can be clearly separated (Sidebar/Details/Content),
  • you need reproducible breakpoint rules rather than ad-hoc Align-Tuning.

You encounter limits when a screen must be highly „fluid“ (many dynamic tiles, true Masonry layouts). In those cases TFlowLayout/TGridPanelLayout or custom layout classes are more appropriate. If very many individual controls move between slots, route maintenance becomes hard to manage — in that case it is better to use larger blocks or introduce a declarative configuration layer (for example a JSON configuration for slot assignments that is loaded at startup).

Conclusion: For responsive FMX layouts, „re-parenting with breakpoints“ is a pragmatic middle ground: less designer chaos, clear rules, stable states. It does not replace a well-considered UI structure, but it provides a reliable framework to evolve FMX clients in digital enterprise solutions across form factors in a controlled way.

If you want to implement such a layout architecture cleanly in an existing Delphi- or FMX application without risking UI regressions in operational scenarios, feel free to classify that with us technically: discuss a project or modernization initiative with Net-Base.

In the professional context, Delphi FMX breakpoints and Firemonkey Layout also play an important role when integrations, data flows and further development must work together cleanly.

Discuss a project or modernization initiative with Net-Base.

Next step

When the topic becomes a real project, architecture, the existing system landscape and operations should be considered together early on.

We support not only with individual issues, but also when source snippets, legacy topics, or portal ideas are to be turned into a robust enterprise project.

  • Current state, target state and technical risks are assessed jointly.
  • REST, data access, portals and rollout are not deferred as afterthoughts.
  • You can determine early which path is economically and operationally viable.

Share post

Share this post directly

LinkedIn, X, XING, Facebook, WhatsApp and email are available immediately. For Instagram, we will prepare the link and a short caption immediately.

Email

Instagram opens in a new tab. The link and short text are copied to the clipboard beforehand.