O HotXLS armazena seleções de planilha e posições de scroll por pane por meio de uma única API consciente de pane tanto em TXLSWorksheet quanto em TXLSXWorksheet: SelectAreas, GetSelectedAreas, ScrollWindow e TryGetWindowScroll. Para arquivos .xls clássicos, o HotXLS grava registros Selection do BIFF8 (0x001D) com no máximo 1369 áreas cada, converte nomes lógicos de pane nos bytes de pane que o formato define e mantém cada eixo de scroll no registro Window2 ou Pane onde o Excel espera
O problema costuma aparecer numa ferramenta de conciliação ou auditoria. A ferramenta abre uma exportação de livro contábil, acha toda célula que discorda do sistema de origem e salva a pasta de trabalho com essas células já selecionadas sob uma linha de cabeçalho congelada, para o revisor cair nas diferenças em vez de rolar atrás delas. Com quarenta diferenças funciona bem. O arquivo de fechamento de mês tem 3.000, e um único registro Selection guardando 3.000 áreas não pode existir: o corpo dele precisaria de 18.009 bytes, mais que o dobro do que um registro BIFF8 consegue carregar. A posição de scroll tem uma armadilha parecida. Numa planilha com panes congelados, "onde o usuário estava olhando" são quatro panes compartilhando duas posições de linha e duas de coluna, não uma coordenada
Por que uma seleção grande precisa de mais de um registro Selection?
Uma seleção grande precisa de vários registros porque o corpo de um registro BIFF8 tem teto de 8224 bytes e cada área selecionada custa seis bytes fixos. A [MS-XLS] §2.4.248 desenha o registro Selection como uma parte fixa de 9 bytes (o byte de pane, rwAct e colAct da célula ativa, irefAct da área ativa e cref da contagem de áreas) seguida de cref estruturas RefU, cada uma com duas linhas de 16 bits e duas colunas de 8 bits. A maior contagem que cabe é (8224 − 9) / 6 arredondada para baixo, que dá 1369, e isso produz um corpo de 8223 bytes, um byte abaixo do limite. O TXLSWorksheet.StoreSelectionGroup usa essa constante como MaxAreasPerRecord e grava um grupo maior como registros Selection consecutivos do mesmo pane, 1369 áreas por vez
O detalhe que morde é o irefAct. Cada bloco repete a mesma linha ativa, coluna ativa e índice de área ativa, e o irefAct indexa a sequência agregada de todos os blocos, não as áreas dentro do registro que o carrega. Uma seleção uma área além do limite torna isso concreto: 1370 áreas com a última ativa viram dois registros, o primeiro com cref 1369 e o segundo com cref 1, e ambos carregam irefAct 1369. Esse valor é maior que a própria contagem de áreas do segundo registro. Um reader que confere o irefAct contra o cref em cada registro rejeita um arquivo válido, e um reader que substitui seu estado a cada registro descarta as primeiras 1369 áreas. O reader do HotXLS anexa registros consecutivos do mesmo pane num só grupo, exige que todo bloco concorde na célula ativa e no índice, e roda a checagem de intervalo só no registro EOF da planilha, quando a sequência completa já é conhecida. A sobrecarga de SelectAreas que recebe o pane primeiro portanto não tem teto de 1369 áreas. Ela valida toda referência A1 e o índice ativo antes de pegar o lock de escrita da planilha, e devolve False com a seleção anterior intocada se algo estiver malformado
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]);
// Congelar reseta 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;
Qual byte de pane um registro Selection usa?
Um registro Selection identifica seu pane pelo 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, que é o pane bottom-right no arquivo. Fazer cast do enum direto no byte de pane gravaria toda seleção top-left no pane bottom-right sem erro nenhum. Todo ponto de entrada do HotXLS consciente de pane converte o enum por um case explícito, então quem chama nunca lida com os códigos numéricos. A existência do pane também é checada: o pane top-right só existe com um split vertical, o bottom-left só com um horizontal, e o bottom-right só com ambos. Para um pane que a geometria atual de split ou freeze não tem, o SelectAreas devolve False, e o GetSelectedAreas devolve um array vazio com ActiveAreaIndex em -1, sem criar pane, objeto de seleção ou célula nenhuma na pasta de trabalho
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, enquanto a linha dos panes de baixo e a coluna dos panes da direita são Pane.rwTop e Pane.colLeft. O ScrollWindow(xlspTopRight, R, C) portanto grava Window2.rwTop e Pane.colLeft, e setar a coluna do pane top-right também move o pane bottom-right, do mesmo jeito que os dois compartilham uma barra de rolagem horizontal no Excel. Os métodos públicos usam números de linha e coluna baseados em 1. Um pane inexistente devolve False e zera as duas saídas da consulta, e uma coordenada fora do intervalo é rejeitada antes que qualquer eixo mude. Nada aqui depende de como um viewer pinta a grade. Um controle de renderização mantém o próprio TopRow e LeftCol, como descreve o artigo sobre renderizar pastas de trabalho numa grade VCL customizada, e esses são estado de runtime, não o que é gravado
O XLSX espalha os mesmos dados por 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. O HotXLS lê primeiro o atributo externo nos campos de nível de janela, deixa o filho pane sobrepor só os campos de nível de pane, e grava os dois de volta separadamente. Colapsar as duas camadas numa só é exatamente como uma posição de scroll superior ou esquerda desaparece em silêncio na carga. Cópias de planilha carregam as duas camadas nos dois motores. Os pontos de entrada mais antigos mantêm o comportamento original: as propriedades clássicas ScrollRow e ScrollColumn, e o SetPaneScroll e GetPaneScroll do XLSX baseados em zero. A geometria de freeze e split em si é configurada com as configurações de nível de planilha cobertas em proteção de planilha, configuração de página e impressão
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 que acontece quando um registro Selection está corrompido?
Quando um registro Selection está corrompido, o HotXLS o mantém como bytes opacos, reporta o código de diagnóstico 1304 (xlsDiagnosticSelectionRecordInvalid) e regrava o corpo original byte a byte no save. Antes de um registro entrar no grupo do seu pane, o reader o checa em ordem. O byte de pane deve ser 3 ou menos. Registros de um mesmo pane devem ser contíguos no stream. Os 9 bytes fixos devem estar presentes. O cref deve estar entre 1 e 1369, e o corpo deve ter exatamente 9 + cref × 6 bytes. Todo bloco de um grupo deve concordar na célula ativa e no irefAct, o irefAct não deve ter o bit de sinal ligado, a coluna ativa deve estar na grade, e nenhuma área pode ter limites invertidos. Problemas num ú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 invisível para a API tipada: o GetSelectedAreas devolve um array vazio com índice -1 para aquele pane, enquanto todo outro pane segue funcionando
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.Add(Format('%s: record $%.4x kept opaque (%s)',
[D.SheetName, D.RecordId, D.Message]));
end;
end;
Como as seleções sobrevivem a inserções de linha e coluna?
Seleções sobrevivem a edições estruturais porque inserir ou apagar linhas ou colunas inteiras remapeia cada grupo de pane representado, nos motores clássico e XLSX, por um remapper compartilhado. As áreas que sobrevivem mantêm a ordem e a área ativa mantém a identidade. Se a área ativa é apagada, a primeira sucessora sobrevivente vira ativa, ou a última predecessora sobrevivente se nada vier depois. Se todas as áreas são apagadas, o grupo colapsa para uma célula no limite da exclusão, e 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. Os limites são deliberados. Grupos clássicos inválidos são pulados pelo remapper em vez de reescritos numa seleção inventada, então os bytes originais deles continuam fazendo round-trip. Editar um pane substitui só os registros daquele pane e deixa os outros byte-idênticos. ODS não recebe estado de seleção de pane nenhum, porque o ODF não tem estrutura equivalente de view de planilha para carregá-lo
Se sua aplicação grava arquivos .xls que os usuários abrem e precisam navegar, seja para revisar células marcadas, retomar de onde pararam ou compartilhar um dashboard congelado, a API de seleção e scroll consciente de pane faz parte do componente de planilha HotXLS para Delphi, e funciona do mesmo jeito para XLS e XLSX