O HotXLS salva todo critério de AutoFilter BIFF8 num registro AUTOFILTER que carrega duas estruturas DOPER de 10 bytes, e o tipo do DOPER decide como o Excel compara. Desde a v2.384.45, o TXLSWorksheet.ApplyAutoFilter grava uma comparação como '>=100' como um DOPER de número IEEE, então o Excel casa células numéricas em vez de comparar texto. O relato de bug que motivou a mudança era curto e desesperador: uma exportação noturna aplicou um filtro numa coluna de valores, o arquivo abriu sem reclamar, a seta do dropdown mostrava o critério, e o filtro casou zero linhas. Nada estava corrompido. Os bytes eram BIFF8 válido, apenas o tipo errado de válido, e essa é a classe de falha que este artigo percorre, junto com dois erros antigos de nível de byte corrigidos na v2.384.18
O que um AutoFilter BIFF8 realmente armazena?
Um AutoFilter BIFF8 é um conjunto de três tipos de registro, não um só, e só o registro por campo guarda critérios. O AUTOFILTERINFO ($009D, [MS-XLS] §2.4.8) anota quantas colunas o intervalo do filtro cobre. O FILTERMODE ($009B) é um marcador sem corpo que o HotXLS emite só quando ao menos um campo tem critério ativo. Depois, cada campo ativo ganha seu próprio registro AUTOFILTER ($009E, §2.4.6): um índice de campo de base zero, uma palavra grbit cujos dois bits baixos são o wJoin, dois DOPERs de exatamente 10 bytes cada, e uma cauda opcional que guarda os caracteres de qualquer DOPER de string. O índice de campo é de base zero no disco embora o ApplyAutoFilter numere campos a partir de 1, o que importa na primeira vez que você for caçar um registro num hex dump. O primeiro byte de cada DOPER, o vt, diz que tipo de operando vem a seguir:
$04é um double IEEE 754 guardado nos 8 bytes restantes, que é como o Excel armazena uma comparação numérica$06é uma string cujo comprimento cabe num único bytecch, com os caracteres em si empurrados para a cauda do registro$08é um valor Bes, um Boolean ou código de erro empacotado em dois bytes$0Ce$0Enão carregam operando e significam casar todos os vazios e casar todos os não vazios
O segundo byte, o grbitSgn, guarda a comparação: 1 a 6 mapeiam para <, =, <=, >, <> e >=. O HotXLS mantém os dois bytes visíveis depois do fato por meio do AutoFilterColumns, cujos itens expõem Criteria1 e Criteria2 como objetos TXLSAutofilterDOPER com DataType, grbitSgn e Value, então você pode dar assert no que será gravado em vez de chutar
Por que um filtro '>=100' não casou nenhuma linha no Excel?
O filtro não casou nada porque o operando foi guardado como texto, e o Excel compara um DOPER de string contra a célula como texto. Antes da v2.384.45, o CreateFilterDoper no lxFilter.pas removia corretamente o prefixo >= e setava o sinal para 6, depois sempre montava um DOPER vtString com os caracteres 100. Uma célula numérica com 250 nunca satisfaz uma comparação de texto contra "100", então toda linha caiu fora. Sem exceção, sem diagnóstico, sem prompt de reparo do Excel. A regra desde a v2.384.45 é estreita de propósito: se o critério começa com um operador de comparação e o resto parseia como número sob regras de cultura invariante, o HotXLS grava um DOPER vtIEEENumber com o mesmo sinal. Um valor nu sem operador mantém a forma de string, porque é assim que o próprio Excel guarda um item escolhido na lista do dropdown
var
Wb: IXLSWorkbook;
Sh: TXLSWorksheet;
Doper: TXLSAutofilterDOPER;
begin
Wb := TXLSWorkbook.Create;
Sh := Wb.Sheets.Add;
Sh.Cells[1, 1].Value := 'Region';
Sh.Cells[1, 2].Value := 'Amount';
Sh.Cells[2, 1].Value := 'North';
Sh.Cells[2, 2].Value := 250;
// Campo 2 = segunda coluna de A1:B100 (base 1 do lado da API)
Sh.ApplyAutoFilter('A1:B100', 2, '>=100');
Doper := Sh.AutoFilterColumns.Find(2).Criteria1;
// v2.384.45+: DataType = 4 (número IEEE), grbitSgn = 6 (>=)
// Antes do fix: DataType = 6 (string), que não casava nada
Assert(Doper.DataType = 4);
Wb.SaveAs('orders.xls');
end;
É no parse que moram as arestas restantes. O operando passa pelo TryStrToFloat com ponto como separador decimal, então '>=1.5' vira número e '>=1,5' continua um DOPER de string e de novo não casa nada em silêncio, diga o que disser o locale do Windows. Datas são a mesma armadilha em fantasia diferente: '>=2026-01-01' não é número, então é gravado como texto, enquanto o Excel guarda células de data como números de série. Para igualdade numérica, tanto '=100' quanto um Variant numérico como 100 produzem um DOPER IEEE com sinal 2, enquanto a string pura '100' produz uma comparação de texto. Monte operandos numéricos em código em vez de formatá-los para humanos:
var
Fmt: TFormatSettings;
Since: TDateTime;
begin
Fmt := TFormatSettings.Create;
Fmt.DecimalSeparator := '.';
// Limiar com fração: sempre formate com ponto
Sh.ApplyAutoFilter('A1:D500', 3, '>' + FloatToStr(1499.5, Fmt));
// Datas: compare contra o número de série que o Excel guarda na célula.
// Um TDateTime do Delphi equivale ao serial do sistema 1900 para datas após março de 1900
Since := EncodeDate(2026, 1, 1);
Sh.AutoFilterColumns.SetFieldCriteria(4, '>=' + IntToStr(Trunc(Since)),
xlAnd, Unassigned);
end;
Como AND e OR juntam duas condições?
Os bits wJoin do grbit do AUTOFILTER são 0 para AND e 1 para OR, e o HotXLS tinha essas duas constantes invertidas até a v2.384.18. Um filtro estilo between como pelo menos 100 e abaixo de 500 era salvo como pelo menos 100 ou abaixo de 500, o que na prática casa todo número e parece que o filtro simplesmente não foi aplicado. As constantes públicas de operador adicionam um segundo perigo de portabilidade. No HotXLS, xlAnd é 0 e xlOr é 1, ao passo que a automação do Excel os numera 1 e 2. XlAutoFilterOperator é um Byte comum, então código traduzido de uma macro VBA com números literais compila limpo, e um literal 1 que significava AND em COM agora significa OR. Use as constantes nomeadas e o problema não pode surgir:
// Valor entre 100 (inclusive) e 500 (exclusive)
Sh.ApplyAutoFilter('A1:D500', 3, '>=100', xlAnd, '<500');
with Sh.AutoFilterColumns.Find(3) do
begin
Assert(Operator = xlAnd); // wJoin = 0 no disco
Assert(Criteria2.grbitSgn = 1); // 1 = menor que
end;
Booleans, vazios e o teto de 255 caracteres
Um critério Boolean é guardado como um valor Bes ([MS-XLS] §2.5.10), e o Bes põe o byte de valor bBoolErr primeiro e a flag fError depois. O HotXLS os gravava na ordem contrária antes da v2.384.18, então um filtro para TRUE punha 1 na flag de erro e o Excel lia o critério como um código de erro. Writer e reader estavam trocados juntos, e é por isso que o HotXLS fazia round-trip dos próprios arquivos sem reclamar enquanto o Excel discordava, um lembrete de que um round-trip autoconsistente não prova nada sobre conformidade com a spec. Vazios não precisam de operando algum: passar '=' sozinho produz um DOPER que casa todos os vazios ($0C) e '<>' sozinho um DOPER que casa todos os não vazios ($0E)
Critérios de string esbarram num limite duro no layout do DOPER. O campo de comprimento cch é um único byte, então um operando de string não pode passar de 255 caracteres, e o CreateFilterDoper trunca texto mais longo depois de remover o operador em vez de deixar o byte de comprimento dar a volta e dessincronizar a cauda do registro. O truncamento é silencioso, e um filtro numa coluna de descrição longa pode casar diferente do texto completo que você passou. No BIFF8 a cauda guarda cada string como uma flag de um byte seguida de unidades de código UTF-16, e o tamanho declarado do registro precisa contar esses bytes exatamente, a mesma disciplina de contabilidade coberta em como declarações de comprimento de registro BIFF derivam num writer XLS em Delphi
Por que uma segunda chamada de ApplyAutoFilter apaga a primeira?
Cada chamada de ApplyAutoFilter redefine o intervalo de filtro inteiro, então só o critério da última chamada sobrevive. Internamente ela chama o SetAutoFilter, que limpa todo campo antes de reconstruir o intervalo, o que é correto para uma coluna e surpreendente para duas. Para filtrar várias colunas, chame o ApplyAutoFilter uma vez para estabelecer o intervalo e o primeiro critério, depois adicione os demais por meio do AutoFilterColumns.SetFieldCriteria, que deixa o intervalo e os outros campos em paz. Os dois caminhos ignoram um número de campo fora do intervalo sem lançar exceção, então verifique lendo de volta, de preferência depois de reabrir o arquivo salvo:
Sh.ApplyAutoFilter('A1:D500', 1, 'North'); // intervalo + campo 1
Sh.AutoFilterColumns.SetFieldCriteria(3, '>=100', xlAnd, Unassigned);
Sh.AutoFilterColumns.SetFieldCriteria(4, True, xlAnd, Unassigned);
Wb.SaveAs('orders.xls');
Wb := TXLSWorkbook.Create;
Wb.Open('orders.xls');
Assert(Wb.Sheets[1].AutoFilterColumns.Find(1).Active);
Assert(Wb.Sheets[1].AutoFilterColumns.Find(3).Criteria1.DataType = 4);
Tenha em mente que o registro AUTOFILTER é uma definição armazenada: o HotXLS grava os critérios e não os avalia na planilha XLS clássica, então um pipeline que precisa das linhas correspondentes no servidor tem que computá-las por conta própria lá, enquanto a fachada XLSX oferece avaliação em nível de linha como mostrado em validação de dados, AutoFilter e tabelas do HotXLS em Delphi. Quando o Excel de fato oculta linhas, qualquer total abaixo do intervalo depende de como SUBTOTAL e AGGREGATE tratam linhas ocultas e filtradas, que é o próximo lugar onde um filtro numérico que silenciosamente não casa nada aparece como um número errado
O HotXLS lê e escreve pastas de trabalho BIFF8 XLS e XLSX nativamente do Delphi e do C++Builder, incluindo critérios de AutoFilter com DOPERs numéricos, Boolean e AND/OR que o Excel avalia como esperado. Veja o componente de planilha HotXLS para Delphi para recursos, edições e download da versão trial