Do tema da revista à prática do projeto
Páginas de serviços e técnicas correspondentes ao artigo
Quem em Delphi FireMonkey precisa lidar com vários fatores de forma, rapidamente recorre a Responsive Layouts FMX – e igualmente rápido a uma mistura de cascatas de Align, contêineres de layout ocultos e soluções alternativas do designer, que desmoronam na próxima mudança de DPI ou rotação. Em clientes de software empresarial já consolidados isso é particularmente incômodo: a UI continua a evoluir, equipes mudam, e de repente a lógica fica presa a detalhes visuais.
O cerne do problema: FireMonkey fornece muitos blocos construtivos (por ex. Align, Anchors, TScaledLayout, TFlowLayout, TGridPanelLayout), mas não um sistema de Breakpoint „nativo“ como na Web. É possível reagir a mudanças de tamanho, mas sem uma arquitetura clara isso termina em „if Width < … then …“ espalhado por muitos Forms.
Este artigo apresenta um Layout-Router: um pequeno componente que gerencia Breakpoints centralmente e reencaixa Controls (ou blocos inteiros de layout) entre Slots preparados. Objetivo: estados permanecem, o código é mantível, e casos-limite como rotação, layouts aninhados e re-entrância são amortecidos. Além disso, há alguns truques menos óbvios que na prática fazem a diferença entre „funciona na demo“ e „funciona estável em produção“.
Por que Breakpoints em FMX são diferentes da Web
Em layouts Web, Breakpoints são geralmente declarativos (CSS Media Queries). Em FMX, decisões de layout em tempo de execução são tipicamente imperativas: no OnResize realiza-se a troca. A isso somam-se particularidades específicas da plataforma:
- Device-Pixel vs. pixels lógicos:
ClientWidth/ClientHeightestão em unidades lógicas (dependendo da escala). Mudanças de DPI (por ex. Windows Per-Monitor-DPI) podem forçar um novo cálculo de layout, sem que algo „fisicamente“ mude. - Rotação e Safe Areas: Plataformas móveis fornecem insets (Notch/Safe Area) – dependendo do SO e do dispositivo. Um „Breakpoint apenas por largura“ frequentemente é insuficiente, porque a área utilizável é menor que o tamanho bruto da janela.
- Layout-Pass: FireMonkey calcula layouts em fases. Se se altera Parent/Align no momento errado, surgem efeitos colaterais (por ex. reflows múltiplos ou tamanhos que piscam).
Um Layout-Router endereça isso ao (1) desacoplar o „quando“ (Resize/Scale/Rotation) do „como“ (regras de layout) e (2) concentrar as regras num único ponto. Para líderes técnicos, o efeito mais importante é: obtêm-se um centro de decisões claro e verificável em vez de muitos casos especiais locais.
Arquitetura: Layout-Router com Slots em vez de criação de Controls
O truque elegante para FMX: não recriar dinamicamente Controls, mas reencaixar Controls existentes entre Slots. Um Slot é simplesmente um contêiner (por ex. TLayout) que representa uma área da UI: Sidebar, Toolbar, Content, Footer, Details-Pane.
Vantagens em software empresarial personalizado:
- Estados permanecem (Edit-Text, posição de rolagem, itens selecionados), porque instâncias não são reconstruídas.
- Menor risco de dupla ligação de eventos, timers ou bindings.
- Regras de layout ficam visíveis: „qual bloco está em qual Slot“ pode ser rastreado e revisado por Breakpoint.
Importante na prática: mantenha os blocos de UI suficientemente grandes. Se você mover 30 controles individuais, a própria lista de rotas se torna fonte de erro. É melhor usar contêineres como layFilterBar, layNavigation, layResultList, layDetails.
Trecho de código-fonte: Breakpoint-Router para layouts responsivos FMX
O código a seguir é pensado como uma unidade auxiliar que você pode usar em FMX-Forms. Ele calcula um breakpoint (XS/SM/MD/LG/XL) e realoca controles definidos para contêineres de slot definidos. Detalhes importantes:
- Debounce via
TThread.ForceQueue: vários eventos de redimensionamento são agrupados em uma única atualização (menos tremores na UI, menos ciclos de reflow). - Proteção contra reentrância: a atualização do layout frequentemente aciona novamente redimensionamento/layout.
- Opcional: orientação (Retrato/Paisagem) pode ser incorporada na lógica de breakpoint.
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);
// Um mapeamento: qual Control deve estar em qual Slot (Container) para um Breakpoint.
TNBRoute = record
Control: TControl;
TargetSlot: TControl; // tipicamente TLayout ou 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; // 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 não podem 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: aplicar apenas uma vez por loop de mensagens
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;
// Atenção: mudança de Parent altera a ordem Z.
// Se a ordem for relevante, chame DefineRoute na ordem desejada.
for LRoute in LList do
begin
if (LRoute.Control.Parent <> LRoute.TargetSlot) then
LRoute.Control.Parent := LRoute.TargetSlot;
// Defina Align apenas depois de ajustar o Parent, caso contrário os Bounds podem ser interpretados de forma diferente.
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 propositalmente aproximados, pois plataformas alvo do FMX variam consideravelmente.
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.
Como usar o Router em um formulário
Você define slots como TLayout (p. ex. layTop, layLeft, layContent) e registra então, por breakpoint, onde cada bloco deve ficar. É típico que a Sidebar e o painel de detalhes se posicionem um abaixo do outro em breakpoints pequenos.
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;Classificação: Por que “reparenting” costuma ser mais estável do que alternar Visible
Uma abordagem comum é manter árvores de layout separadas para cada variante e apenas alternar Visible. Isso parece conveniente no designer, mas traz efeitos colaterais típicos:
- Bindings/Eventos duplicados: Dois controles semelhantes precisam ser mantidos em sincronia (p. ex. duas barras de filtro).
- Ordem de Tabulação e foco: Ao alternar perde-se o foco ou acaba-se em controles invisíveis, se TabStop/HitTest estiverem configurados inadequadamente.
- Divergência de estado: Posições de scroll, estados de seleção ou textos editados divergem.
O reparenting mantém a instância única. É importante cortar os blocos de layout de forma que possam ser movidos independentemente (p. ex. tratar a “Sidebar” como um container próprio em vez de muitos controles soltos). Isso compensa em manutenção e análise de erros: você depura uma instância, não duas UIs-sombra paralelas.
Armadilhas na prática (e como depurá-las)
1) Tempestades de redimensionamento e reentrância
FMX dispara OnResize não apenas em redimensionamento pelo usuário, mas também em mudanças de estilo, alterações de Parent e em alguns casos durante mudanças de DPI. Sem debounce a aplicação prende-se em loops de layout. O Router usa TThread.ForceQueue para adiar as mudanças para o próximo tick da UI.
Dica de depuração: logging (p. ex. via OutputDebugString) com breakpoint, tamanho e um contador de updates ajuda a localizar loops de reflow. Se também registrar o momento em que ApplyRoutes inicia e termina, você vê rapidamente se um único resize está causando um efeito em cascata.
2) Z-Order, HitTest e bloqueadores de clique “invisíveis”
Mudanças de Parent alteram a Z-Order. Se overlays (p. ex. Flyouts) deixam de receber cliques, frequentemente é porque um container alinhado como Client está por cima e o HitTest está ativo. Variante: reserve intencionalmente um slot separado no topo para áreas de overlay e parent apenas nesses slots esses controles. Em FMX o HitTest (se um control intercepta eventos de mouse/touch) costuma ser a causa mais comum, mais ainda do que a visibilidade.
3) TGridPanelLayout e tamanhos percentuais
TGridPanelLayout pode, em colunas/linhas percentuais combinadas com Align=Client e remanejamento dinâmico, provocar recálculos inesperados. Se precisar usar Grid, coloque o Grid num slot e remova/recoloque apenas blocos inteiros do Grid, não os filhos do Grid. Isso reduz a combinatória das passagens de layout.
4) Foco, teclado virtual e campos de entrada que “saltam”
Um caso limite que ocorre em apps FMX móveis e também em tablets Windows: ao reparentar, um Edit-control com foco pode perder temporariamente o Parent. Isso pode fechar o teclado virtual ou redefinir o cursor. Na prática tem funcionado bem: antes do roteamento armazenar o foco atual (Focused/IFMXFocusControl) e, após o roteamento (no mesmo UI-tick), restaurar o foco. Vale a pena sobretudo em formulários de entrada que alternam entre “duas colunas” (Tablet/PC) e “uma coluna” (Phone).
Variantes: Breakpoints por fator de forma em vez de apenas por largura
Em clientes multiplataforma reais, a “largura” sozinha muitas vezes não é o sinal correto. Variantes sensatas:
- Largura e altura: janelas muito rasas (ex.: terminais de caixa, ecrãs partilhados) exigem regras diferentes.
- Orientação:
Landscapeem tablets costuma ser parecido com desktop; o Portrait tende a ser mais orientado a mobile. - Área útil (Safe-Area): no iOS/Android a altura efetivamente utilizável pode encolher significativamente por causa das barras do sistema. Quem considera apenas
Heightàs vezes faz o roteamento “tarde demais”.
O Router foi deliberadamente concebido de forma a permitir que você troque a função de Breakpoint. Isso é útil também em cenários legacy, quando o mesmo formulário é executado em vários hosts (por exemplo, uma vez como janela normal, outra vez dentro de um contêiner embutido).
Incomumente limpo: Layout-Routing como “transação”
Em telas maiores o problema tende a não ser os próprios breakpoints, mas a ordem das operações de UI. Um padrão prático é tratar o roteamento como uma transação: primeiro decidir, depois reparentar, depois executar efeitos colaterais (Visibility, foco, refresh de dados) de forma ordenada.
Concretamente: evite que controles individuais disparem eventos durante o reparenting que, por sua vez, iniciem layout ou acesso a dados. Em FMX isso acontece, por exemplo, quando a troca de Parent dispara OnEnter/OnExit ou quando uma expressão de LiveBinding é reavaliada por uma atualização de Bounds. Se você observar esses efeitos, ajuda ter um interruptor central de “Updating” (como no Router) e um passo claro posterior: só após ApplyRoutes devem ocorrer operações custosas (ex.: recarregar a lista, vincular a vista de detalhe).
Isso é particularmente relevante em clientes com acesso REST: um reload indesejado durante um resize pode gerar requests desnecessários. No LAN isso passa despercebido, mas em VPN ou em redes móveis aparece imediatamente.
Quando a abordagem vale a pena — e onde tem limites
O Layout-Router compensa quando:
- uma aplicação FMX tem ciclo de vida de anos e vários desenvolvedores trabalham nas mesmas telas,
- blocos de UI podem ser claramente separados (barra lateral/detalhe/conteúdo),
- você precisa de regras de breakpoint reproduzíveis, em vez de ajustes ad-hoc de Align.
Limites aparecem quando uma tela precisa ser muito “fluida” (muitos blocos dinâmicos, layouts Masonry reais). Nesse caso, TFlowLayout/TGridPanelLayout ou classes de layout próprias são mais adequadas. Também quando muitos controles individuais trocam entre slots, a manutenção das rotas torna-se confusa — então é melhor cortar blocos maiores ou introduzir uma camada de configuração declarativa (por exemplo, uma configuração JSON para atribuições de slot, carregada na inicialização).
Conclusão: Para layouts responsivos em FMX, “reconfigurar com Breakpoints” é um meio-termo pragmático: menos caos no designer, regras claras, estados estáveis. Não substitui uma estrutura de UI bem pensada, mas fornece uma estrutura robusta para desenvolver de forma controlada clientes FMX em soluções empresariais digitais através dos fatores de forma.
Se desejar reproduzir uma arquitetura de layout assim em uma aplicação Delphi existente ou FMX, sem correr o risco de regressões de UI em cenários de operação, podemos avaliar isso tecnicamente com você: discutir projeto ou iniciativa de modernização com Net-Base.
No contexto técnico, Delphi Fmx Breakpoints e Firemonkey Layout também desempenham um papel importante quando integrações, fluxos de dados e evolução devem funcionar em conjunto de forma coerente.
Discutir projeto ou iniciativa de modernização com Net-Base.
Próximo passo
Quando um tema se torna um projeto real, arquitetura, sistemas existentes 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 uma fase posterior.
- Você vê cedo qual caminho é economicamente e operacionalmente viável.