当您在 XLSX 工作表中插入或删除行或列时,HotXLS 会自动调整公式引用;引擎的 InsertRows、DeleteRows、InsertCols 和 DeleteCols 方法重写了每个幸存的公式,使其 A1 引用(包括相对、绝对和范围引用)在结构编辑后仍指向相同的数据,并且指向删除块的引用将变为 #REF!,与 Excel 的行为相匹配
这防范的 bug 是报告生成中最不易被察觉的之一;生成器在 C2:C9 中写入每日数据,并在下方写上 =SUM(C2:C9),接着后面的一步在顶部插入了一个标题行;如果引擎仅移动单元格值而让公式文本保持原样,该 SUM 仍读取 C2:C9,而数据现在活在 C3:C10 中 —— 导致总和默默丢掉了最后一天的数据并双重计算了标题行;没有任何内容引发异常,文件打开正常,而数字纯粹是错的;在 2.160 版本之前,HotXLS XLSX 引擎在结构编辑期间不会碰公式;自 2.160 起,重写是自动的,且没有需要设置的标志
在 Excel 中插入行时公式会发生什么?
Excel 的规则是引用跟随数据,而不是地址;当插入一行时,行索引位于或低于插入点的每个引用,都会向下移动所插入的行数,完全在插入点之上的引用则不受影响;删除行相反地执行相同的规则:删除块之下的引用向上滑动,而指向删除块本身的引用将变成 #REF!,因为它们命名的单元格已不复存在;列在另一个轴上表现得完全一样;如果电子表格库希望其输出在 Excel 用户使用时仍能存活,就必须精确地重现这些机制,因为用户是在不经思考的情况下,以这些术语来推导其公式的
让开发人员感到惊讶的部分是绝对引用也会发生移动;$B$2 中的 $ 锚点控制当公式被复制或填充到另一个单元格时会发生什么 —— 它们在结构编辑期间不起作用;在第 2 行上方插入一行,Excel 会将 $B$2 重写为 $B$3,美元符号保持完好,因为公式所依赖的值在物理上移动到了第 3 行;仅平移相对引用的引擎会损坏人们锚定得最刻意的公式;HotXLS 移动这两种形式并在重写的文本中保留 $ 标记
HotXLS 如何自动平移公式引用?
TXLSXWorksheet 上的所有四个结构编辑方法都委托给单个几何引擎:ShiftSheetGeometry(RowFrom, RowDelta, ColFrom, ColDelta); InsertRows(BeforeRow, Count) 以正的行 Delta 调用它, DeleteRows(StartRow, Count) 以负的 Delta 调用它,列方法在列轴上执行相同的操作;该例程首先重新定位单元格本身 —— 丢弃落在删除块内的任何单元格 —— 然后遍历每个幸存的公式单元格,并将其文本传递通过 XlsxAdjustFormulaRowColRefs(这是一个查找 A1 样式引用并对照平移量重写其行和列组件的扫描器);同一遍处理还会重新定位合并范围、超链接、批注、图像、图表、条件格式、数据验证和表格范围,使整个工作表作为单个单元进行移动
// Layout before the edit:
// C2..C9 daily figures
// C10 =SUM(C2:C9)
Sheet.InsertRows(2, 1); // one blank row before row 2
// Layout after the call:
// C3..C10 daily figures
// C11 =SUM(C3:C10) -- the range moved with the data
扫描器的几个细节值得了解;XlsxAdjustFormulaRowColRefs 识别所有四种锚定形式(A1、$A1、A$1、$A$1) 的单单元格引用以及两角范围(例如 A1:B3),并独立调整每个端点;文本已经以 #(来自较早编辑的错误标记)开头的公式将被跳过,而不是重新扫描;而且 InsertCols 增添了它自有的、与 Excel 平齐的细节:新插入的列继承其左侧邻居的宽度,这与 Excel 的“插入工作表列”命令所做的一样
删除的引用何时会变成 #REF!?
单行或单列索引的平移规则有三种结果;编辑点之前的索引保持不变;处于或超过编辑点的索引平移 Delta 量;在删除时,落在删除块内的索引没有意义的新值 —— 单元格已消失 —— 因此扫描器将整个引用重写为 #REF!;对于范围引用,两个端点都运行相同的规则,如果任一端点落在删除块内,引用将被重写为 #REF!,而不是留下一半有效
// A12 holds =A4+A6+A10
Sheet.DeleteRows(5, 3); // delete rows 5..7
// The formula, now in A9, reads =A4+#REF!+A7
// A4 : above the deleted block, unchanged
// A6 : inside rows 5..7, gone -> #REF!
// A10 : below the block, slides up -> A7
产生显眼的 #REF! 而不是静默地重新指向是正确的交易,这也是 Excel 所做出的决断;在其实际输入被删除后仍指向相邻单元格的公式,会返回看似合理的数字, #REF! 则会通过依赖公式传播,并在第一次冒烟测试中浮出水面;同样的转换也适用于列轴:
// E1 holds =B1*$C$1
Sheet.DeleteCols(3, 1); // remove column C
// The formula, now in D1, reads =B1*#REF!
// The absolute anchor did not protect $C$1 -- the cell itself is gone
哪些引用形式不会被重写?
扫描器针对的是带有显式列字母加行号形状的同表 A1 引用,值得明确什么是处于其外的;整列引用(例如 A:A)和整行引用(例如 1:1)缺少这两个组件之一,因此扫描器保留它们原样;结构化表格引用( Table1[Amount])也同样保持不动;重写也严格对 A1 表示法进行操作 —— 如果您的代码以 R1C1 样式构建公式,请在结构编辑前将它们转为 A1,这在关于 Delphi 中 R1C1 公式表示法的配套文章中进行了介绍
跨表和工作簿级构造是通过单独的处理运行的,而不是通过单元格文本扫描器;调整被编辑的工作表后,ShiftSheetGeometry 将相同的几何变化传播到引用了该编辑工作表的其他表上的公式、图表系列范围、内部超链接目标以及定义名称;定义名称在工作表生命周期级别得到了进一步的保护:自 2.150 版本起,删除工作表会将定义名称公式内部的每个 SheetN! 限定符重写为 #REF!,重命名工作表会将限定符重写为新名称,因此名称永远不会指向不再存在的工作表;关于名称和跨表公式是如何契合在一起的,在关于定义名称和跨表公式的文章中有所涵盖
平移后重新计算
引用调整重写了公式的文本,它并不重新计算结果;结构编辑后,存储在公式旁边的缓存值描述了旧的几何形状,因此可靠的顺序是:首先执行所有的插入和删除,然后触发一次重新计算,接着保存;在重新计算前运行平移也可以保持依赖信息的真实 —— 每个重写的引用命名了其实际的前驱,这正是增量重新计算引擎及其依赖图重新计算受影响单元格的最低集所需要的;如果重写的公式现在包含 #REF!,重新计算会立即显示错误值,而不是在文件中留下陈旧的数字
公式引用调整作为 XLSX 引擎的标准行为随适用于 Delphi 和 C++Builder 的 HotXLS Delphi Excel Component 一起提供,产品页面载有完整的工作表编辑 API 参考,包括此处展示的插入和删除方法