技术文章

HotXLS 中锚定条件格式的分区处理

HotXLS 是面向 Delphi 和 C++Builder 的 Excel 组件,当插入或删除行列将规则覆盖范围切分成需要不同相对公式锚点的片段时,它会自动把条件格式或数据验证规则拆分成两个或更多独立规则对象,然后为每条条件格式规则重新分配全新且唯一的优先级编号。这项行为随 XLSX 引擎 2.196 版本发布并自动运行,不提供退出设置。触发条件虽然具体却很常见:某个 cellIs 或 expression 规则的公式读取相对于自身范围的单元格,而该规则所在的工作表随后在这个准确范围的中间插入或删除了行

大多数 Excel 自动化文章都会止步于公式文本问题:调整每个 SUM() 和每个 VLOOKUP() 中的行列编号,让引用仍然指向正确单元格。这部分确实存在,HotXLS 在行列移动时如何重写公式引用的配套文章对此有详细说明,但条件格式或数据验证规则并不只是放在单元格中的公式。它会把公式与范围配对,用 ECMA-376 的术语来说就是 sqref,两者必须一起移动。当结构编辑把范围切成需要两个不同相对偏移才能保持正确的两部分时,保留一个带有一个公式字符串的规则对象就不再可行,而忽略这一点会让高亮规则悄悄开始比较错误的行

为什么插入行会拆分条件格式规则,而不是只移动它

条件格式或数据验证规则会为整个范围保留一个公式,并相对于单个锚点单元格进行计算,因此一旦某次编辑使该范围的两部分需要不同的相对偏移,一个公式就无法再正确描述两部分。ECMA-376 将规则覆盖范围表示为 conditionalFormattingdataValidation 元素上的 sqref 属性,Excel 会将 Formula1Formula2 视为输入该 sqref 左上角单元格后再填充到其余范围,就像普通相对公式向下填充一列。设想一个覆盖 B2:B50 的差异高亮,它会标记任何超过预算的实际数值,具体实现为 cellIs 规则,其 Formula1 是字面文本 C2,含义是将当前行的 B 单元格与同一行的 C 单元格比较

在第 25 行(旧行号)插入一行,把 HotXLS 在 Delphi 中覆盖 B2:B50 的条件格式拆为保留 Formula1 C2 的 B2:B25 和重定基准到 Formula1 C26 的 B26:B51
插入点上方的行保留原 C2 锚点,下移的行需要新的 C26 锚点,一个规则对象无法再同时服务两块区域
Idx := Sheet.AddConditionalFormat('B2:B50', xlsxCfOpGreaterThan, 'C2');
Sheet.ConditionalFormats[Idx].Style.SetFillBgColor($FFFFC7CE);

Sheet.InsertRows(25, 1);   // 一个空白分隔行,从原第 25 行开始

在旧第 25 行插入这一空白分隔行后,插入点上方的行不会移动,因此规则在这些行中的部分仍能正确将 Formula1 读取为 C2。原来第 25 行到第 50 行的内容会下移到第 26 行到第 51 行,对这些行来说,C2 已经完全不是正确单元格,因为第 26 行需要与 C26 比较,而不是与上方二十多行的预算数值比较

HotXLS 如何判断规则是否需要拆分

只有在几何关系确实要求时,HotXLS 才会创建额外规则对象:内部例程 XlsxBuildShiftedRuleParts 会遍历规则 sqref 中的每个不相邻区域,计算该区域在编辑前的锚点单元格以及编辑后的锚点单元格,并检查每个结果片段是否都需要相同的相对偏移修正。如果所有片段意见一致,就保留一个规则,将移动后片段的并集重建为其 sqref,并对公式重新锚定一次。只有片段意见不一致时才会真正拆分,正如上面的 B2:B50 示例,其中上方块保留原锚点,而下方块需要新锚点

重新锚定片段中的公式分为两步,这会复用 HotXLS 已经用于 OOXML 共享公式组的机制:首先,按照该片段自身左上角单元格作为原始锚点来转换公式,使用与跨范围展开共享公式相同的相对偏移计算方式,然后让结果经过同一个行列移动扫描器,由它重写普通工作表公式。这就是 Formula1 如何通过两次移动从 C2 变成 C26,而不是依赖手写的特殊情况:先将 C2 向前转换 23 行得到 C25,仿佛规则一直从那里开始,再让第 25 行的普通移动将它推到 C26。其他属性、填充颜色、stop-if-true 以及运算符本身都会原样带到新规则对象上,因此两半仍会以原来的颜色填充单元格

HotXLS 在 Delphi 中编辑触及条件格式或数据验证规则时的决策流程:就同一偏移修正达成一致的片段保留单条规则,不一致的片段拆分为各自重定基准的规则对象
XlsxBuildShiftedRuleParts 只在每个片段都需要相同偏移修正时保留单一规则;否则各片段经两步重新定基,各自成为独立的规则对象
// ConditionalFormats 现在持有两个规则而不是一个:
//   B2:B25    Formula1 = 'C2'    (插入点之上、上方的行)
//   B26:B51   Formula1 = 'C26'   (向下移动的行)

数据条和图标集也会像 cellIs 规则一样拆分吗

不会:HotXLS 只会分区处理正确性确实依赖每个区域相对公式的规则类型,也就是 cellIs 比较和 expression 规则;其他条件格式类型仍保持为单个规则对象,只需让其 sqref 扩展为覆盖移动片段的多区域并集。内部判断只是普通的 Kind 检查,即 cf.Kind in [cfkCellIs, cfkExpression],没有更复杂的逻辑。数据条、双色和三色刻度、图标集、前 N 项和后 N 项排名,以及重复值、空白值和错误值检测器,都携带描述整个覆盖范围的负载、条形颜色、刻度节点集合或图标族,而不是针对每个单元格的相对比较,因此把它们拆成多个具有优先级的规则对象不会带来正确性收益,只会增加需要管理的规则。当编辑切分它们的范围时,HotXLS 会把片段重新合并成一个带多区域 sqref 的规则,并将负载作为一个整体重新锚定,而不是为每个片段克隆新的规则对象。这种区别与条件格式和富文本基础文章中的规则类型分类一致:数据条、色阶和图标集本来就因完全忽略 Style 属性而与 cellIs 规则不同,现在事实证明,它们也因同一个根本原因而不同于按区域重新锚定

为什么结构编辑后规则优先级会改变

优先级会改变,是因为每个克隆对象起初都保留与其拆分来源规则完全相同的优先级值,而 HotXLS 随后会运行规范化过程,将由此产生的重复值整理为整洁且无间隙的顺序,而不是留下两个具有相同排名的规则。第二个内部例程 XlsxNormalizeConditionalFormatPriorities 会读取每条条件格式当前的优先级,对于从未显式设置优先级的规则则使用其在集合中的位置作为后备值,稳定地对整个列表排序以保留相同优先级项的原始相对顺序,然后将排序结果重新编号为连续的 1、2、3 序列,不留间隙也不重复。HotXLS 会在移动开始前运行一次,使克隆从干净基线开始;每次拆分以及每次移除空规则后还会再次运行,因此最终保存的文件不会出现两个规则项声明相同优先级。这一点很重要,如果你按照条件格式基础文章中的建议,在优先级值之间预留间隙,以便后续规则插入而不必重新编号其余规则,那么这些间隙会一直保留,直到下一次行列编辑触及该工作表,然后被合并,因为规范化只保证唯一性和稳定顺序,并不保证恢复原来的编号方案

HotXLS 在 Delphi 中拆分后规范化条件格式优先级:并列在优先级 3 上的克隆被稳定排序并重编号为紧密的 1、2、3 序列
克隆在父优先级上并列,归一化环节因此在保存工作簿前对规则做稳定排序并无间隔地重新编号

数据验证规则也会拆分,但没有优先级需要重新编号

数据验证规则会经过与 cellIs 和 expression 条件格式相同的范围分区逻辑,而且与条件格式不同,每种验证类型都会统一走这条路径:HotXLS 没有像数据条和图标集那样针对数据验证设置独立的非公式类别,因此普通列表或整数规则会通过与相对自定义公式相同的例程进行分区。区别在于优先级:ECMA-376 完全没有为 dataValidation 元素提供 priority 属性,因此验证规则不存在类似条件格式的重新编号步骤。设想一个自定义公式验证,它让每一行的实际金额不能超过旁边列中的该行预算

Sheet.AddCustomValidation('D2:D400', 'D2<=C2');
Sheet.DeleteRows(150, 5);   // 从验证范围内删除五行
// DataValidations 现在持有两个规则而不是一个:
//   D2:D149    Formula1 = 'D2<=C2'      (删除点之上、上方的行)
//   D150:D395  Formula1 = 'D150<=C150'  (向上移动的行)

这与数据验证基础文章提醒不要在行数确定前附加规则的原因相同:验证只覆盖你提供的字面单元格,后续结构编辑可能让原本由一条规则完成的工作变成由两个或更多规则完成。功能上不会出错,原范围中的每个单元格仍然由某条规则验证,但假定每列只有一个 DataValidations 条目的代码会在第一次编辑触及该列后开始错误索引。拆分能够达到的程度有明确上限:如果拆分会使工作表超过 65,534 条数据验证规则,HotXLS 会抛出异常,而不是写入 Excel 会静默拒绝的文件,这是库拒绝制造损坏工作簿的方式,而不是普通使用可能达到的限制

批量插入或删除后需要检查什么

脚本在一个充满条件格式和数据验证的工作表上批量执行行列编辑后,值得核对的两件事是规则总数和优先级顺序,因为两者都可能以代码审查时不易察觉、却会在有人打开 Excel 的管理规则界面时立即显现的方式发生漂移。一次编辑通常不会造成太大影响:在一个 cellIs 规则的中间插入一次,最多会将一个规则对象变成两个。风险会在报告生成例程逐行插入数据时叠加,如果工作表本来就带有多条公式锚定规则,每次处理都可能再次拆分前一次已经拆分的规则,五条原始 cellIs 规则最终可能变成其数倍的低价值片段,覆盖原范围的狭小区域。批量执行结构编辑,一次调用插入完整的新数据块,而不是一次插入一行,可以让规则数量由真正不同的锚点数量决定,而不是由执行的编辑次数决定

规则分区和优先级规范化作为 XLSX 引擎的标准行为,随用于 Delphi 和 C++Builder 的 HotXLS Delphi Excel Component 提供;产品页面包含完整的工作表编辑 API 参考,其中包括本文介绍的条件格式和数据验证方法