Artigo Técnico

Incorporar gráficos em folhas de cálculo no HotXLS

O HotXLS pode colocar um gráfico diretamente numa folha de cálculo, ancorado a um intervalo de células, em vez de o pôr numa folha de gráfico dedicada. Em termos BIFF8 isso significa escrever uma forma de desenho com um registo OBJ de tipo 5 e estacionar o substream do gráfico no fim do stream de registos da folha, que é exatamente o layout que o Excel produz e exatamente onde o leitor espera encontrá-lo

A distinção importa a quem gera relatórios operacionais. Uma folha de gráfico é um bom lar para um único visual de destaque. Um detalhe regional mensal quer o gráfico ao lado dos números que resume, na mesma folha, dimensionado para o bloco de células a que pertence, para o leitor rolar uma vez em vez de trocar de separador e perder contexto

A leitura já lá estava, a escrita não

A assimetria vale a pena nomear porque molda o trabalho. O HotXLS já sabia ler gráficos incorporados: quando o stream de registos da folha contém um BOF marcado como substream de gráfico, o parser muda de contexto, recolhe os registos do gráfico, e no EOF de fecho devolve-os à forma de desenho que o registo OBJ introduziu. Esse caminho tinha sido exercitado por todos os livros de Excel que a biblioteca alguma vez abriu

O que faltava era o lado da autoria, e a consequência útil é que o novo escritor tinha uma especificação precisa a atingir: produzir o layout de bytes que o leitor existente já reanexa. Não há melhor critério de aceitação para uma funcionalidade de formato binário do que um leitor escrito de forma independente que não teve a oportunidade de mudar

De que é feito um gráfico incorporado

Três peças têm de estar de acordo. A camada de desenho contribui uma forma de controlo anfitrião, a camada de objetos contribui um registo OBJ cujos dados de objeto comuns declaram tipo de objeto 5, e o stream de registos contribui o próprio substream do gráfico. As flags de opções no registo OBJ são as que o Excel escreve para uma moldura de gráfico: posicionado, bloqueado, linha automática e preenchimento automático, que é o que faz o gráfico incorporado comportar-se como um nativo quando um utilizador clica nele

O HotXLS ancora um substream de gráfico BIFF8 a uma folha Delphi através de três peças em concordância: a forma de controlo anfitrião da camada de desenho, o registo OBJ cujos dados de objeto comuns declaram tipo de objeto 5, e a cadeia de registos do gráfico estacionada no fim do stream de registos da folha, onde um BOF de gráfico muda o contexto do parser e o EOF de fecho reanexa os registos
Três camadas transportam um gráfico incorporado: a forma de desenho ancora-o, o registo OBJ tipa-o como anfitrião de gráfico, e o substream do gráfico no fim do stream da folha fornece os registos que o leitor reanexa

A âncora merece uma nota porque é uma fonte comum de bugs de off-by-one. A API do HotXLS recebe números de linha e coluna base um, a condizer com o resto da biblioteca, e a âncora de cliente escrita no ficheiro é base zero. A conversão acontece dentro de AddChartObject, por isso os chamadores ficam no sistema de coordenadas que usam em todo o resto, mas quem comparar um hex dump com a sua própria chamada precisa de lembrar de que lado dessa fronteira está a ler

var
  Book: TXLSWorkbook;
  Sheet: TXLSWorksheet;
  Series: array[0..1] of TXLSChartSeriesInfo;
begin
  Book := TXLSWorkbook.Create(nil);
  try
    Book.LoadFromFile('regional-sales.xls');
    Sheet := Book.Sheets[0];

    FillChar(Series, SizeOf(Series), 0);
    Series[0].Name := 'Actual';
    Series[0].Categories := 'Data!$A$2:$A$13';
    Series[0].Values := 'Data!$B$2:$B$13';
    Series[0].DataLabels.ShowValue := True;
    Series[0].HasDataLabels := True;

    Series[1].Name := 'Target';
    Series[1].Categories := 'Data!$A$2:$A$13';
    Series[1].Values := 'Data!$C$2:$C$13';
    Series[1].SecondaryAxis := True;

    // Ancorado a E2:M20 nesta folha, base um
    Sheet.AddChartObject(xlsChartTypeColumn, 'Regional sales',
      'Month', 'Amount', Series, 2, 5, 20, 13);

    Book.SaveToFile('regional-sales-charted.xls');
  finally
    Book.Free;
  end;
end;

O FillChar no array de séries não é decoração. TXLSChartSeriesInfo transporta vários sub-registos opcionais, rótulos de dados, estilo por série, linhas de tendência e barras de erro, cada um condicionado por um booleano, e um registo parcialmente inicializado na stack vai entregar ao emissor flags que ninguém pôs. Ponha o array a zero, e depois defina os campos que quer

Que referências de série aceita o caminho incorporado?

Intervalos em estilo A1 simples dentro do mesmo livro, e essa restrição é deliberada e não um descuido. Toda a referência é resolvida contra a lista de folhas do livro e convertida no índice de referência externa de que os registos do gráfico precisam. Um intervalo com nome ou uma referência a um livro externo cai num placeholder com uma expressão parseada de comprimento zero, por isso o gráfico escreve-se limpo mas essa série em particular não tem fonte de dados até a apontar a um intervalo

Aceitação de referências de série pelo HotXLS no caminho de gráfico BIFF8 incorporado: intervalos em estilo A1 como Data!$B$2:$B$13 dentro do mesmo livro resolvem contra a lista de folhas para o índice de referência externa de que os registos do gráfico precisam, enquanto intervalos com nome e referências a livros externos caem num placeholder com uma expressão parseada de comprimento zero, com ambos cobertos pelo AddChartSheet
Só intervalos simples em estilo A1 dentro do mesmo livro compilam para referências de séries de gráfico; todo o resto escreve-se limpo como placeholder até ser reapontado, e o caminho completo vive no AddChartSheet

A razão é uma troca de engenharia direta. O caminho completo de compilação de referências existe na rota da folha de gráfico, embrulhado na camada de coleção de folhas, e extraí-lo limpo significaria duplicar uma centena de linhas de lógica de resolução para um caso incomum na prática. Um gráfico incorporado quase sempre traça células na sua própria folha ou numa folha de dados irmã. As referências com nome e externas estão cobertas no caminho da folha de gráfico via AddChartSheet, por isso nada fica indisponível, apenas alcançado a partir de um ponto de entrada diferente

Todo o resto no modelo de séries funciona identicamente nas duas rotas. Ligação de eixo secundário, estilos de linha, preenchimento e marcadores por série, linhas de tendência, barras de erro e rótulos de dados fazem todos parte de TXLSChartSeriesInfo e todos são emitidos da mesma forma, por isso uma definição de gráfico pode mover-se entre um objeto incorporado e uma folha de gráfico mudando apenas a chamada. A mecânica de grupos de eixos por trás da flag do eixo secundário está coberta em grupos de eixo secundário na escrita BIFF

Porque é que o título do gráfico leu como dois caracteres?

Porque foi passada uma contagem de caracteres onde se esperava uma contagem de bytes, e as strings Unicode BIFF tornam esse erro fácil de escrever e difícil de ver. Uma string Unicode BIFF curta começa com uma contagem de caracteres e um byte de flags, e o byte de flags transporta o bit de byte alto que diz se o payload é de um byte por caráter ou de dois. Leia um payload de 16 bits com a contagem de caracteres como se fosse um comprimento em bytes e recebe exatamente metade da string: uma série chamada Sales volta como Sa, e um título de gráfico trunca da mesma forma porque títulos e rótulos de série partilham o caminho de descodificação

O que torna este defeito notável é que recorreu três vezes na mesma família de registos: uma vez em nomes de linhas de tendência, uma vez em nomes de gráficos dinâmicos, e uma vez em títulos de gráfico. Cada ocorrência parecia um bug fresco numa funcionalidade nova. Os três eram a mesma multiplicação em falta. A regra que finalmente o encerrou é mecânica e deve ser aplicada sem julgamento: sempre que ler uma destas strings, consulte primeiro a flag de byte alto e multiplique a contagem de caracteres pela largura do payload antes de tocar no buffer. Os detalhes ao nível do registo estão em descodificar contagens de caracteres XLUnicodeString e a flag de byte alto

// O gráfico incorporado partilha a camada de desenho com imagens e formas,
// por isso um desenho existente na folha é preservado. AddChartObject
// devolve o índice do objeto criado
var
  ObjIndex: Integer;
begin
  ObjIndex := Sheet.AddChartObject(xlsChartTypeLine, 'Trend',
    'Week', 'Units', Series, 2, 8, 18, 16);
  if ObjIndex < 0 then
    raise Exception.Create('chart object was not created');
end;

Onde os gráficos incorporados encaixam face às alternativas

Três rotas existem e respondem a perguntas diferentes. Um objeto de gráfico incorporado pertence ao lado dos seus dados numa folha de cálculo e é o que a maioria dos relatórios quer. Uma folha de gráfico serve um único visual de apresentação e dá-lhe o caminho completo de compilação de referências. Preservar um gráfico existente de um ficheiro carregado, intocado, é a resposta certa quando o livro veio do Excel com formatação que ninguém quer que a biblioteca reinterprete; esse comportamento de passagem está descrito em ChartML preservado e gráficos de combinação

Como o gráfico incorporado anda na camada de desenho, coexiste com imagens e formas na mesma folha em vez de as substituir, e o modelo geral dessa camada está coberto em gráficos, imagens e desenhos no HotXLS. As três rotas são distribuídas no componente de folhas de cálculo Delphi HotXLS, por isso a escolha é sobre o aspeto que o relatório deve ter e não sobre o que a biblioteca consegue exprimir

O ponto metodológico é o que vale a pena guardar. Quando uma funcionalidade de formato binário tem um leitor existente, construa o escritor contra o leitor e não contra a sua leitura da especificação. O leitor codifica anos de contacto com ficheiros que aplicações reais produziram de facto, incluindo as partes que a especificação enuncia vagamente, e um escritor que o satisfaz tem muito mais probabilidade de satisfazer também o Excel