Artigo Técnico

Células Unidas e Modelos de Relatório HotXLS em Delphi

Percorra as células de um modelo de relatório acabado de abrir e um título unido comporta-se como uma fuga. Lê A1 e obtém "Quarterly Statement"; lê B1 até F1, que visivelmente ficam debaixo da mesma faixa, e não obtém nada. Escreva um valor em C1 para corrigir o cabeçalho e ele nunca aparece no ecrã. A grelha não perdeu os seus dados. Está a fazer exatamente o que uma união significa: tanto em XLS como em XLSX, um retângulo unido representa o conteúdo de uma célula, a âncora no canto superior esquerdo, e trata as restantes como espaço coberto que guarda valores mas nunca os mostra. Os utilizadores de Excel absorvem isto por tentativa e erro. Um gerador de relatórios tem de o codificar como regra, porque em código gerado o sintoma é uma região em branco sem exceção nenhuma que o denuncie. O HotXLS, uma biblioteca nativa em Object Pascal que lê e escreve ambos os formatos do Excel a partir de Delphi e C++Builder, expõe a tabela de uniões de forma explícita o suficiente para se programar contra a regra em vez de a redescobrir num pedido de suporte

Um valor, uma âncora

Uma união é uma instrução de apresentação sobreposta a uma grelha que não muda de forma. Todas as células cobertas continuam a existir no ficheiro como posições próprias; o registo de união apenas diz a quem o consome que pinte o conteúdo da âncora por todo o retângulo. Essa distinção comanda três comportamentos que vale a pena interiorizar antes de escrever código de layout. Ler uma célula coberta devolve o seu próprio valor guardado, que numa faixa que construiu costuma estar vazio, pelo que qualquer código que inspecione um título unido tem de resolver e ler a âncora. Escrever numa célula coberta tem sucesso ao nível do ficheiro e não aparece em lado nenhum, que é a armadilha do cabeçalho invisível com que começámos. E desfazer a união de uma região expõe o que lá estava por baixo o tempo todo, pelo que um valor perdido escrito em espaço coberto se transforma num defeito visível no dia em que alguém dissolver a união

Diagrama de uma faixa unida do HotXLS onde as celulas cobertas mantem as suas proprias posicoes enquanto as leituras se resolvem na ancora A1 em folhas de calculo Delphi
O HotXLS mantém cada célula coberta como uma posição real e repinta apenas a âncora, pelo que as leituras se resolvem por A1 enquanto as escritas em espaço coberto ficam invisíveis até se desfazer a união

Do lado do XLSX, essa tabela é um objeto de primeira classe. O Sheet.MergedCells traz Add('A1:C1'), FindAt(Row, Col), DeleteAt e Items, e a chamada a que mais recorre é FindAt: entregue-lhe qualquer coordenada e devolve a região unida que cobre essa célula, ou nil quando a célula está sozinha. Essa única consulta é a base das duas metades do tratamento correto de uniões, a leitura segura e a proteção da escrita, e ambas aparecem mais à frente

Duas fachadas, dois idiomas de união

O HotXLS mantém o motor clássico BIFF8 .xls e o motor OOXML .xlsx como modelos de objetos separados, e cada um escreve a união de forma diferente porque descendem de convenções diferentes. A fachada XLS segue o idioma COM do Excel: obtém-se um intervalo a partir de uma propriedade indexada de dois argumentos e chama-se Merge com um OleVariant cujo valor decide a geometria com que se fica

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

O argumento de Merge é a parte em que as pessoas se enganam. Sobre um intervalo de duas linhas, Merge(True) produz duas uniões independentes de uma linha cada, que é o "Unir ao Longo" do Excel e exatamente o que se quer para uma banda de cabeçalho empilhada que deve manter as suas linhas separáveis. O Merge(False) funde todo o retângulo num único bloco. O intervalo também reporta MergeCells como sinalizador de estado, devolve a região que o contém através de MergeArea, e dissolve-se com Unmerge. A fachada XLSX expõe as mesmas operações com outros nomes: o Sheet.MergeCells(Row1, Col1, Row2, Col2) recebe limites inteiros, o TXLSXRange.Merge aceita a variante Across equivalente, e a coleção MergedCells guarda o resultado

Um modelo que cresce com os seus dados

Um modelo de relatório a sério não é uma grelha fixa. O cabeçalho e os totais são fixos, mas a secção de detalhe entre eles estica-se até ao que a consulta devolver. O padrão que aguenta mantém no modelo uma linha de detalhe completamente estilizada, clona-a uma vez por registo, e depois abre uma folga antes do bloco de totais para que tudo o que está ancorado por baixo desça sem perder a formatação

Modelo de relatorio do HotXLS a crescer em Delphi: a linha de detalhe estilizada e clonada por registo e o InsertRows abre uma folga para que o bloco de totais desca com as unioes intactas
Clonar a linha de detalhe estilizada leva os seus estilos e fórmulas para todas as cópias, e o InsertRows faz depois descer a banda de totais com as uniões e os formatos intactos
Sheet.Range['A1:F1'].Merge;
Sheet.Cells[1, 1].Value := 'INVOICE #2026-0611';    // o valor vai para a âncora, A1
Sheet.RowHeight[1] := 28;
TitleFont := Book.Fonts.Add('Calibri', 16, True, False);
Sheet.Cells[1, 1].FontIndex := TitleFont + 1;        // índice do pool começa em 0, lado da célula em 1

// a linha 5 é a linha-modelo de detalhe já estilizada
for I := 0 to ItemCount - 1 do
  Sheet.CopyRange(5, 1, 5, 6, 6 + I, 1);             // os estilos e as fórmulas viajam com ela

// abre uma folga acima do bloco de totais; o conteúdo abaixo desce
Sheet.InsertRows(6 + ItemCount, 1);
Sheet.Range['A1:F1'].SetBorders(xlsxEdgeOutline, xlsxBorderMedium);

Duas linhas merecem um segundo olhar. A atribuição da fonte traz um desvio de uma unidade que morde em silêncio: o Fonts.Add devolve uma posição do pool com base em 0, enquanto uma célula guarda uma referência de fonte com base em 1, onde 0 significa a fonte por omissão, pelo que deixar cair o + 1 não levanta nada, apenas estiliza o seu título com o tipo de letra errado. A outra linha é o CopyRange, que leva a formatação e as fórmulas juntamente com os valores. É essa a razão inteira para clonar uma linha-modelo construída à mão em vez de reconstruir o seu aspeto em código. Um designer é dono da aparência uma vez, no modelo; o gerador limita-se a verter dados em cópias dela

Essa separação escala ainda mais quando o layout reutilizável vive na sua própria pasta de trabalho, digamos uma folha de bandas de cabeçalho e rodapé partilhada entre relatórios. O CopyRangeTo executa o mesmo clone através das fronteiras entre folhas de cálculo, recebendo uma folha de destino e as coordenadas de destino, de modo que um gerador pode manter uma folha-modelo imaculada e carimbar as suas regiões em tantas folhas de saída quantas o trabalho precisar. A alternativa, mutar o modelo no lugar e tentar restaurá-lo depois, é do género de coisa que funciona até ao dia em que uma execução aborta a meio

O que o InsertRows move, e o que não move

O padrão de fazer crescer um modelo só funciona porque o InsertRows do XLSX é uma edição estrutural e não uma baralhada de células. Quando abre uma folga, relocaliza as regiões unidas, as alturas de linha, as hiperligações, os comentários, os painéis fixos, os intervalos de filtro automático, os formatos condicionais, as validações de dados, as tabelas, os nomes definidos, as âncoras de imagens e as âncoras de gráficos que estejam abaixo do ponto de inserção, e não apenas os valores das células. É isso que permite ao bloco de totais chegar à sua nova linha com as uniões e os formatos numéricos intactos, em vez de chegar despido

Os seus dois limites documentados são aquilo em torno do qual se deve desenhar. O ajuste de fórmulas está circunscrito à folha que está a ser editada: as referências dentro dessa folha são reescritas, e uma fórmula noutra folha que aponte para a área deslocada também é reescrita, mas o ajuste só segue referências que visem a folha editada, pelo que qualquer esquema de referências entre pastas de trabalho merece a sua própria auditoria em vez de confiança cega. O segundo limite é mais afiado, e é do lado do XLS. As tabelas dinâmicas sobrevivem aos ciclos de abrir e gravar como registos preservados em bruto, e não como objetos modelados que o HotXLS consiga mover, pelo que inserir linhas não relocaliza a área ocupada por uma tabela dinâmica. Qualquer modelo que construa para o formato .xls deve estacionar as suas regiões de tabela dinâmica bem longe de qualquer banda que cresça

Recusar escrever dados em espaço de layout

A falha de células unidas que chega mesmo a produção não é a cosmética. É estrutural: uma linha de detalhe deriva para dentro de uma banda de layout unida, os seus valores aterram em células cobertas e tornam-se invisíveis, e os totais das colunas deixam discretamente de bater certo com o que qualquer pessoa que leia a folha consegue ver. Como o FindAt responde à pergunta da região que cobre qualquer coordenada, o gerador pode recusar essa escrita no momento em que aconteceria, em vez de publicar um relatório que conta a menos em silêncio

// recusa escrever dados de detalhe numa região de layout unida
if Sheet.MergedCells.FindAt(Row, 1) <> nil then
  raise Exception.CreateFmt('row %d overlaps a merged layout region', [Row]);
Sheet.Cells[Row, 1].Value := Detail.Description;

A mesma verificação de fronteira faz falta em qualquer sítio onde um utilizador venha mais tarde a ordenar ou filtrar o resultado. Um intervalo com uniões lá dentro não se ordena bem, porque a ordenação move as linhas de forma independente e uma união que abranja várias linhas não tem uma linha única com que viajar; o Excel responde com um erro ou um layout baralhado. A disciplina que mantém os relatórios corretos é geográfica. Confine as uniões a bandas de título, separadores de secção e blocos de assinatura, e mantenha plano o miolo tabular da folha. O artigo sobre geração de relatórios a partir de modelos desenvolve esta separação entre layout e dados até um fluxo completo assente em marcadores de posição, e o artigo sobre formatação condicional e texto formatado trata da estilização dessa banda plana de dados

Como as uniões se degradam à saída

Uma união é um conceito da pasta de trabalho, e cada formato de exportação orientado a texto honra-a em grau diferente. Conhecer os três comportamentos à partida poupa um ciclo de QA. A exportação HTML reproduz as uniões fielmente, emitindo colspan e rowspan numa única tabela, pelo que um relatório destinado ao browser mantém o seu aspeto em bandas. A exportação RTF não abrange colunas de todo: o texto da âncora aterra na sua própria célula e a largura restante da união sai como células vazias, o que deixa um título largo visualmente empurrado para a esquerda num processador de texto. O CSV não tem qualquer noção de união, pelo que o valor da âncora ocupa um campo e cada célula coberta sai como campo vazio. A conclusão para uma pasta de trabalho que também alimenta exportações delimitadas é manter tudo o que é estrutural fora da geometria unida; o artigo sobre exportação CSV, TSV e HTML percorre cada formato em detalhe

Titulo unido do HotXLS exportado a partir do Delphi para HTML com colspan e rowspan, para RTF sem abrangencia e para CSV como campos achatados
O mesmo título unido sobrevive à exportação HTML através de colspan e rowspan, degrada-se em RTF numa célula solitária presa à esquerda, e achata-se em CSV num valor mais campos vazios

Uma garantia para quem esteja a pesar isto contra o tamanho do ficheiro: as uniões não custam quase nada à escala de um relatório. A tabela de uniões é minúscula ao lado dos dados das células, e ler uma célula coberta continua a passar por FindAt em vez de uma varredura. A pressão de desempenho em pastas de trabalho grandes vem de outro lado, sobretudo do crescimento do pool de estilos e da memória que o caminho de gravação retém, assunto que o artigo sobre desempenho com pastas de trabalho grandes aborda diretamente. Ambas as APIs de união, as operações de edição estrutural e as demonstrações de modelos fazem parte do HotXLS Delphi Component