HotXLS 通过 TXLSCalculator.GetCompiledFormulaR1C1 编译以 R1C1 引用表示法编写的公式,该方法接受像 R2C3(绝对引用)、R[-1]C[2](相对偏移引用)和 RC(当前单元格)这样的形式,将它们对照公式所在单元格的行 and 列转换为 A1 表示法,并将结果输送给处理普通 A1 公式相同的编译器;对于在数百行中生成相同公式的 Delphi 和 C++Builder 代码,这单个方法消除了整类的字符串拼接 bug
这种 bug 类对于任何以编程方式填充过一列的人来说都是熟悉的;您遍历行,并为每行使用 Format('D%d*E%d', [Row, Row]) 构建 A1 公式字符串;每次迭代都把行号拼接到文本中,而行号是唯一变化的部分;一旦搞错偏移量,将基于 0 的循环计数器与基于 1 的 A1 行号混淆,或者因标题行移动了数据块,那么列中的每个公式都会指向偏移一行;没有任何内容引发异常,数字纯粹是错的;您实际想表达的公式“乘我左侧的两个单元格”根本不需要提及行号,而 R1C1 表示法正允许您以该方式进行编写
什么是 R1C1 表示法,您应当何时使用它?
R1C1 表示法通过行号和列号(而不是列字母加行号)来寻址单元格,并将相对引用标记为来自公式单元格的显式偏移;R2C3 是位于第 2 行、第 3 列的绝对单元格,在 A1 中写为 $C$2;R[-1]C[2] 位于公式所在位置往上一行、往右两列;RC 是公式单元格本身;括号中的偏移量是关键:无论哪个单元格承载它,R1C1 中的相对引用读起来都是相同的,而同一引用的 A1 渲染图则随着每行而改变
该表示法恰好在一种场景中发挥作用,而且这是一种常见场景:模板生成(即相同的相对公式必须种植到数据区域的每一行中);在 A1 中,您必须按行重新渲染公式文本,而在 R1C1 中,文本是常量;这同样也更接近电子表格文件格式内部的想法:共享公式记录将相对引用存储为来自宿主单元格的行和列偏移量,因此按行的 A1 字符串不过是您的代码合成的内容,只为了让解析器将其重新分解为偏移量;R1C1 略过了这一往返过程;对于交互式的人类编写的公式,A1 仍然是自然的选择,这就是为什么它在 HotXLS 中到处都是默认设置的原因
HotXLS 如何编译一个 R1C1 公式?
HotXLS 在两个级别上暴露了该功能(在 v2.175.0 中添加);TXLSCalculator.GetCompiledFormulaR1C1(UncompiledFormula: String; SheetID, CurRow, CurCol: Integer): TXLSCompiledFormula 是大多数代码所调用的:它返回准备进行评估的编译后公式,与其 A1 孪生体 GetCompiledFormula 类似,但带有两个额外的参数命名公式所属单元格的基于 0 的行和列;在其下方, TXLSFormula.GetCompiledR1C1 产生原始的语法树,而独立函数 R1C1ToA1(const AFormula: String; CurRow, CurCol: Integer): String 执行实际的表示法转换;该流水线刻意保持简单:使用宿主单元格坐标将 R1C1 文本翻译为等效的 A1 文本,然后通过现有的引擎编译 A1 文本 —— 该引擎也解析定义名称和跨表引用,并派发自定义工作表函数
由于转换发生在编译之前,下游的一切表现就像您自己编写了 A1 公式一样;R1C1ToA1('R2C3', 4, 3) 返回 '$C$2'(与宿主单元格无关,因为两个坐标都是绝对的);R1C1ToA1('SUM(R[-3]C[0]:R[-1]C[0])', 4, 3) —— 这活在 D5 中的公式(因为 CurRow = 4 且 CurCol = 3 是基于 0 的)—— 返回 'SUM(D2:D4)':偏移量是对照第 5 行、D 列解析的,并作为普通的相对 A1 引用发出;范围不需要特殊处理,冒号会通过,每个端点都是独立转换的:
// The formula lives in D5: CurRow = 4, CurCol = 3 (both 0-based)
S := R1C1ToA1('R2C3', 4, 3);
// S = '$C$2' (absolute row and column)
S := R1C1ToA1('SUM(R[-3]C[0]:R[-1]C[0])', 4, 3);
// S = 'SUM(D2:D4)' (offsets resolved against D5)
S := R1C1ToA1('ROUND(R[-1]C[0], 2)', 4, 3);
// S = 'ROUND(D4, 2)' (the R in ROUND is left alone)
用一个相对公式填充一列
回报显现在循环中;将每次迭代都重新渲染公式文本的 A1 版本,与公式为常量且仅宿主坐标发生移动的 R1C1 版本进行比较;两者都通过 TXLSCalculator 编译并用 GetValue 评估,计算器在构造时接受一个单元格提供器回调,以便评估器可以从您的数据源中提取单元格值:
// A1 style: a different formula string for every row
for Row := 1 to 500 do
begin
FormulaText := Format('D%d*E%d', [Row + 1, Row + 1]); // 1-based A1 rows
Compiled := Calc.GetCompiledFormula(FormulaText, 0);
// ... evaluate, store, free ...
end;
const
AmountFormula = 'RC[-2]*RC[-1]'; // two cells to the left, same row
var
Calc: TXLSCalculator;
Compiled: TXLSCompiledFormula;
Value: Variant;
Row: Integer;
begin
Calc := TXLSCalculator.Create(nil, Provider.GetValue);
try
for Row := 1 to 500 do
begin
Compiled := Calc.GetCompiledFormulaR1C1(AmountFormula, 0, Row, 5);
try
if Calc.GetValue(0, Compiled, Row, 5, Value, 1) = lxOk then
StoreResult(Row, 5, Value);
finally
Compiled.Free;
end;
end;
finally
Calc.Free;
end;
end;
R1C1 循环在公式文本中根本没有行算术;'RC[-2]*RC[-1]' 意味着在第 2 行和第 500 行都表示“同屏的左侧两列,乘以同屏的左侧一列”,如果数据块后来被标题行移到了下方,公式常量并不会改变 —— 只有循环边界会变;A1 版本有两个地方会弄错 + 1 的调整,而 R1C1 版本一个也没有
转换器接受哪些 R1C1 形式?
R1C1ToA1 转换器识别 v2.175.0 记录的形式:用于任一方向相对偏移的 R[n]C[m]、绝对行与列的 RnCm、带负偏移的 R[-n]C[m]、用于公式单元格本身的裸 RC 以及诸如 R1C1:R3C3 或 R[-1]C:R[1]C 这样的范围;行和列部分是独立解析的,因此混合形式(如 R[1]C3 —— 相对行,绝对列)也能工作,且字母不区分大小写,因此 r[-1]c[2] 与其大写双胞胎编译出来的一样;括号部分变成无锚定(相对)的 A1 坐标,裸数字变成以 $ 锚定的绝对坐标
有趣的问题是转换器如何避免破坏公式中的其他一切,因为 R 和 C 都是常用字母;它的辨别规则是基于 Token 的:仅当 R 之前没有其他字母时,它才被认为是引用的开始,然后候选者必须完全解析 —— 可选的行部分、强制的 C、可选的列部分 —— 否则文本将保持原样恢复;这就是为什么 ROUND(R[-1]C[0], 2) 仅转换内部引用的原因: ROUND 中的 R 后面跟的是 O,而不是数字、括号或 C,因此解析失败,函数名称被逐字通过;相同的逻辑保护了 ROW(),且以 C 开头的函数名称根本不会作为候选,因为只有 R 才能启动引用;v2.175.0 的版本说明指出字符串字面量和标识符也会被跳过,即使这样,如果您公式中的带引号字面量碰巧包含形状酷似 R1C1 引用的文本,在生产环境中信任它之前,仍值得检查一次编译的输出
值得了解的坐标基准与锚定细节
这两种约定在该 API 内部交汇,理清它们可以避免唯一真正的陷阱; GetCompiledFormulaR1C1 的 CurRow 和 CurCol 参数是基于 0 的(遵循计算器 API),而表示法本身的数字则是基于 1 的(与 Excel 显示相匹配): R2C3 是第 2 行、第 3 列(即 $C$2,而不是 $D$3);如果您的循环计数器已经是基于 0 的,您可以直接将其作为 CurRow 传入, + 1 活在转换器内部,而不是您的代码中
如果编译后的公式寿命长于它所编译的单元格,那么还有一个细节重要;当您略去行或列部分时 —— RC[-1] 或 R[2]C 形式 —— 略去的坐标会解析为宿主单元格,并在转换后的 A1 文本中作为绝对的、以 $ 锚定的坐标发出;在评估时,这是不可见的,因为无论哪种方式值都是相同的;但是在相对与绝对锚定上,它决定了稍后插入或删除行或列时引用如何平移,这在关于公式引用调整的配套文章中有所涵盖;如果您需要一个坐标在结构编辑期间保持相对,请显式写出偏移量 —— 使用 R[0]C[-1] 而不是 RC[-1] —— 这样转换器就会发出无锚定的 A1 引用
平移后重新计算
引用调整重写了公式的*文本*,它并不重新计算结果;结构编辑后,存储在公式旁边的缓存值描述了旧的几何形状,因此可靠的顺序是:首先执行所有的插入和删除,然后触发一次重新计算,接着保存;在重新计算前运行平移也可以保持依赖信息的真实 —— 每个重写的引用命名了其实际的前驱,这正是增量重新计算引擎及其依赖图重新计算受影响单元格的最低集所需要的;如果重写的公式现在包含 #REF!,重新计算会立即显示错误值,而不是在文件中留下陈旧的数字
公式引用调整作为 XLSX 引擎的标准行为随适用于 Delphi 和 C++Builder 的 HotXLS Delphi Excel Component 一起提供,产品页面载有完整的工作表编辑 API 参考,包括此处展示的插入和删除方法