HotXLS 是原生的 Delphi 和 C++Builder Excel 库,专为无损 XLSX 双向回转(round-trip)而设计:打开工作簿、更改一个单元格并保存,客户的自定义主题、外部 extLst 扩展块以及计算链都能完好无损地保留下来。三种机制保证了该功能的实现 — xl/theme/theme1.xml 的原样缓存、未知 <ext> 块的基于事件的重新序列化,以及在每次保存公式工作簿时生成全新的、符合规范的 xl/calcChain.xml
激发这三种机制的需求场景非常普遍。计费服务加载客户在 Excel 中设计的模板 — 企业颜色主题、KPI 列中的迷你图(sparkline)、以及较新 Excel 构建版本添加的条件格式规则 — 向 B3 单元格写入一个发票总额,然后保存。客户打开结果,发现品牌颜色变回了 Office 默认的蓝色,迷你图消失了,并且 Excel 提示要“修复”文件。代码中的任何内容都没有触及这些功能。但库在保存时触及了
为什么 Excel 文件在库编辑后会丢失格式?
Excel 文件在库编辑后丢失格式是因为大多数库实际上并没有编辑文件 — 而是重建了它。.xlsx 数据包是 XML 部件的 ZIP 压缩包,包括 xl/workbook.xml、每个工作表一个的 xl/worksheets/sheetN.xml、xl/styles.xml、xl/theme/theme1.xml、xl/calcChain.xml 等。典型的库在打开时将这些部件解析为对象模型,并在保存时根据该模型重新生成每个部件。模型不表示的任何特征 — 例如从未解析的主题、来自较新 Excel 的扩展块 — 在内存中都无处存放,因此重新生成的部件会默默将其忽略
ECMA-376 预见到了这个问题的一半。SpreadsheetML 将 extLst(ECMA-376 第 1 部分,即“未来功能数据存储区”,§18.2.10 针对工作簿级元素)定义为指定的扩展点:较新的生成器将功能停放在那里,每个功能都包裹在携带标识该功能的 uri 属性的 <ext> 元素中,而较旧的消费者应当保留他们无法理解的内容。迷你图、切片器和较新的条件格式类型都以此方式传递。因此,丢弃未知 <ext> 块的库不仅是损耗性的 — 它还违反了该格式所设计的向前兼容性契约。对您正在评估的任何电子表格库,提出的问题都很简单直接:如果我更改一个单元格,还会发生什么变化?
HotXLS 如何逐字节保留自定义主题?
HotXLS 通过在打开时缓存 xl/theme/theme1.xml 的原始字节,并在保存时逐字将其写回来保留工作簿的主题。主题部件(ECMA-376 第 1 部分,§14.2.7)是 DrawingML,而不是 SpreadsheetML — 包括配色方案、字体方案、格式方案 — 电子表格引擎没有理由对其进行深度建模。早期的 HotXLS 版本在每次保存时都会重新生成固定的 Office 主题,这正是上述“品牌颜色变回原样”失败的原因;自 v2.89.46 以来,打开的数据包的主题将被原始存储并原封不动地重新输出,而内置 of 的 Office 主题仅为从头创建的工作簿生成。原始字节是可能的最强忠实度保证:不解析、不重新序列化,没有产生漂移的机会
逐字复制刻意优于对主题的编程访问。TXLSXWorkbook 公开了 ThemeMajorFont and ThemeMinorFont,以便您为新工作簿选择标题和正文字体,但在打开时捕获了逐字主题时,这些设置器对保存的文件没有影响 — 双向回转具有最高优先级。如果您确实需要修改现有工作簿的主题,这是一个通过 Excel 本身而不是数据导向型 API 来编辑模板的信号。日常的案例根本不需要任何 API:
var
Book: TXLSXWorkbook;
begin
Book := TXLSXWorkbook.Create;
try
Book.Open('branded-invoice.xlsx');
Book.Sheets[0].Cells[3, 2].Value := 42750.00; // 唯一的一次编辑
Book.SaveAs('branded-invoice-out.xlsx');
// 输出中的 theme1.xml 与输入在字节上完全相同
finally
Book.Free;
end;
end;
保存时未知的 extLst 块会怎样?
HotXLS 会捕获它未原生建模的每个工作表级 <ext> 块,并将其重放到已保存工作表的 extLst 中,从而使较新 Excel 构建版本写入的功能在双向回转中保持完好。自 v2.131.0 以来,捕获的片段可通过每个 XLSX 工作表上的只读 RawWorksheetExts 属性(一个 TStringList)进行查看,这使得该保证可以通过测试代码进行审计,而不仅是出于信念:
var
Book: TXLSXWorkbook;
Sheet: TXLSXWorksheet;
i: Integer;
begin
Book := TXLSXWorkbook.Create;
try
Book.Open('from-newer-excel.xlsx');
Sheet := Book.Sheets[0];
WriteLn(Format('%d foreign ext block(s) captured',
[Sheet.RawWorksheetExts.Count]));
for i := 0 to Sheet.RawWorksheetExts.Count - 1 do
WriteLn(Copy(Sheet.RawWorksheetExts[i], 1, 100)); // 窥视每个 uri
finally
Book.Free;
end;
end;
值得了解的实现细节是,捕获是基于事件级别的重新序列化,而不是原始字节复制。HotXLS 的流式 XML 读取器不公开源偏移量,因此未知的子树是在元素(Element)、文本(Text)和结束元素(EndElement)事件流过时重新构建的。该方法隐藏了一个经典的陷阱:诸如 <a/> 之类的自闭合元素仅触发一个被标记为空的 Element 事件,而绝不触发 EndElement,因此任何仅在 EndElement 上递减的深度计数器都将永远无法看到子树闭合。解决它,重新构建的片段在语义上与原始片段等价 — 属性引用和自闭合形式均被规范化,因此它在字节上并不完全相同,但 Excel 读取的是含义,而不是字节。Excel 自身输出的两个属性使重放变得安全:Excel 在 <ext> 元素上或内部声明了必需 durable 的 xmlns 属性,因此每个捕获的片段都是命名空间自包含的,且正是这种自包含性,使得在工作簿内或跨工作表复制工作表可以通过简单的字符串列表分配(assign)来携带这些外部块
编写 calcChain.xml 以使 Excel 信任您的公式
只要保存的工作簿包含公式,HotXLS 就会写入 xl/calcChain.xml(计算链部件,ECMA-376 第 1 部分,§12.3.1),并在两种排序之间进行选择。如果公式依赖图已经构建且为最新状态 — 即您在最后一次编辑后调用了 Recalculate — 则计算链会以完整的拓扑顺序输出(前驱在后继之前),且任何循环引用成员都会附加在末尾。否则,单元格将按文档顺序排列。两者都是正确的:Microsoft 针对该格式的实现说明 [MS-XLSX] 将计算链视为 Excel 在加载期间会进行验证并重新排序的提示,因此任何完整的列表都是合法的,并且 HotXLS 刻意拒绝在 SaveAs 内部强制构建依赖图 — 边的构建与单元格数量呈二次方关系,在保存百万单元格时这是无法接受的隐藏成本
Book.Open('model.xlsx');
Book.Sheets[0].Cells[10, 4].Formula := '=SUM(D2:D9)';
// 如果此时保存,calcChain.xml 按文档顺序列表公式单元格。
// 在 Recalculate 之后,依赖图已存在,因此相同的保存
// 会改为输出完整的拓扑顺序:
Book.Recalculate;
Book.SaveAs('model-out.xlsx');
为什么关注一个 Excel 视为建议的部件?因为它的缺失是一个信号。一些消费者 — 修复启发式算法、第三方查看器、diff 工具 — 期望公式工作簿携带计算链,并且在保存时默默丢弃该部件的库会生成与 Excel 写入的完全不同的文件。输出有效的链条可以将输出保持在已针对生态系统其余部分进行了测试的范围之内,这是无声、无光彩的双向回转工程的核心
无损双向回转的终点
诚实比营销复选框更重要,因此这些界限同样值得关注。HotXLS 并不逐字节复制整个数据包:工作表 XML、样式、共享字符串和工作簿部分都是根据解析后的模型重新生成的,因此输出在语义上是忠实的,但在二进制上并不完全相同 — 仅 ZIP 本地头就携带了新的 DOS 时间戳。正如上文所述,捕获的 <ext> 片段返回时会进行规范化。当存在逐字主题时,对主题字体的编程覆盖会被忽略。并且保留网具有定义的网格:HotXLS 原生建模的功能(例如迷你图会被解析并重写,而不是盲目复制)加上外部的 extLst 内容以及逐字缓存的部件。对于既未建模也不在扩展点内的部件 — 例如奇特的加载项的自定义部件 — 落在本文介绍的三种机制之外,因此请测试您真实的模板,而不要凭空假定
相邻的保留工作完善了这一图景。VBA 项目和外部工作簿引用在保存时也遵循相同的“保留您未建模的内容”哲学(详见VBA 和外部链接保留的配套文章),且 docProps 中的文档属性有它们自己的读写 API,而不是被默默丢弃。在评估任何电子表格库时,请运行单单元格测试:打开一个功能丰富的生产工作簿、更改单个值、保存,并比对解压后部件与原始文件的差异。除了您修改的工作表之外还有什么被更改了,这比任何功能矩阵都能告诉您更多关于该库的信息
此处介绍的双向回转机制 — 自 v2.89.46 起的逐字主题保留,以及自 v2.131.0 起的外部 extLst 捕获和 calcChain.xml 输出 — 均在当前的 HotXLS Delphi Excel Component 中发售,其产品页面记录了针对 Delphi 和 C++Builder 的完整 XLSX 读写功能集