Artigo Técnico

Células Unidas e Layout de Modelos de Relatórios em Delphi com o HotXLS

Ao percorrer as células de um modelo de relatório acabado de abrir, um título unido comporta-se de forma intrigante: se ler A1, obtém "Quarterly Statement"; contudo, se ler de B1 a F1 (que visualmente se situam sob a mesma barra), não obterá qualquer valor. Se escrever na célula C1 para tentar alterar o cabeçalho, esse valor nunca surgirá no ecrã. A grelha não perdeu os dados: está simplesmente a aplicar as regras de uma união de células. Tanto no XLS quanto no XLSX, um retângulo unido exibe o conteúdo de apenas uma célula — a âncora superior esquerda —, tratando o restante intervalo como espaço sobreposto que pode conter dados mas nunca os mostra. Os utilizadores do Excel aprendem isto por tentativa e erro. Um gerador de relatórios tem de codificar este comportamento como regra, pois em código gerado o sintoma manifesta-se como uma região em branco sem exceções associadas para depuração. O HotXLS, uma biblioteca nativa Object Pascal que lê e escreve ambos os formatos Excel a partir de Delphi e C++Builder, expõe a tabela de uniões explicitamente para que possa programar em conformidade, em vez de redescobrir o problema através de um pedido de suporte

Um valor, uma âncora

A união de células consiste numa instrução de visualização sobreposta a uma grelha que não sofre alteração física de estrutura. Cada célula coberta continua a existir no ficheiro de forma individual; o registo de união apenas instrui o leitor a desenhar o conteúdo da âncora ao longo de todo o retângulo. Esta particularidade dita três comportamentos que deve compreender antes de desenhar layouts via código: ler uma célula coberta devolve o seu valor guardado individualmente (que na barra de títulos costuma ser vazio), pelo que qualquer código que inspecione um título unido tem de obter e ler a âncora; escrever numa célula coberta conclui-se com sucesso ao nível do ficheiro mas não exibe qualquer resultado (a armadilha do cabeçalho invisível); e anular a união (unmerge) de uma região expõe o conteúdo que esteve sob ela todo o tempo, pelo que dados perdidos escritos no espaço coberto tornar-se-ão visíveis no dia em que alguém anular a união

No lado XLSX, essa tabela é um objeto de primeira classe. A propriedade Sheet.MergedCells disponibiliza os métodos Add('A1:C1'), FindAt(Row, Col), DeleteAt e a coleção Items, sendo a chamada a FindAt a mais utilizada: forneça-lhe qualquer coordenada e esta devolverá o intervalo unido que cobre a célula, ou nil se a célula estiver isolada. Esta consulta constitui a base para gerir uniões corretamente, tanto na leitura segura quanto na proteção de escrita, como veremos adiante

Duas interfaces, duas abordagens de união

A interface XLS adota a convenção COM do Excel: obtém um intervalo a partir de uma propriedade indexada de dois argumentos e invoca Merge com um tipo OleVariant cujo valor determina a geometria resultante

var
  Book: IXLSWorkbook;   // referências de interface: sem Free manual
  Sh: IXLSWorksheet;
begin
  Book := TXLSWorkbook.Create;
  Sh := Book.Sheets[1];                 // a coleção de folhas XLS baseia-se em 1
  Sh.Range['A1', 'F1'].Merge(False);    // False = um bloco unido
  Sh.Cells.Item[1, 1].Value := 'Quarterly Statement';
  Sh.Range['A3', 'F4'].Merge(True);     // True = união horizontal: uma união por linha
  Book.SaveAs('layout.xls');
end;

O parâmetro passado a Merge é frequentemente mal interpretado. Num intervalo de duas linhas, Merge(True) gera duas uniões independentes de uma única linha, que corresponde à funcionalidade "Merge Across" (Unir na Horizontal) do Excel e é o comportamento pretendido para blocos de cabeçalho empilhados onde as linhas devem permanecer separadas. O método Merge(False) funde todo o retângulo num único bloco. O intervalo disponibiliza também o marcador de estado MergeCells, devolve a região abrangida através de MergeArea e desfaz a união com Unmerge. A faceta XLSX expõe as mesmas operações sob assinaturas distintas: Sheet.MergeCells(Row1, Col1, Row2, Col2) recebe limites inteiros; TXLSXRange.Merge aceita o parâmetro equivalente Across; e a coleção MergedCells armazena os resultados

Um modelo que cresce com os dados

Sheet.Range['A1:F1'].Merge;
Sheet.Cells[1, 1].Value := 'FATURA #2026-0611';    // o valor é atribuído à âncora, A1
Sheet.RowHeight[1] := 28;
TitleFont := Book.Fonts.Add('Calibri', 16, True, False);
Sheet.Cells[1, 1].FontIndex := TitleFont + 1;        // índice do pool baseado em 0, na célula baseia-se em 1

// a linha 5 é a linha modelo de detalhe formatada
for I := 0 to ItemCount - 1 do
  Sheet.CopyRange(5, 1, 5, 6, 6 + I, 1);             // os estilos e fórmulas acompanham o deslocamento

// abrir espaço antes do bloco de totais; o conteúdo abaixo desloca-se para baixo
Sheet.InsertRows(6 + ItemCount, 1);
Sheet.Range['A1:F1'].SetBorders(xlsxEdgeOutline, xlsxBorderMedium);

Duas linhas merecem particular atenção. A atribuição do tipo de letra contém um desfasamento de uma unidade que falha de forma silenciosa: o método Fonts.Add devolve uma posição baseada em 0, ao passo que as células armazenam uma referência baseada em 1, na qual o valor 0 representa o tipo de letra padrão; assim, omitir o + 1 não gerará exceções, mas aplicará o tipo de letra incorreto ao título. A outra instrução relevante é CopyRange, que duplica a formatação e as vias fórmulas em conjunto com os valores. Esta é a razão principal para clonar uma linha modelo construída manualmente em vez de tentar recriar o seu aspeto via código: o designer define a estética no modelo uma vez, restando ao gerador apenas preencher os dados nas cópias

Esta separação escala quando o layout reutilizável reside no seu próprio livro de cálculo (como uma folha contendo secções de cabeçalho e rodapé partilhadas entre vários relatórios). O método CopyRangeTo realiza a clonagem entre folhas distintas, recebendo a folha de destino e as coordenadas correspondentes; isto permite ao gerador manter uma folha modelo inalterada e replicar as suas áreas em tantas folhas de saída quantas as necessárias. A alternativa (alterar o modelo no próprio local e tentar repor a estrutura no final) é suscetível a falhas se o processamento for interrompido a meio

O que o InsertRows desloca e o que não desloca

Inserir linhas antes do bloco de totais é o que permite ao intervalo de uma fórmula SUM expandir-se à medida que a secção de detalhe cresce. No lado XLSX, o método InsertRows desloca uma vasta lista de estruturas em conjunto com as células: células unidas, alturas de linhas, hiperligações, comentários, painéis fixos (frozen panes), intervalos de autofiltro, formatação condicional, validação de dados, tabelas, nomes definidos e âncoras de gráficos e imagens que se situem abaixo do ponto de inserção. É isto que garante que o bloco de totais seja deslocado para a sua nova linha mantendo as suas uniões e formatos numéricos sem perdas

As duas limitações documentadas devem ser tidas em conta no desenho da aplicação. O ajuste de fórmulas está limitado à folha em edição: as referências internas são reescritas, e uma fórmula noutra folha que aponte para o intervalo deslocado também é atualizada, mas esta atualização afeta apenas referências direcionadas para a folha em edição; como tal, qualquer referência entre livros distintos requer validação adicional. A segunda limitação é mais rígida e afeta a faceta XLS: as tabelas dinâmicas (pivot tables) são preservadas como blocos de bytes não modelados pelo motor, pelo que a inserção de linhas não altera a sua posição no documento. Qualquer modelo estruturado para o formato .xls deve colocar as áreas de tabelas dinâmicas longe de qualquer intervalo que cresça

Impedir a escrita de dados em espaço de layout

O erro com células unidas que costuma manifestar-se em produção não é meramente estético, mas sim estrutural: uma linha de detalhe sobrepõe-se a uma área de layout unida, os seus valores são gravados em células cobertas (ficando invisíveis) e os totais das colunas deixam de coincidir com a informação visível. Dado que o método FindAt identifica a região de união para qualquer coordenada, o gerador consegue impedir a escrita no exato momento em que esta ocorreria, evitando disponibilizar um relatório com cálculos incorretos

// recusar a escrita de dados de detalhe numa região de layout unida
if Sheet.MergedCells.FindAt(Row, 1) <> nil then
  raise Exception.CreateFmt('a linha %d sobrepõe-se a uma região de layout unida', [Row]);
Sheet.Cells[Row, 1].Value := Detail.Description;

Esta validação de limites deve ser integrada em qualquer área onde o utilizador possa vir a aplicar ordenação ou filtros. Um intervalo contendo células unidas não pode ser ordenado corretamente porque a ordenação move as linhas individualmente, e uma união que abranja várias linhas não tem uma linha única à qual associar o deslocamento (o Excel gerará erro ou desestruturará o layout). A boa prática recomenda delimitar a união a áreas geográficas específicas: restrinja as uniões a blocos de títulos, divisores de secções e áreas de assinaturas, mantendo o bloco tabelado central plano. O artigo sobre geração de relatórios com modelos desenvolve esta separação entre layout e dados num fluxo baseado em marcadores, e o artigo sobre formatação condicional e rich text aborda a formatação destas colunas planas

Como as uniões divergem na exportação

A união de células é um conceito específico de livros de cálculo, e cada formato de exportação de texto suporta-o em níveis distintos. Conhecer estes comportamentos antecipadamente evita falhas na validação de qualidade: a exportação para HTML reproduz as uniões fielmente, gerando atributos colspan e rowspan numa única tabela, mantendo o aspeto formatado no browser; a exportação para RTF não une as colunas no resultado, gravando o texto da âncora na sua célula e mantendo a largura restante da união em branco (o que encosta os títulos à esquerda no processador de texto); e o formato CSV não suporta o conceito de união, pelo que o valor da âncora ocupa um campo e as células cobertas são exportadas como campos vazios. A regra para livros que também alimentam exportações delimitadas consiste em evitar colocar dados relevantes em áreas unidas; o artigo sobre exportações para CSV, TSV e HTML detalha o comportamento de cada formato

Uma garantia relevante quanto ao tamanho dos ficheiros: as uniões têm custo negligenciável na escala de relatórios. A tabela de uniões é minúscula em comparação com os dados das células, e a leitura de uma célula coberta recorre ao método FindAt em vez de efetuar varrimentos. A otimização de grandes livros foca-se noutros aspetos (como a consolidação de estilos e a memória do fluxo de gravação), abordados no artigo sobre desempenho com grandes livros de cálculo. Ambas as APIs de união, as operações de edição estrutural e os exemplos de modelos são fornecidos com o HotXLS Component