Net-Base Magazine

03.06.2026

Mises en page réactives dans Delphi FMX : points de rupture sans chaos dans le Designer (avec Layout-Router en extrait de code)

Les mises en page responsives FMX deviennent rapidement fragiles en pratique : rafales de redimensionnements, changements de DPI, rotation et «Visible-Layouts» créent un état dupliqué et des reflows difficiles à déboguer. Cet article présente un routeur de mise en page avec breakpoints qui contrôle des blocs UI à l'exécution...

03.06.2026

Du thème du magazine à la pratique des projets

Pages de services et techniques pertinentes pour l'article

Lorsqu’on doit prendre en charge plusieurs facteurs de forme dans Delphi FireMonkey, on en arrive vite à Responsive Layouts FMX — et tout aussi vite à un mélange de cascades d’Align, de conteneurs de layout cachés et de contournements côté designer qui se dérobent au prochain changement de DPI ou de rotation. Dans des clients de logiciel métier existants, cela est particulièrement problématique : l’UI évolue, les équipes changent, et soudain la logique dépend de détails visuels.

Le cœur du problème : FireMonkey propose de nombreux éléments (p. ex. Align, Anchors, TScaledLayout, TFlowLayout, TGridPanelLayout), mais pas de système de breakpoints « natif » comparable au Web. On peut bien réagir aux changements de taille, mais sans une architecture claire cela se termine par des « if Width < … then … » disséminés sur de nombreux Forms.

Cet article présente un Layout-Router : une petite composante qui gère les breakpoints de manière centralisée et réaffecte les Controls (ou des blocs de layout entiers) entre des slots préparés. Objectif : les états sont préservés, le code reste maintenable, et les cas limites tels que la rotation, les layouts imbriqués et la réentrance sont atténués. S’y ajoutent quelques astuces moins évidentes qui, en pratique, font la différence entre « marche en démo » et « marche de façon stable en production ».

Pourquoi les breakpoints en FMX diffèrent-ils de ceux du Web

Dans les layouts Web, les breakpoints sont généralement déclaratifs (CSS Media Queries). Dans FMX, les décisions de mise en page se prennent typiquement à l’exécution de manière impérative : on bascule dans le OnResize. S’y ajoutent des spécificités liées aux plateformes :

  • Pixels physiques vs. pixels logiques : ClientWidth/ClientHeight sont en unités logiques (dépendant de l’échelle). Les changements de DPI (p. ex. Windows Per-Monitor-DPI) peuvent déclencher un recalcul du layout sans qu’il n’y ait de changement « physique ».
  • Rotation et zones sûres (Safe Areas) : Les plateformes mobiles fournissent des insets (notch/safe area) — selon l’OS et l’appareil. Un « breakpoint seulement basé sur la largeur » est souvent insuffisant, car la surface utilisable peut être plus petite que la taille brute de la fenêtre.
  • Layout-Pass : FireMonkey calcule les mises en page en phases. Si l’on modifie Parent/Align au mauvais moment, des effets secondaires apparaissent (p. ex. reflows multiples ou tailles clignotantes).

Un Layout-Router répond à cela en séparant (1) le « quand » (Resize/Scale/Rotation) du « comment » (règles de layout) et en (2) centralisant les règles. Pour les responsables techniques, l’effet principal est le suivant : ils obtiennent un centre de décision clair et vérifiable au lieu de nombreux cas locaux spéciaux.

Architecture: Layout-Router mit Slots statt Control-Erzeugung

L’astuce propre pour FMX : ne pas recréer dynamiquement des Controls, mais rattacher les Controls existants entre des Slots. Un Slot est simplement un conteneur (p. ex. TLayout) qui représente une zone de l’UI : barre latérale, barre d’outils, contenu, pied de page, panneau de détails.

Avantages pour les logiciels d’entreprise sur mesure :

  • Les états sont préservés (champ d’édition, position de défilement, éléments sélectionnés), car les instances ne sont pas reconstruites.
  • Risque réduit de double câblage des événements, timers ou bindings.
  • Les règles de layout deviennent visibles : « quel bloc se trouve dans quel Slot » peut être suivi et vérifié pour chaque breakpoint.

Important pour la pratique : segmentez les blocs UI de manière suffisamment grossière. Si vous réaffectez 30 contrôles individuels, la liste des routes devient elle‑même une source d’erreurs. Mieux vaut utiliser des conteneurs tels que layFilterBar, layNavigation, layResultList, layDetails.

Extrait de code : Breakpoint-Router pour mises en page responsives FMX

Le code suivant est conçu comme un composant utilitaire que vous pouvez utiliser dans des FMX-Forms. Il calcule un breakpoint (XS/SM/MD/LG/XL) et réaffecte des contrôles définis dans des conteneurs de type slot définis. Détails importants :

  • Debounce via TThread.ForceQueue : plusieurs événements de redimensionnement sont groupés en une seule mise à jour (moins de scintillement de l’interface, moins de boucles de reflow).
  • Protection contre la réentrance : la mise à jour du layout déclenche souvent à son tour des Resize/Layout.
  • Optionnel : orientation (portrait/paysage) peut entrer dans la logique des breakpoints.

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

// Un mapping : quel contrôle doit être placé dans quel slot (conteneur) pour un breakpoint.
TNBRoute = record
Control: TControl;
TargetSlot: TControl; // typiquement TLayout ou TPresentedControl
Align: TAlignLayout;
end;

TNBRouteList = TList;

TNBGetBreakpointEvent = reference to function(const AClientSize: TSizeF): TNBLayoutBreakpoint;

TNBLayoutRouter = class(TComponent)
private
FRoot: TControl;
FPending: Boolean;
FUpdating: Boolean;
FCurrent: TNBLayoutBreakpoint;
FOnGetBreakpoint: TNBGetBreakpointEvent;
FRoutes: TObjectDictionary;
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; // recalculer manuellement
property Current: TNBLayoutBreakpoint read FCurrent;
property OnGetBreakpoint: TNBGetBreakpointEvent read FOnGetBreakpoint write FOnGetBreakpoint;
end;

implementation

{ TNBLayoutRouter }

constructor TNBLayoutRouter.Create(AOwner: TComponent);
begin
inherited;
FRoutes := TObjectDictionary.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 ne doivent pas être 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 : appliquer une seule fois par boucle de messages
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;

// Attention : un changement de Parent modifie l’ordre Z.
// Si l’ordre est important, appeler DefineRoute dans l’ordre souhaité.
for LRoute in LList do
begin
if (LRoute.Control.Parent <> LRoute.TargetSlot) then
LRoute.Control.Parent := LRoute.TargetSlot;

// Définir Align seulement après avoir changé le Parent, sinon les Bounds peuvent être interprétés différemment.
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 volontairement approximatifs, car les plateformes cibles FMX varient fortement.
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.

Comment utiliser le routeur dans un formulaire

Vous définissez des slots en tant que TLayout (p. ex. layTop, layLeft, layContent) puis vous enregistrez, pour chaque breakpoint, l’emplacement des blocs. Il est courant que la Sidebar et le panneau de détails se retrouvent l’un sous l’autre sur les petits 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;

Contexte : pourquoi le « reparenting » est souvent plus stable que d’activer/désactiver la propriété Visible

Une approche répandue consiste à maintenir pour chaque variante des arbres de layout séparés et à simplement basculer la propriété Visible. Cela paraît pratique dans le designer mais présente des effets secondaires typiques :

  • Bindings/événements dupliqués : Deux contrôles similaires doivent être maintenus synchronisés (p. ex. deux barres de filtre).
  • Ordre de tabulation et focus : Lors du basculement, on perd le focus ou l’on aboutit sur des contrôles invisibles si TabStop/HitTest sont mal configurés.
  • Dérive d’état : Les positions de défilement, les états de sélection ou les textes en cours d’édition divergent.

Le reparenting conserve une instance unique. Il est important de découper les blocs de layout de manière à pouvoir les déplacer indépendamment (p. ex. « Sidebar » comme conteneur autonome plutôt que de nombreux contrôles individuels). C’est exactement ce qui rapporte en maintenance et en analyse de bugs : vous debuggez une instance, pas deux UI parallèles en double.

Pièges courants en pratique (et comment les déboguer)

1) Rafales de redimensionnement et ré-entrance

FMX déclenche OnResize non seulement lors d’un redimensionnement par l’utilisateur, mais aussi lors de changements de style, de modifications de parent et parfois de changements de DPI. Sans mécanisme de debounce, l’application peut rester bloquée dans des boucles de layout. Le routeur utilise TThread.ForceQueue pour repousser les modifications au tick UI suivant.

Astuce de debug : du logging (p. ex. via OutputDebugString) avec point d’arrêt, taille et un compteur de mises à jour aide à repérer les boucles de reflow. Si vous enregistrez aussi le moment où ApplyRoutes démarre et se termine, vous verrez rapidement si un seul redimensionnement se propage en cascade.

2) Z-Order, HitTest et bloqueurs de clics « invisibles »

Un changement de parent modifie l’ordre Z. Si des overlays (p. ex. des flyouts) ne reçoivent plus de clics, c’est souvent parce qu’un conteneur aligné client se trouve au-dessus et que HitTest est actif. Variante : prévoir volontairement un slot séparé tout en haut pour les zones d’overlay et n’y parenter ces contrôles qu’à cet endroit. Dans FMX, HitTest (le fait qu’un contrôle intercepte les événements souris/tactile) est plus fréquemment la cause que la visibilité.

3) TGridPanelLayout et tailles en pourcentage

TGridPanelLayout peut, en présence de colonnes/lignes en pourcentage combinées avec Align=Client et une réaffectation dynamique, déclencher des recalculs inattendus. Si vous devez utiliser un Grid, placez le Grid dans un slot et réaffectez uniquement des blocs entiers du Grid, pas les enfants du Grid. Cela réduit la combinatoire des passes de layout.

4) Fokus, virtuelle Tastatur und „springende“ Eingabefelder

Un cas limite présent dans les applications FMX mobiles et aussi sur Windows-tablettes : lors de la réaffectation, un Edit contrôlé et focalisé peut temporairement perdre son parent. Cela peut fermer le clavier virtuel ou réinitialiser le curseur. Une pratique éprouvée : sauvegarder le focus courant avant le routage (Focused/IFMXFocusControl) et le restaurer après le routage (dans le même tick UI). Cela vaut surtout pour les formulaires de saisie qui basculent entre « deux colonnes » (tablette/PC) et « une colonne » (téléphone).

Varianten: Breakpoints nach Formfaktor statt nur nach Breite

Dans des clients multiplateformes réels, la « largeur » seule n’est souvent pas le bon signal. Variantes pertinentes :

  • Largeur et hauteur : des fenêtres très plates (par ex. terminaux de caisse, écrans partagés) nécessitent d’autres règles.
  • Orientation : Landscape sur tablette est souvent « semblable au desktop », le Portrait plutôt « orienté mobile ».
  • Surface utile de la safe area : sur iOS/Android, la hauteur réellement utilisable peut fortement diminuer à cause des barres système. Qui ne regarde que la Height routage parfois « trop tard ».

Le routeur est délibérément conçu pour que vous puissiez remplacer la fonction de breakpoint. C’est utile aussi dans des situations legacy, lorsque la même Form tourne dans plusieurs hôtes (p. ex. une fois comme fenêtre normale, une fois dans un conteneur embarqué).

Ungewöhnlich sauber: Layout-Routing als „Transaktion“

Dans les écrans de taille importante, le problème vient moins des breakpoints que de l’ordre des opérations UI. Un pattern opérationnel est de traiter le routage comme une transaction : d’abord décider, ensuite réaffecter, puis exécuter de manière ordonnée les effets secondaires (Visibility, focus, rafraîchissement des données).

Concrètement : évitez que des contrôles individuels déclenchent, pendant la réaffectation, des événements qui relancent à leur tour du layout ou des accès aux données. En FMX, cela se produit par exemple si, lors du changement de parent, OnEnter/OnExit se déclenche ou si une expression LiveBinding est réévaluée suite à une mise à jour des bounds. Si vous observez de tels effets, un interrupteur central «Updating» (comme dans le routeur) associé à une étape de post-traitement claire aide : seules les opérations coûteuses peuvent s’exécuter après ApplyRoutes (p. ex. recharger une liste, lier une vue de détail).

Cela est particulièrement pertinent pour des clients avec accès REST : un reload involontaire lors d’un redimensionnement peut générer des requêtes inutiles. Cela passe inaperçu sur un LAN, mais se voit immédiatement en VPN ou en mobilité.

Wann sich der Ansatz lohnt – und wo er Grenzen hat

Le layout-router est pertinent si :

  • une application FMX doit vivre plusieurs années et que plusieurs développeurs travaillent sur les mêmes écrans,
  • les blocs UI peuvent être clairement séparés (sidebar / détails / contenu),
  • vous avez besoin de règles de breakpoint reproductibles plutôt que d’ajustements d’alignement ad hoc.

Vous atteignez des limites lorsque l’écran doit être très „fluid“ (beaucoup de tuiles dynamiques, véritables mises en page Masonry). Dans ce cas, TFlowLayout/TGridPanelLayout ou des classes de layout personnalisées sont plus appropriés. De même, si de nombreux contrôles individuels changent de slot, la maintenance des routes devient difficile à suivre – mieux vaut découper des blocs plus grands ou introduire une couche de configuration déclarative (par ex. une configuration JSON pour les affectations de slots, chargée au démarrage).

Conclusion : Pour les layouts responsive sous FMX, le recours au repositionnement via breakpoints est un compromis pragmatique : moins de chaos côté designer, règles claires, états stables. Ce n’est pas un substitut à une structure d’interface réfléchie, mais il vous fournit une ossature fiable pour faire évoluer de manière contrôlée les clients FMX des solutions d’entreprise numériques à travers les différents facteurs de forme.

Si vous souhaitez reproduire proprement une telle architecture de layout dans une application existante Delphi ou FMX, sans risquer des régressions de l’interface en exploitation, vous pouvez l’examiner techniquement avec nous : discuter du projet ou du programme de modernisation avec Net-Base.

Dans le contexte métier, Delphi Fmx Breakpoints et Firemonkey Layout jouent également un rôle important lorsque les intégrations, les flux de données et l’évolution doivent s’articuler proprement.

Discuter du projet ou du programme de modernisation avec Net-Base.

Étape suivante

Lorsque ce sujet devient un projet concret, l'architecture, l'existant et l'exploitation doivent être examinés ensemble dès le départ.

Nous n'intervenons pas seulement sur des questions ponctuelles, mais aussi lorsque des fragments de code source, des problématiques liées aux systèmes legacy ou des concepts de portail doivent se transformer en un projet d'entreprise robuste.

  • L'état des lieux, l'état cible et les risques techniques sont évalués conjointement.
  • REST, l'accès aux données, les portails et le déploiement ne sont pas repoussés en tant que conséquences ultérieures.
  • Vous identifiez tôt quelle voie est viable sur le plan économique et opérationnel.

Partager l'article

Partager directement cette publication

LinkedIn, X, XING, Facebook, WhatsApp et e-mail sont immédiatement disponibles. Pour Instagram, nous préparons directement le lien et un court texte.

Courriel

Instagram s'ouvre dans un nouvel onglet. Le lien et le court texte sont préalablement copiés dans le presse-papiers.