Uma ferramenta de conciliação acha 3.000 células que discordam do livro contábil e quer que o .xls salvo abra com todas elas selecionadas sob uma linha de cabeçalho congelada. No papel isso é um registro Selection. Em bytes são 9 + 3.000 × 6 = 18.009, e o corpo de um registro BIFF8 para em 8.224. A mesma pasta de trabalho levanta uma segunda pergunta que soa trivial e não é: numa planilha congelada em B2, "a posição de scroll" são quatro panes compartilhando dois offsets de linha e dois de coluna, e esses quatro números vivem em dois registros diferentes
Os dois problemas são aritmética de formato: a seleção é cortada em registros que um leitor precisa costurar de volta, o pane é nomeado por um código que não segue a ordem de leitura, e cada eixo de scroll aterrissa no registro de que o Excel o lê
Por que 3.000 áreas selecionadas precisam de três registros Selection
A [MS-XLS] §2.4.248 desenha o registro Selection (0x001D) como uma parte fixa de 9 bytes seguida pelas áreas. A parte fixa guarda o byte de pane, o rwAct e o colAct da célula ativa, o irefAct do índice da área ativa, e o cref da contagem de áreas. Cada área é uma estrutura RefU com duas linhas de 16 bits e duas colunas de 8 bits. A maior contagem que cabe é (8.224 − 9) / 6 arredondada para baixo, o que dá 1.369 áreas e um corpo de 8.223 bytes, um byte abaixo do teto
O HotXLS usa 1.369 como teto por registro. Quando o TXLSWorksheet salva um grupo de seleção com mais áreas que isso, ele grava registros Selection consecutivos do mesmo pane, 1.369 áreas por vez, e cada registro repete o mesmo byte de pane, célula ativa e índice ativo. A seleção de conciliação de 3.000 células sai como três registros segurando 1.369, 1.369 e 262 áreas. Quem chama nunca vê essa divisão; entrega um array de referências A1
var
Book: TXLSWorkbook;
Sheet: TXLSWorksheet;
Diffs: TXLSSelectedAreas;
I: Integer;
begin
Book := TXLSWorkbook.Create;
try
Sheet := Book.Sheets.Add;
Sheet.FreezePanes(1, 1); // a linha de cabeçalho e a coluna A ficam paradas
SetLength(Diffs, 3000);
for I := 0 to High(Diffs) do
Diffs[I] := Format('C%d', [I + 2]);
// FreezePanes limpa a seleção armazenada, então selecione depois de congelar.
// 3000 áreas são salvas como três registros Selection: 1369 + 1369 + 262
if not Sheet.SelectAreas(xlspBottomRight, Diffs, 0) then
raise Exception.Create('Selection rejected');
Book.SaveAs('reconciliation.xls');
finally
Book.Free;
end;
end;
A ordem das chamadas importa aqui. O FreezePanes reconstrói o layout de panes e limpa toda seleção por pane que a planilha carregava, então uma seleção anterior teria ido embora na hora de salvar. Numa planilha sem congelamento o pane inferior-direito ainda não existe, e a mesma chamada SelectAreas simplesmente retornaria False. Congele, depois selecione
Para que aponta o irefAct quando uma seleção atravessa vários registros?
Numa seleção dividida, o irefAct indexa a sequência agregada inteira de áreas através de todos os blocos, e todo bloco carrega o mesmo valor. Uma seleção uma área além do limite torna isso concreto: 1.370 áreas com a última ativa viram dois registros, o primeiro com cref 1.369 e o segundo com cref 1, e ambos carregam irefAct 1.369. No segundo registro esse índice é maior que a contagem de áreas do próprio registro. Isso parece quebrado se você lê um registro por vez, e dois tipos de leitor tropeçam nisso
- Um leitor que confere o
irefActcontra ocrefdentro de cada registro rejeita um arquivo perfeitamente válido no segundo bloco - Um leitor que substitui o estado de seleção dele a cada registro Selection guarda só o último bloco e descarta as primeiras 1.369 áreas
O leitor do HotXLS anexa registros consecutivos do mesmo pane num só grupo e exige que todo bloco concorde na linha ativa, na coluna ativa e no irefAct. A checagem de intervalo do irefAct, e a checagem de que a célula ativa realmente senta dentro da área para a qual aponta, rodam só quando o registro EOF da planilha chega e a sequência completa é conhecida. Checar antes seria código mais simples e a resposta errada para qualquer seleção acima de 1.369 áreas
No lado da escrita, a sobrecarga do SelectAreas que recebe o pane primeiro não tem teto de 1.369 áreas próprio. Ela analisa toda referência antes de tocar na planilha: marcadores $ são aceitos e removidos, um intervalo invertido como C9:A2 é normalizado para A2:C9, e uma entrada vazia ou não analisável falha a chamada inteira. O índice ativo precisa cair dentro do array e ficar em ou abaixo de 32.767, porque o irefAct é um campo de 16 bits com sinal. Qualquer falha retorna False e deixa a seleção anterior exatamente como estava
Códigos de pane não seguem a ordem de leitura
Um registro Selection nomeia o pane dele com o código numérico que o formato define: 0 para bottom-right, 1 para top-right, 2 para bottom-left e 3 para top-left. A enumeração pública TXLSPanePosition é declarada em ordem de leitura, xlspTopLeft, xlspTopRight, xlspBottomLeft, xlspBottomRight. Então Ord(xlspTopLeft) é 0, e 0 no arquivo significa bottom-right. Um escritor que faz cast do enum direto no byte de pane põe toda seleção top-left no pane bottom-right, e o arquivo continua estruturalmente válido porque os dois são códigos de pane legais
Todo ponto de entrada consciente de pane no HotXLS converte o enum por um case explícito, então o código da aplicação nunca lida com os códigos numéricos. A existência do pane também é checada contra a geometria atual de freeze ou split. O pane top-left sempre existe. O top-right precisa de um split vertical, o bottom-left de um horizontal, e o bottom-right de ambos. Para um pane que a planilha não tem, o SelectAreas retorna False e o GetSelectedAreas retorna um array vazio com ActiveAreaIndex em -1, e nenhuma das chamadas cria pane, seleção ou célula no caminho
A planilha também mantém o SelectAreas(Areas, ActiveAreaIndex) mais antigo de dois argumentos, que grava no pane que estiver ativo no momento e retorna um status Integer. Numa planilha com panes congelados isso costuma ser um chute sobre um estado que você mesmo não definiu. Use a sobrecarga que recebe um TXLSPanePosition primeiro para qualquer coisa que rode em planilhas congeladas ou divididas; o pane então faz parte da chamada, e um resultado False diz que a geometria e o pedido discordam. A leitura de volta segue a mesma forma:
var
Areas: TXLSSelectedAreas;
Active: Integer;
begin
Areas := Sheet.GetSelectedAreas(xlspBottomRight, Active);
if Active < 0 then
Memo1.Lines.Add('No usable selection on the bottom-right pane')
else
Memo1.Lines.Add(Format('%d areas, active area %s',
[Length(Areas), Areas[Active]]));
end;
Onde vive a posição de scroll de cada pane?
A posição de scroll de cada pane fica dividida entre dois registros, porque quatro panes compartilham só duas posições de linha e duas de coluna. Numa pasta de trabalho clássica, a primeira linha visível dos panes de cima e a primeira coluna visível dos panes da esquerda são Window2.rwTop e Window2.colLeft. A linha dos panes de baixo e a coluna dos panes da direita são Pane.rwTop e Pane.colLeft. O top-right portanto toma a linha do Window2 e a coluna do Pane, e o bottom-left faz o inverso
O ScrollWindow e o TryGetWindowScroll seguem esse layout exatamente e usam números de linha e coluna 1-based. O ScrollWindow(xlspTopRight, R, C) grava Window2.rwTop e Pane.colLeft, o que significa que definir a coluna do top-right também move o pane bottom-right. Isso bate com o Excel, em que os dois panes da direita dividem uma barra de rolagem horizontal. Um pane inexistente faz o ScrollWindow retornar False e faz o TryGetWindowScroll zerar as duas saídas. Uma linha ou coluna fora do intervalo é rejeitada antes de qualquer eixo mudar, então uma coluna ruim não deixa uma linha pela metade para trás
var
Row, Col: Integer;
begin
Sheet.FreezePanes(1, 1);
// Bottom-right: eixo de linha inferior (Pane.rwTop) e eixo de coluna direito (Pane.colLeft)
Sheet.ScrollWindow(xlspBottomRight, 500, 3);
// Top-right compartilha o eixo de coluna direito, então isso também move o bottom-right para a coluna 6
Sheet.ScrollWindow(xlspTopRight, 1, 6);
if Sheet.TryGetWindowScroll(xlspBottomRight, Row, Col) then
Memo1.Lines.Add(Format('Bottom-right starts at row %d, column %d', [Row, Col]));
// Bottom-right começa na linha 500, coluna 6
end;
O XLSX guarda as mesmas duas camadas em dois elementos: sheetView/@topLeftCell (ECMA-376 Part 1, §18.3.1.87) para a janela como um todo e o filho pane/@topLeftCell (§18.3.1.66) para o lado inferior-direito de um split. Os dois atributos podem estar presentes ao mesmo tempo. Um loader que fica com só um deles perde ou as linhas de cima ou as colunas da esquerda de toda planilha congelada que tocar. O TXLSXWorksheet lê o atributo externo nos campos de nível de janela primeiro, deixa o filho pane sobrepor só os campos de nível de pane, e grava os dois de volta separadamente. O ScrollWindow e o TryGetWindowScroll têm as mesmas assinaturas e o mesmo significado no TXLSXWorksheet, e copiar uma planilha carrega as duas camadas em qualquer formato
As propriedades mais estreitas continuam lá para código que só precisa de um par de eixos: ScrollRow e ScrollColumn na planilha clássica, e o SetPaneScroll e o GetPaneScroll zero-based para o pane inferior-direito no XLSX. Um controle de renderização mantém o TopRow e o LeftCol próprios, como descreve o artigo sobre renderizar pastas de trabalho numa grade VCL customizada, e esse estado de runtime é separado do que é gravado. A geometria de freeze e split em si é definida com as configurações de nível de planilha cobertas em proteção de planilha, configuração de página e impressão
Registros Selection corrompidos são mantidos byte a byte
Quando um registro Selection está malformado, o HotXLS mantém o corpo dele como bytes opacos, reporta o código de diagnóstico 1304 (xlsDiagnosticSelectionRecordInvalid) como aviso, e regrava o corpo original sem mudanças no save. Arquivos de exportadores desconhecidos ou escritores BIFF feitos à mão podem conter registros assim. Antes de um registro entrar no grupo do pane dele, o leitor checa um byte de pane de 3 ou menos, registros contíguos por pane, cref entre 1 e 1.369 com corpo de exatamente 9 + cref × 6 bytes, blocos que concordam na célula ativa e um irefAct sem o bit de sinal, uma coluna ativa na grade, e nenhuma área invertida. Problemas dentro de um único registro físico são reportados uma vez por registro; contradições que só aparecem depois da agregação, como um irefAct apontando além da contagem total de áreas ou uma célula ativa fora da área indexada, são reportadas uma vez por grupo no EOF
Um grupo inválido fica fora da API tipada: o GetSelectedAreas retorna um array vazio com índice -1 para aquele pane, e os outros panes seguem funcionando normalmente
var
I: Integer;
D: TXLSDiagnostic;
begin
if Book.Open('supplier-upload.xls') <> 1 then
Exit;
for I := 0 to Book.Diagnostics.Count - 1 do
begin
D := Book.Diagnostics[I];
if D.Code = xlsDiagnosticSelectionRecordInvalid then
// Log é um TStrings seu; o HotXLS só preenche Diagnostics
Log.Add(Format('%s: record $%.4x kept opaque (%s)',
[D.SheetName, D.RecordId, D.Message]));
end;
end;
Consertar o registro, limitando o irefAct ou descartando a área invertida, era a outra opção. Preservá-lo é o padrão mais seguro: uma seleção reparada é uma que ninguém escolheu, e os bytes originais teriam ido embora. A mensagem de diagnóstico nomeia a checagem que falhou, como "the area count does not match the record length" ou "records for the same pane are not contiguous", e o RecordId é sempre $001D para esse código
Inserções de linha e coluna remapeiam a seleção de todo pane
Inserir ou apagar linhas ou colunas inteiras remapeia a seleção de todo pane por uma rotina compartilhada, no motor clássico e no XLSX. As áreas que sobrevivem mantêm a ordem e se deslocam com as células que cobrem, e a área ativa mantém a identidade. Se a área ativa é apagada, a primeira área sobrevivente depois dela vira ativa; se nada vem depois, a última área sobrevivente antes dela. Se toda área é apagada, o grupo colapsa para uma única célula no limite da exclusão. Uma célula ativa que não cai mais dentro da área escolhida se move para o canto superior esquerdo dessa área, para que índice e coordenada nunca se contradigam no arquivo salvo
Grupos marcados como inválidos na carga são deixados em paz pelo remapper, então os bytes originais deles continuam fazendo round-trip depois da edição. Mudar a seleção num pane reescreve só os registros daquele pane e deixa os registros dos outros panes byte-idênticos. Arquivos ODS não carregam estado de seleção de pane nenhum, porque o OpenDocument não tem estrutura equivalente de view de planilha por pane; seleções e posições de scroll de pane se aplicam a XLS e XLSX
Se a sua aplicação salva pastas de trabalho que as pessoas abrem para revisar células marcadas, retomar de onde pararam ou ler um dashboard congelado, as chamadas de seleção e scroll conscientes de pane vêm com o HotXLS Delphi spreadsheet component e se comportam do mesmo jeito para saída .xls e .xlsx