Net-Base Revista

03.06.2026

Layouts responsivos en Delphi FMX: puntos de interrupción sin caos en el diseñador (con Layout-Router como fragmento de código fuente)

Los layouts responsivos en FMX se vuelven rápidamente frágiles en la práctica: picos de redimensionado, cambios de DPI, rotación y «layouts visibles» generan estado duplicado y reflows difíciles de depurar. Este artículo muestra un enrutador de layout con breakpoints que controla bloques de interfaz de usuario en tiempo de ejecución...

03.06.2026

Del tema de la revista a la práctica del proyecto

Páginas de servicios y técnicas relacionadas

Quien en Delphi FireMonkey tiene que atender varios factores de forma, acaba rápidamente en Responsive Layouts FMX – y con la misma rapidez en una mezcla de cascadas de Align, contenedores de diseño ocultos y soluciones de diseñador que fallan en el siguiente cambio de DPI o rotación. En clientes de software empresarial con historia esto es especialmente incómodo: la UI continúa evolucionando, los equipos cambian y de repente la lógica queda atada a detalles visuales.

El núcleo del problema: FireMonkey ofrece muchos bloques constructivos (p. ej. Align, Anchors, TScaledLayout, TFlowLayout, TGridPanelLayout), pero no un sistema de Breakpoints «nativo» como en la web. Es posible reaccionar a cambios de tamaño, pero sin una arquitectura clara eso acaba en «if Width < … then …» repartido por muchos formularios.

Este artículo muestra un Layout-Router: una pequeña componente que gestiona centralmente los Breakpoints y reubica controles (o bloques enteros de layout) entre Slots preconfigurados. Objetivo: los estados se mantienen, el código es mantenible y se atenúan casos límite como rotación, layouts anidados y Re-Entrancy. Además, hay algunos trucos menos obvios que en la práctica marcan la diferencia entre «funciona en la demo» y «funciona de forma estable en producción».

Por qué los Breakpoints en FMX son diferentes que en la web

En los layouts web los Breakpoints suelen ser declarativos (CSS Media Queries). En FMX las decisiones de layout se toman típicamente de forma imperativa en tiempo de ejecución: en el OnResize se realiza el cambio. A eso se suman particularidades por plataforma:

  • Píxeles de dispositivo vs. píxeles lógicos: ClientWidth/ClientHeight están en unidades lógicas (dependientes de la escala). Cambios de DPI (p. ej. Windows Per-Monitor-DPI) pueden volver a activar los layouts sin que «físicamente» cambie nada.
  • Rotación y Safe Areas: Las plataformas móviles proporcionan insets (Notch/Safe Area) – según el OS y el dispositivo. Un «Breakpoint solo por anchura» suele ser insuficiente, porque el área utilizable es menor que el tamaño bruto de la ventana.
  • Paso de layout: FireMonkey calcula los layouts por fases. Si se cambian Parent/Align en el momento equivocado surgen efectos secundarios (p. ej. reflows múltiples o tamaños parpadeantes).

Un Layout-Router aborda esto desacoplando (1) el «cuándo» (Resize/Scale/Rotation) del «cómo» (reglas de layout) y (2) concentrando las reglas en un único lugar. Para los responsables técnicos el efecto más importante es: obtienen un centro de decisiones claro y verificable en lugar de muchos casos especiales locales.

Arquitectura: Layout-Router con Slots en lugar de generar controles

El truco limpio para FMX: no crear controles dinámicamente, sino colgar las instancias existentes entre Slots. Un Slot es simplemente un contenedor (p. ej. TLayout) que representa una zona de la UI: Sidebar, Toolbar, Content, Footer, Details-Pane.

Ventajas en software empresarial a medida:

  • Los estados se mantienen (Edit-Text, posición de scroll, elementos seleccionados), porque las instancias no se reconstruyen.
  • Menor riesgo de cableado duplicado de eventos, temporizadores o bindings.
  • Las reglas de layout quedan visibles: «qué bloque está en qué Slot» puede seguirse y revisarse por Breakpoint.

Importante en la práctica: divida los bloques de UI con suficiente amplitud. Si reasigna 30 controles individuales, la propia lista de rutas se convierte en fuente de errores. Es preferible usar contenedores como layFilterBar, layNavigation, layResultList, layDetails.

Fragmento de código: Router de puntos de interrupción para layouts responsive en FMX

El siguiente código está pensado como una unidad auxiliar que puede usar en formularios FMX. Calcula un breakpoint (XS/SM/MD/LG/XL) y reasigna controles definidos a contenedores de ranura definidos. Detalles importantes:

  • Debounce mediante TThread.ForceQueue: varios eventos de redimensionado se consolidan en una sola actualización (menos parpadeo de la UI, menos ciclos de reflow).
  • Protección contra reentrada: la actualización del diseño suele desencadenar a su vez nuevos redimensionados/actualizaciones del layout.
  • Opcional: orientación (retrato/apaisado) puede incorporarse en la lógica de puntos de interrupción.
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);

  // Ein Mapping: welches Control soll in welchen Slot (Container) für einen Breakpoint.
  // Un mapeo: qué Control debe colocarse en qué slot (contenedor) para un breakpoint.
  TNBRoute = record
    Control: TControl;
    TargetSlot: TControl; // typischerweise TLayout oder TPresentedControl
    // típicamente TLayout o 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; // manuell neu berechnen
    // recalcular manualmente
    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 no pueden ser 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: nur einmal pro Message-Loop anwenden
  // Debounce: aplicar solo una vez por ciclo de mensajes
  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;

    // Achtung: Parent-Wechsel verändert Z-Order.
    // Atención: cambiar el Parent modifica el orden Z.
    // Wenn Reihenfolge relevant ist, DefineRoute in gewünschter Reihenfolge aufrufen.
    // Si el orden es relevante, llamar a DefineRoute en el orden deseado.
    for LRoute in LList do
    begin
      if (LRoute.Control.Parent <> LRoute.TargetSlot) then
        LRoute.Control.Parent := LRoute.TargetSlot;

      // Align erst nach Parent setzen, sonst werden Bounds ggf. anders interpretiert.
      // Establecer Align solo después de asignar el Parent, de lo contrario los Bounds podrían interpretarse de forma distinta.
      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 bewusst grob, da FMX Zielplattformen stark variieren.
  // Breakpoints deliberadamente aproximados, ya que las plataformas objetivo de FMX varían mucho.
  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.

Cómo usar el Router en un formulario

Define slots como TLayout (p. ej. layTop, layLeft, layContent) y registras, para cada breakpoint, dónde deben colocarse los distintos bloques. Es habitual que la barra lateral y el panel de detalles se dispongan uno debajo del otro en breakpoints pequeños.

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;

Contexto: por qué el «reparentado» suele ser más estable que alternar Visible

Un enfoque común es mantener árboles de layout separados para cada variante y simplemente alternar Visible. En el diseñador parece cómodo, pero tiene efectos secundarios típicos:

  • Binding/Eventos duplicados: Dos controles similares deben mantenerse sincronizados (p. ej., dos barras de filtro).
  • Orden de tabulación y foco: Al alternar se pierde el foco o se queda en controles invisibles si TabStop/HitTest están configurados de forma desfavorable.
  • Deriva de estado: Posiciones de scroll, estados de selección o textos editados pueden divergir.

El reparentado mantiene la instancia única. Es importante dividir los bloques de layout de modo que puedan moverse de forma independiente (p. ej., la barra lateral como contenedor propio en lugar de muchos controles individuales). Precisamente eso compensa en mantenimiento y análisis de errores: depuras una instancia, no dos UIs paralelas en sombra.

Problemas habituales en la práctica (y cómo depurarlos)

1) Oleadas de redimensionamientos y reentrada

FMX desencadena OnResize no solo por redimensionado del usuario, sino también por cambios de estilo, cambios de padre y a veces por cambios de DPI. Sin debounce la aplicación puede quedarse en bucles de layout. El Router utiliza TThread.ForceQueue para desplazar los cambios al siguiente tick de la UI.

Consejo de depuración: el logging (p. ej., vía OutputDebugString) con breakpoint, tamaño y un contador de actualizaciones ayuda a localizar bucles de reflow. Si además registras cuándo empieza y termina ApplyRoutes, verás rápidamente si un único resize se está „cascading“.

2) Z-Order, HitTest y bloqueadores de clics „invisibles“

Los cambios de padre modifican la Z-Order. Si overlays (p. ej. flyouts) dejan de recibir clics, a menudo es porque hay un contenedor alineado al cliente encima y HitTest está activo. Variante: reservar conscientemente un slot separado en la capa superior para áreas de overlay y asignar allí esos controles como hijos. En FMX HitTest (si un control intercepta eventos de ratón/táctiles) es con más frecuencia la causa que la visibilidad.

3) TGridPanelLayout y tamaños porcentuales

TGridPanelLayout puede provocar recálculos inesperados cuando se usan columnas/filas porcentuales en combinación con Align=Client y reubicaciones dinámicas. Si debe emplear Grid, coloque el Grid en un Slot y reubique solo bloques completos del Grid, no los hijos del Grid. Esto reduce la combinatoria de los pases de layout.

4) Foco, teclado virtual y campos de entrada que «saltan»

Un caso límite que ocurre en apps móviles FMX y también en tablets Windows: al reubicar un control, un Edit control con foco puede perder temporalmente al Parent. Eso puede cerrar el teclado virtual o restablecer el cursor. Una práctica consolidada: antes del enrutamiento almacenar temporalmente el foco actual (Focused/IFMXFocusControl) y restaurarlo después del enrutamiento (en el mismo tick de UI). Esto merece la pena sobre todo en formularios de entrada que alternan entre „dos columnas“ (tablet/PC) y „una columna“ (phone).

Variantes: breakpoints por factor de forma en lugar de solo por ancho

En clientes multiplataforma reales, el „ancho“ por sí solo a menudo no es la señal adecuada. Variantes útiles:

  • Ancho y altura: ventanas muy planas (p. ej. terminales de caja, pantallas divididas) requieren reglas distintas.
  • Orientación: Landscape en tablets suele ser más „tipo escritorio“, el modo Portrait tiende a ser más „móvil“.
  • Área utilizable (Safe Area): en iOS/Android la altura efectivamente utilizable puede reducirse considerablemente por barras del sistema. Si solo se considera Height, a veces se hace el enrutamiento „demasiado tarde“.

El Router está deliberadamente diseñado para que pueda intercambiar la función de breakpoint. Esto también es útil en situaciones legacy, cuando la misma forma se ejecuta en varios hosts (p. ej. una vez como ventana normal y otra dentro de un contenedor embebido).

Inusualmente limpio: routing de layout como „transacción“

En pantallas grandes el problema no suele ser tanto los breakpoints en sí, sino el orden de las operaciones UI. Un patrón práctico es tratar el enrutamiento como una transacción: primero decidir, luego reubicar, y por último ejecutar los efectos secundarios (Visibility, foco, refresco de datos) de forma ordenada.

Concretamente: evite que controles individuales disparen eventos durante la reubicación que a su vez inicien layout o acceso a datos. En FMX esto ocurre, por ejemplo, cuando al cambiar de Parent se dispara OnEnter/OnExit o cuando una expresión de LiveBinding se reevalúa por una actualización de bounds. Si observa esos efectos, ayuda un interruptor central „Updating“ (como en el Router) junto con un paso posterior claro: solo después de ApplyRoutes deben ejecutarse las operaciones costosas (p. ej. recargar una lista, enlazar una vista detalle).

Esto es especialmente relevante en clientes con REST-acceso: un reload no deseado durante un resize puede generar requests innecesarios. En LAN pasa desapercibido, pero en VPN o en móvil se nota de inmediato.

Cuándo merece la pena el enfoque — y dónde tiene límites

El layout-router merece la pena cuando:

  • una aplicación FMX vive varios años y varios desarrolladores trabajan en las mismas pantallas,
  • los bloques UI pueden separarse claramente (sidebar/detalle/contenido),
  • necesita reglas de breakpoint reproducibles en lugar de ajustes de Align ad-hoc.

Verá límites cuando una pantalla deba ser muy „fluid“ (muchos mosaicos dinámicos, diseños Masonry reales). Entonces son más adecuados TFlowLayout/TGridPanelLayout o clases de layout propias. También, si muchos controles individuales cambian entre slots, el mantenimiento de las rutas se vuelve confuso; entonces es mejor cortar bloques más grandes o introducir una capa de configuración declarativa (p. ej., una configuración JSON para asignaciones de slots que se cargue al inicio).

Conclusión: Para Responsive Layouts en FMX, la „recolocación con Breakpoints“ es un compromiso pragmático: menos caos en el diseñador, reglas claras, estados estables. No reemplaza una estructura de UI bien pensada, pero le proporciona un andamiaje sólido para desarrollar de forma controlada clientes FMX en soluciones empresariales digitales a través de factores de forma.

Si desea reproducir de forma limpia tal arquitectura de layout en una aplicación existente Delphi o FMX, sin poner en riesgo regresiones de UI en escenarios operativos, puede evaluarlo técnicamente con nosotros: discutir el proyecto o la iniciativa de modernización con Net-Base.

En el entorno técnico, Delphi Fmx Breakpoints y Firemonkey Layout también juegan un papel importante cuando integraciones, flujos de datos y evolución deben encajar de forma ordenada.

Discutir proyecto o iniciativa de modernización con Net-Base.

Siguiente paso

Cuando el tema se convierte en un proyecto real, la arquitectura, los sistemas existentes y la operación deben considerarse desde el principio.

No solo apoyamos en consultas puntuales, sino también cuando, a partir de fragmentos de código fuente, temas heredados o ideas de portales, debe consolidarse un proyecto empresarial robusto.

  • La situación actual, el estado objetivo y los riesgos técnicos se evalúan conjuntamente.
  • REST, el acceso a datos, los portales y el rollout no se posponen como consecuencias tardías.
  • Detecta con antelación qué enfoque es viable desde el punto de vista económico y operativo.

Compartir entrada

Compartir esta publicación directamente

LinkedIn, X, XING, Facebook, WhatsApp y correo electrónico están disponibles de inmediato. Para Instagram preparamos el enlace y un texto breve de inmediato.

Correo electrónico

Instagram se abre en una nueva pestaña. El enlace y el texto breve se copian previamente en el portapapeles.