技术文章

Delphi 里不开 Excel 执行 Excel AutoFilter:HotXLS

HotXLS 不启动 Excel 就能求值 XLSX 文件里已保存的 AutoFilter 条件。自 v2.384.136 起,TXLSXWorksheet.PreviewAutoFilterRows、ApplyAutoFilter 和 AutoFilterRowVisible 能执行值列表、日期分组、Top/Bottom-n 与百分比筛选、相对日期与平均值筛选、填充与字体颜色筛选以及条件格式图标筛选,全程在冻结的参考日期与显式资源限额之下

常见触发场景是:某人在 Excel 里筛过一份工作簿,而服务器得对它做点什么。有人设了"按营收 Top 10"、订购日期上的"本月",再给逾期行加红色填充,保存、上传。你的 Delphi 服务现在需要 Excel 会显示的那批行:数一数、导出去,或把正确的行隐藏后写回文件。文件里只有定义。在这个版本之前,HotXLS 能求值普通自定义条件,但高级种类躺在存储的 XML 里没人执行,诚实的回答是"去 Excel 里打开"

HotXLS 真正执行已保存 AutoFilter 的哪些部分?

HotXLS 对每列执行一种筛选,取自 Excel 写下的 filterColumn 元素,列与列之间用 AND 组合,与 Excel 的下拉完全一致。这些元素来自 ECMA-376 Part 1 §18.3.2 的 AutoFilter 组,每一种映射到一条独立的求值路径:

HotXLS 在 Delphi 里执行 AutoFilter 时,把每个已保存的 filterColumn 元素分派到五条求值路径之一——filters、customFilters、top10、dynamicFilter、colorFilter 或 iconFilter——其余一律抛 EXLSXAutoFilterUnsupported
HotXLS 从 ECMA-376 Part 1 的 AutoFilter 组按列求值一种筛选、以 AND 组合各列,可见行集合因此与 Excel 下拉会显示的一致
  • 带 filter 和 dateGroupItem 子元素的 filters:勾选的值列表、可选的空白项、从年到秒的日历日期分组,以 OR 相连
  • customFilters:一或两个比较条件,以 AND 或 OR 连接,早已存在的老路径
  • top10:Top 或 Bottom n 项,或 n 百分比,val 限 1-500 项或 1-100 百分比
  • dynamicFilter:今天、昨天、明天、本周/上周/下周、本月/上月/下月、本季/上季/下季、今年/去年/明年、年初至今、M1-M12、Q1-Q4、高于平均与低于平均
  • colorFilter 和 iconFilter:经 differential style 解析出的有效填充或字体颜色,以及按图标集与从零起计 icon id 的条件格式图标

清单之外的任何东西都以 EXLSXAutoFilterUnsupported 失败,而不是交出一套猜出来的行。ST_DynamicFilterType 里 above 和 below 标准差类型、非格里高利历的 calendarType 值、未知扩展元素、解析不了的 dxfId 引用、以及不知怎么同时带两种筛选的列,都算在内。一个悄悄返回"差不多"行集合的筛选引擎,比一个拒绝的更糟——没人会注意到,直到一份带错客户的报表发出去

先预览,后应用

HotXLS 把工作拆成只读预览和事务式应用,两者收同一份 TXLSXAutoFilterExecutionOptions 记录。PreviewAutoFilterRows 返回匹配的数据行数,绝不碰行可见性。ApplyAutoFilter(AResetVisible, Options) 先备好每一行的决定,然后一步发布新的隐藏行集合,准备期间抛异常的话工作表保持原样

var
  Book: TXLSXWorkbook;
  Sheet: TXLSXWorksheet;
  Options: TXLSXAutoFilterExecutionOptions;
  Matching: Integer;
begin
  Book := TXLSXWorkbook.Create;
  try
    Book.Open('orders-filtered-by-sales.xlsx');
    Sheet := Book.Sheets[1];   // Sheets 索引器从 1 起计

    // 永远从默认值出发,再改单个字段
    Options := XLSDefaultAutoFilterExecutionOptions;
    Options.ReferenceDate := EncodeDate(2026, 10, 19);

    Matching := Sheet.PreviewAutoFilterRows(nil, Options);  // 只计数
    WriteLn('Rows Excel would show: ', Matching);

    // True:匹配行变可见,哪怕之前被手动隐藏
    Sheet.ApplyAutoFilter(True, Options);
    Book.SaveAs('orders-applied.xlsx');
  finally
    Book.Free;
  end;
end;

每次都用 XLSDefaultAutoFilterExecutionOptions 初始化记录。该函数先把记录清零,再设好每个限额,执行上下文遇到限额不全为正的记录会抛 EXLSXAutoFilterLimit。栈上声明、逐字段填的记录,你忘了的那些字段通常装着垃圾或零。无参重载(ApplyAutoFilter(True)、PreviewAutoFilterRows(Callback)、AutoFilterRowVisible(Row))用同一套默认值,参考日期取当前日期

AResetVisible 标志决定通过了筛选、但之前就被隐藏的行的命运。True 让每个匹配行可见,与 Excel 重新应用时的做法一致。False 保留匹配行上早先的手动隐藏,只为未通过的行追加隐藏。AutoFilter 范围之外的行无论怎样都保持原状。准备与发布之间工作簿若被改动,ApplyAutoFilter 会抛异常,而不是发布基于过期数据算出的决定

为什么值列表无视星号和问号?

filters 元素里的值列表条目是字面的显示 token,所以 HotXLS 把 * 和 ? 当普通字符比较。下拉里勾选 A* 意味着单元格显示的正是 A*,不是"以 A 开头"。通配符属于 customFilters——那里的"开头是"和"包含"才真是模式——并保留 HotXLS 如何匹配 Excel 通配符条件与波浪号转义覆盖的那套单独规则。把两种语义搞混,是把零件号列筛成乱码的最快办法

比较对象是显示值,于是就有一个问题:哪个显示。HotXLS 用单元格的基础数字格式(含存储的样式叠加)格式化,刻意无视条件格式叠加在其上的数字格式。HotXLS 测试夹具里的原生 Excel 控件说明了这个 token 为什么要紧:数字 1 和文本 "1" 都能匹配勾选的 1;格式化为 0.0 的数字 1 则与文本 "1.0" 一起匹配 1.0;文本 "01" 与两者都不同。长得像日期的文本不会被解析成日期,所以装着 "2026-10-19" 的文本单元格不会落进日期分组

Top 10、平均值与动态日期筛选怎么判?

HotXLS 对整个源列只算一次排名与平均截断,不是对其他筛选留下的可见行算。只有有限数字进入总体——看着像数字的文本如 "100" 被排除——且总体在任何行决定之前收集。原生控件工作簿里,above-average 筛选在第二列把值 50 藏起来之后,仍保持全列均值 27;不会切到剩余行的 21.25

截断处并列的项全部入选,与 Excel 一致。对两个值并列 20 的数据做 Top 3 筛选,选出四行。百分比模式取数字总体的截断份额,下限一项,所以五个值的 Top 50 percent 选出两个值、阈值 30。Bottom 变体镜像比较方向。这些数字来自 HotXLS 测试夹具里记录的 Excel 自身行集合,不是读规范读来的——规范对其中大多数保持沉默

HotXLS 在 Delphi 里对整个源列一次算出 Excel AutoFilter 的 Top 10、百分比与高于平均截断,总体保留隐藏值,截断处并列的项进结果
above-average 保持全列均值 27 而不是剩余行的 21.25;两个值并列 20 时 Top 3 筛选选出四行

动态日期需要冻结的参考日期

TXLSXAutoFilterExecutionOptions.ReferenceDate 为整个操作冻结"今天"的含义。本周、上季这类相对筛选,以及给颜色筛选供料的条件格式日期规则,全都对着这一个日期求值,于是 23:59:59 的预览与一秒后的应用一致。零值表示操作开始时取一次当前日期;序列值遵循工作簿的 1900 或 1904 日期系统,包括 1900 系统里 Excel 虚构的序列 60——它影响 M2 和 Q1 筛选

窗口是半开的日历区间:本周从周日到下一个周日,本季从季首日到下一季首日,年初至今从 1 月 1 日到参考日结束。凡在批处理或测试里跑的东西,都显式钉住 ReferenceDate。跨午夜跑的夜间任务、或十月在写却要在一月也通过的单元测试,否则会给出毫无可见理由的不同行集合

HotXLS 在 Delphi 里对着一个冻结的 TXLSXAutoFilterExecutionOptions.ReferenceDate 求值 Excel dynamicFilter 相对日期规则,本周、季度与年初至今窗口均为半开区间
冻结参考日期让 23:59:59 的预览与一秒后的应用一致,也钉住了影响 M2 与 Q1 筛选的 1900 系统序列 60 怪癖

公式单元格按其缓存值筛选。筛选引擎不会悄悄重算工作簿,所以写出方没留缓存结果的文件,需要先显式算一遍,筛选才能看到有意义的数字或日期

颜色筛选看得到哪些颜色?

HotXLS 的颜色筛选看到的是直接单元格格式加上匹配的条件格式覆盖得出的有效颜色,仅来自表格样式的颜色一概看不见。Excel 的行为相同:内置表格样式涂成浅蓝的镶边行,在筛选下拉里不算"浅蓝"。原生控件里,被条件格式涂红的单元格会被红色填充筛选选中;直接格式化为红、又被生效的黄色规则覆盖的单元格被排除

三个细化随后续版本到来。v2.384.138 把线性和路径渐变填充与纯色、无填充都区分开,渐变永远不会匹配纯色条件,哪怕它某个色标恰好是那个颜色。v2.384.140 加了富文本的字体颜色筛选:任一非空 run 带有所要求的 RGB、automatic 或继承颜色即匹配。v2.384.150 补全了条件富文本判定——结果发现那才是怪的那一个

原生 Excel 会在富文本单元格与同文本、同基础样式、同匹配条件规则路径的普通单元格之间复用颜色判定,所以可见行集合可以取决于这些单元格的顺序。HotXLS 复刻这一点的方式是按行升序准备整个数据范围——哪怕你只对一行问 AutoFilterRowVisible。因此检查第 40 行要付出一次范围遍历的成本,换来的是行检查、预览与应用对同一行永远给出同一答案

每种资源都有上限,以及该接住什么

HotXLS 用 TXLSXAutoFilterExecutionOptions 里各自的字段给 AutoFilter 操作的每一类工作设界,因为上传表单来的筛选过的工作簿是不可信输入。默认值为:MaxXmlCharacters 1048576、MaxXmlNodes 16384、MaxFilterItems 4096、MaxExaminedCells 4000000、MaxEffectiveFormatCells 与 MaxPopulationValues 各 1000000、MaxContextBytes 67108864 字节、MaxConditionalSteps 16000000。MaxContextBytes 量的是保留的逻辑状态与预留的临时封装,不是进程内存

type
  TFilterJob = class
  public
    Deadline: TDateTime;
    VisibleRows: TList<Integer>;
    procedure HandleRow(Sender: TObject; SheetIndex, Row: Integer;
      Visible: Boolean; var Cancel: Boolean);
    procedure HandleWork(Sender: TObject; ACompleted: Int64;
      var ACancel: Boolean);
  end;

procedure TFilterJob.HandleRow(Sender: TObject; SheetIndex, Row: Integer;
  Visible: Boolean; var Cancel: Boolean);
begin
  // 每个数据行都会调用;Visible 是筛选决定
  if Visible then
    VisibleRows.Add(Row);
end;

procedure TFilterJob.HandleWork(Sender: TObject; ACompleted: Int64;
  var ACancel: Boolean);
begin
  // ACompleted 计已检查单元格,格式化期间可能停滞
  ACancel := Now > Deadline;
end;

PreviewAutoFilterRows 的行回调收到每个数据行及其决定,不只是匹配行,函数仍返回匹配计数。把它的 Cancel 参数置位即可提前结束访问,不抛异常。OnWork 是取消钩子:ACancel 置 True 抛 EXLSXAutoFilterCancelled。操作准备决定期间持有工作簿的读租约,两个回调都不许改那个工作簿

Options := XLSDefaultAutoFilterExecutionOptions;
Options.ReferenceDate := Date;
Options.MaxExaminedCells := 500000;   // 上传文件用更紧的上限
Options.OnWork := Job.HandleWork;
try
  Sheet.PreviewAutoFilterRows(Job.HandleRow, Options);
  Sheet.ApplyAutoFilter(True, Options);
except
  on E: EXLSXAutoFilterUnsupported do
    Log('Filter needs Excel: ' + E.Message);
  on E: EXLSXAutoFilterLimit do
    Log('Filter exceeded a budget: ' + E.Message);
  on E: EXLSXAutoFilterCancelled do
    Log('Filter cancelled at deadline');
  on E: EXLSXAutoFilterExecution do
    Log('Filter state invalid: ' + E.Message);
end;

三个具体异常都派生自 EXLSXAutoFilterExecution,它本身覆盖无效状态——参考日期超出支持的日历、工作簿在发布前被改动等——所以基类要放最后。任何情况下工作表的行可见性都不变,这让"记日志、跳过、把文件标记待人工复核"成为安全的默认策略

边界在哪

覆盖面很广,但在向客户承诺对齐之前,有几条边值得先知道:

  • 标准差类动态筛选被拒绝,直到有经过验证的策略为止——尽管枚举里为它们留了值
  • 周窗口从周日开始;依赖其他周起始日的工作簿不在建模范围
  • 公式结果只来自存储的缓存,筛选调用内部不重算
  • 表格样式的颜色永远不匹配颜色筛选,这是设计使然,也与 Excel 一致
  • 未知的富文本结构或未知颜色元数据显式失败;字体族这类已知的非颜色属性不阻碍颜色匹配
  • XLS 门面及其 BIFF8 存储是另一条路径,见 HotXLS 如何写 BIFF8 AutoFilter DOPER 条件

如果你还在琢磨自己生成的工作簿里 AutoFilter 定义、数据验证和表格如何相处,从 HotXLS 的数据验证、AutoFilter 与表格开始,它讲了 SetAutoFilter、列 id 以及筛选何时真正生效

FAQ

PreviewAutoFilterRows 会改变哪些行被隐藏吗?

不会。PreviewAutoFilterRows 只求值存储的条件,经返回值和可选回调报告决定。只有 ApplyAutoFilter 发布新的隐藏行集合,且在全部决定备好之后,失败时原可见性保持不变

为什么相对日期筛选在不同日子给出不同的行?

今天、本周、上月这类筛选按定义相对当前日期,与 Excel 完全一致。需要可复现结果时,把 TXLSXAutoFilterExecutionOptions 里的 ReferenceDate 设成固定日期;零值表示操作开始时的日期

HotXLS 能按表格样式来的颜色筛选吗?

不能,Excel 也不能。HotXLS 颜色筛选只匹配直接单元格格式与受支持的条件格式覆盖。仅由内置或自定义表格样式涂上的颜色不参与,所以颜色筛选的结果与 Excel 下拉会选中的行一致

高级 AutoFilter 执行随 HotXLS,无需 Excel 读写 Excel 文件的 Delphi 表格组件的 XLSX 工作表 API 自 v2.384.136 起发布,渐变、富文本与条件富文本颜色处理到 v2.384.150 补齐