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/ClientHeightare 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.
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.
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/HitTestare 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:
Landscapeon 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
Heightsometimes 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.