HotXLS 现在能求值结构化表引用,因此 =SUM(Table1[Amount]) 会得出一个数字,而不是被跳过。解析器支持 Table[Column]、Table[[Column]]、像 Table[[Q1]:[Q4]] 这样的跨列区间,以及 [#Data]、[#All]、[#Headers] 和 [#Totals] 这些项目说明符,在解析阶段就对照工作簿的表模型逐一解析,而原始公式文本会原样往返保留
有一种写法是被刻意排除在外的,也恰恰是人们最先碰到的那种。当前行简写形式 [@Column] 不受支持,背后有一个值得理解、而不是盲目绕过去的结构性原因
为什么结构化引用不只是一个带友好名字的区域?
因为已定义名称会冻结一个地址,而表引用不会。把 DataBlock 写成一个指向 Sheet1!$A$2:$D$100 的名称,它就会一直是这个矩形区域,直到有东西把它改写。把 Sales[Amount] 写出来,它的意思是"Sales 表的 Amount 列",具体是什么范围取决于公式求值那一刻这张表的实际大小。给表格添加二十行,求和会自动覆盖到它们;这里没有引用需要调整,因为公式里本来就从来没有过一个具体地址
正是这种符号性质,让这类引用无法靠字符串替换来解析。解析器必须在工作簿里按名称找到这张表,按表头文字查到对应的列,判断请求的项目说明符覆盖哪些行,并生成一个具体的矩形区域。HotXLS 通过表模型在公式编译阶段完成这一切,这也是为什么一个在表格增长之前写下的公式,仍然会按表格当前的范围求值
HotXLS 支持解析的语法
受支持的说明符语法覆盖的都是单一矩形结果,值得精确说明清楚,因为 Excel 官方文档展示的语法范围,比大多数引擎实际实现的要大得多。HotXLS 支持 [Col] 及其加括号变体 [[Col]],裸的项目说明符 [#Data]、[#All]、[#Headers] 和 [#Totals],组合形式 [[#Data],[Col]],项目说明符内部的区间 [[#Data],[Col1]:[Col2]],以及一个普通区间 [Col1]:[Col2]
这套语法涵盖的是所有能产出单一连续区块的引用形态:一列、一串相邻的列、其中任意一种只含数据部分或包含表头的切片。不相邻的并集和多区域结果都不在支持范围内。当一个引用无法解析时,公式会保持原有的"跳过、不生成值"行为,而不是替换成一个猜测值,因此一个无法解析的引用永远不会变成一个看似合理的错误数字
var
Book: TXLSXWorkbook;
Sheet: TXLSXWorksheet;
Cols: TStringList;
begin
Book := TXLSXWorkbook.Create;
Cols := TStringList.Create;
try
Sheet := Book.Sheets.Add('Sales');
Cols.Add('Region');
Cols.Add('Q1');
Cols.Add('Q2');
Cols.Add('Amount');
Sheet.Tables.Add('SalesTable', 'A1:D25', Cols);
// ……写入表头行和 24 行数据……
Sheet.Cells[27, 4].Formula := 'SUM(SalesTable[Amount])';
Sheet.Cells[28, 4].Formula := 'SUM(SalesTable[[Q1]:[Q2]])';
Sheet.Cells[29, 4].Formula := 'COUNTA(SalesTable[[#Data],[Region]])';
Sheet.Cells[30, 4].Formula := 'ROWS(SalesTable[#All])';
Book.Recalculate;
Book.SaveAs('sales.xlsx');
finally
Cols.Free;
Book.Free;
end;
end;
为什么当前行写法被有意排除在外?
[@Column] 和 [#This Row] 的意思是"这个公式所在那一行、对应列的那个单元格"。因此这个值不仅取决于表格本身,还取决于正在求值的单元格所在的位置。这是另一类引用:不是编译器能一次性解析出来的矩形区域,而是必须为公式所在的每一行分别重新解析的逐单元格结果
对于这些写法,HotXLS 的表区域解析器会返回 False,把它们导向"跳过、不生成值"的路径。公式文本会被保留并原样写回,因此一份使用了 [@Amount] 的工作簿,在经过你的应用程序处理一轮之后,仍然能在 Excel 里正确打开;缺失的只是 HotXLS 计算出的值。在"值缺失"和"值按错误的行算出来"这两者之间,缺失是你能够检测出来的那一种
实际的变通方案很机械化:在你自己生成的工作簿里,写出等价的 A1 风格相对引用,反正 Excel 内部对大量表作用域逻辑本来也是这样存储的。在你只是处理、不生成的工作簿里,就不要动这个公式,直接读取 Excel 已经存好的缓存值,这通常也正是一条"加载并生成报表"管道想要的结果
var
Book: TXLSXWorkbook;
Sheet: TXLSXWorksheet;
Table: TXLSXTable;
Row: Integer;
begin
Book := TXLSXWorkbook.Create;
try
if Book.Open('sales.xlsx') <> 1 then Exit;
Sheet := Book.Sheets[1];
Table := Sheet.Tables.FindByName('SalesTable');
if Table <> nil then
begin
// 类似记录集的表体查找,行号结果从 1 开始
Row := Table.FindFirst(Sheet, 'Region', 'EMEA');
while Row > 0 do
begin
Log(VarToStr(Sheet.Cells[Row, 4].Value));
Row := Table.FindNext(Sheet, 'Region', 'EMEA', Row);
end;
end;
finally
Book.Free;
end;
end;
表格形状发生变化时会怎样
当结构化引用所指的东西消失时,它会失效,而不是被悄悄重新指向别处。删除一列,引用那一列的公式会像 Excel 那样失效;删除或重命名表格,对它的引用也会用同样的方式处理。这是正确的行为,也和插入删除时的公式引用调整一文中描述的普通引用调整逻辑一致——引擎的职责是让公式保持诚实,而不是让它们看起来有效
行数增长是恰恰相反的情况,完全不需要任何调整。因为引用指的是表格本身而不是一个矩形区域,在表格范围内追加行会扩大 [#Data] 覆盖的范围,而不需要改动任何一个公式。正是这个特性,让表格值得用在报表模板里:不管一次导入最终产生了多少行,总计行都会持续汇总导入产生的全部内容
往返一致性
HotXLS 会保留原始公式文本。一份加载时是 SUM(SalesTable[Amount]) 的工作簿,保存时仍然是 SUM(SalesTable[Amount]),而不是解析后的 SUM(D2:D25)。这一点比看上去更重要:在 Excel 里打开你输出结果的用户,期望看到自己当初写下的公式,而解析后的地址会悄悄把一个能自我维护的模型,变成一个不再能覆盖新增行的脆弱模型
还有两项相关能力补全了这幅图景。表格定义本身,包括无表头的表格和每张表各自的注释,都通过数据验证、AutoFilter 与 Excel 表格一文中描述的表模型往返保存。当很多单元格共用同一种模式时,XLSX 会把它们只存储一次,作为一个共享公式,展开和重新写出的过程在共享公式 si 展开一文中有说明。共享公式内部的结构化引用会同时经过这两条路径,因此两者都必须表现正常,而它们确实做到了
HotXLS 让 Delphi 和 C++Builder 无需安装 Excel、无需任何 Office 自动化即可读写 XLS、XLSX 和 ODS,公式在它自己的引擎中求值。表模型、公式引擎和重新计算 API 都记录在 HotXLS Delphi 电子表格组件页面