HotXLS 把每一条 BIFF8 AutoFilter 条件都保存为携带两个 10 字节 DOPER 结构的 AUTOFILTER 记录,而 DOPER 的类型决定了 Excel 怎么做比较。从 v2.384.45 起,TXLSWorksheet.ApplyAutoFilter 会把 '>=100' 这类比较写成 IEEE 数字型 DOPER,于是 Excel 按数值匹配单元格,而不是拿文本来比。促成这次修改的 bug 报告又短又气人:一次夜间导出在金额列上加了筛选,文件打开毫无抱怨,下拉箭头上看得见条件,筛选结果却是零行。文件没有坏。字节是完全合法的 BIFF8,只不过是「错误的那种合法」——这正是本文要剖析的一类失败,连同 v2.384.18 修掉的两个更早的字节级错误一起
BIFF8 AutoFilter 到底存储了什么?
BIFF8 AutoFilter 不是一条记录,而是三种记录类型的组合,其中只有按字段的记录携带条件。AUTOFILTERINFO($009D,[MS-XLS] §2.4.8)记录筛选范围覆盖多少列。FILTERMODE($009B)是个无载荷的标记,HotXLS 只在至少一个字段有生效条件时才发出它。然后每个生效字段都有自己的 AUTOFILTER 记录($009E,§2.4.6):一个从零起算的字段索引,一个低两位是 wJoin 的 grbit 字,两个各恰好 10 字节的 DOPER,以及一个可选的尾部,存放字符串型 DOPER 的字符。字段索引在磁盘上从零起算,而 ApplyAutoFilter 的字段编号从 1 开始——第一次在十六进制转储里找记录时,这一点就会找上你。每个 DOPER 的第一个字节 vt 说明后面跟着什么类型的操作数:
$04是 IEEE 754 双精度浮点,存放在剩余 8 字节里,Excel 的数值比较就是这样存的$06是字符串,长度放在单个cch字节里,字符本身则推进记录尾部$08是 Bes 值,一个打包进两个字节的布尔值或错误码$0C和$0E不带操作数,分别表示匹配所有空单元格和匹配所有非空单元格
第二个字节 grbitSgn 存放比较符:1 到 6 分别对应 <、=、<=、>、<> 和 >=。HotXLS 让这两个字节事后仍然可见:AutoFilterColumns 的条目把 Criteria1 和 Criteria2 暴露为 TXLSAutofilterDOPER 对象,带 DataType、grbitSgn 和 Value,你可以对将要写入的内容直接做断言,而不是靠猜
为什么 '>=100' 筛选在 Excel 里一行都匹配不到?
筛选之所以一行都匹配不到,是因为操作数被存成了文本,而 Excel 拿字符串型 DOPER 按文本和单元格比较。v2.384.45 之前,lxFilter.pas 里的 CreateFilterDoper 正确地剥掉了 >= 前缀、把符号位设为 6,然后总是构建一个装着字符 100 的 vtString DOPER。存着 250 的数字单元格永远不会满足与 "100" 的文本比较,于是每一行都被筛掉。没有异常,没有诊断,Excel 也不弹修复提示。v2.384.45 起的规则刻意收得很窄:条件以比较运算符开头、且余下部分按 invariant-culture 规则能解析成数字时,HotXLS 才写出同符号位的 vtIEEENumber DOPER。不带运算符的裸值仍保留字符串形式,因为 Excel 自己从下拉列表里选中一项时也是这么存的
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;
// 字段 2 = A1:B100 的第二列(API 侧从 1 起算)
Sh.ApplyAutoFilter('A1:B100', 2, '>=100');
Doper := Sh.AutoFilterColumns.Find(2).Criteria1;
// v2.384.45+:DataType = 4(IEEE 数字),grbitSgn = 6(>=)
// 修复前:DataType = 6(字符串),什么也匹配不到
Assert(Doper.DataType = 4);
Wb.SaveAs('orders.xls');
end;
解析环节还埋着剩下的几处尖刺。操作数经 TryStrToFloat 解析,小数分隔符固定为句点,所以 '>=1.5' 会变成数字,而 '>=1,5' 仍是字符串 DOPER,再次静默地匹配不到任何东西,Windows 区域设置说什么都没用。日期是同一陷阱换了身戏服:'>=2026-01-01' 不是数字,会被写成文本,可 Excel 存日期单元格用的是序列号。按数字判等时,'=100' 和 100 这类数字 Variant 都产出符号位为 2 的 IEEE DOPER,而裸字符串 '100' 产出的是文本匹配。数字操作数请在代码里构造,别按给人看的格式来拼:
var
Fmt: TFormatSettings;
Since: TDateTime;
begin
Fmt := TFormatSettings.Create;
Fmt.DecimalSeparator := '.';
// 带小数的阈值:永远用句点格式化
Sh.ApplyAutoFilter('A1:D500', 3, '>' + FloatToStr(1499.5, Fmt));
// 日期:与 Excel 存进单元格的序列号比较
// 1900 年 3 月之后的日期,Delphi TDateTime 等于 1900 日期系统的序列号
Since := EncodeDate(2026, 1, 1);
Sh.AutoFilterColumns.SetFieldCriteria(4, '>=' + IntToStr(Trunc(Since)),
xlAnd, Unassigned);
end;
AND 和 OR 是怎么连接两个条件的?
AUTOFILTER grbit 的 wJoin 位为 0 表示 AND、为 1 表示 OR,而 HotXLS 直到 v2.384.18 才把这两个常量搞对。在那之前,「至少 100 且低于 500」这类介于两者之间的筛选保存出去会变成「至少 100 或低于 500」,实际效果是每个数字都匹配,看起来就像筛选根本没生效。公开的运算符常量还埋着第二个移植陷阱:HotXLS 里 xlAnd 是 0、xlOr 是 1,而 Excel automation 里两者编号是 1 和 2。XlAutoFilterOperator 只是个普通的 Byte,所以从 VBA 宏翻译过来、带字面数字的代码照样编译通过,COM 里表示 AND 的字面量 1 到这儿就变成了 OR。用命名常量,这个问题就无从产生:
// 金额介于 100(含)与 500(不含)之间
Sh.ApplyAutoFilter('A1:D500', 3, '>=100', xlAnd, '<500');
with Sh.AutoFilterColumns.Find(3) do
begin
Assert(Operator = xlAnd); // 磁盘上 wJoin = 0
Assert(Criteria2.grbitSgn = 1); // 1 = 小于
end;
布尔值、空单元格与 255 字符上限
布尔条件以 Bes 值存储([MS-XLS] §2.5.10),Bes 把值字节 bBoolErr 放在前、fError 标志放在后。v2.384.18 之前 HotXLS 写的顺序正好相反,于是筛 TRUE 会把 1 写进错误标志,Excel 把这个条件读成错误码。写入方和读取方是成对换的,所以 HotXLS 对自家文件的往返毫无怨言,Excel 却不认——这提醒我们:自洽的往返证明不了符合规范。空单元格完全不需要操作数:单传一个 '=' 产出匹配全部空单元格的 DOPER($0C),单传 '<>' 产出匹配全部非空单元格的 DOPER($0E)
字符串条件在 DOPER 布局里撞上一道硬限制。cch 长度字段只有一个字节,字符串操作数因此不能超过 255 字符,剥掉运算符之后更长的文本会被 CreateFilterDoper 截断,而不是任由长度字节回绕、把记录尾部带偏。截断是静默的,对长描述列的筛选行为可能和你传入的全文不一致。BIFF8 的尾部把每个字符串存成单字节标志加 UTF-16 code unit,声明的记录长度必须把这些字节算准——这正是Delphi XLS 写入器中 BIFF 记录长度声明的漂移讲过的同一门记账功夫
为什么第二次调用 ApplyAutoFilter 会抹掉第一次?
每次调用 ApplyAutoFilter 都会重新定义整个筛选范围,所以只有最后一次调用的条件留得下来。它内部调用 SetAutoFilter,后者在重建范围前清空每个字段——这对单列来说正确,对两列来说出人意料。想筛多列,先调用一次 ApplyAutoFilter 建立范围和第一个条件,再通过 AutoFilterColumns.SetFieldCriteria 追加其余条件,它不动范围也不动其他字段。两条路径对超出范围的字段号都是静默忽略、不抛异常,所以请读回来核对,最好在重新打开保存后的文件之后:
Sh.ApplyAutoFilter('A1:D500', 1, 'North'); // 范围 + 字段 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);
记住 AUTOFILTER 记录只是一份存储下来的定义:HotXLS 写入条件,但不会在经典 XLS 工作表上执行它们,需要在服务器端拿到匹配行的流水线得自己在那里算,而 XLSX 门面提供行级求值,参见Delphi 中的 HotXLS 数据验证、AutoFilter 与表格。等 Excel 真把行藏起来,范围下方的合计就取决于SUBTOTAL 和 AGGREGATE 如何对待隐藏行与筛选行——一个静默匹配不到任何行的数字筛选,下一个现形的地方往往就是一处错误的合计数
HotXLS 在 Delphi 和 C++Builder 上原生读写 BIFF8 XLS 与 XLSX 工作簿,包括数字、布尔和 AND/OR 型 DOPER 的 AutoFilter 条件,Excel 都能按预期求值。功能、版本与试用下载请见HotXLS Delphi 电子表格组件