HotXLS 从 Delphi 和 C++Builder 用一次调用创建 Excel 表格 slicer——Worksheet.Slicers.Add(Table, ColumnName)——并写入一个 slicer 需要的全部八个包位置:slicer 和 slicerCache 部件、两个内容类型覆盖、带各自匹配 extLst 块的工作表和 workbook 关系,以及 drawing 锚点
slicer 看起来像你拖到工作表上的控件。在文件里它完全不是那么回事。它是一个分散的记录,横跨两个新部件、两张关系图、三个扩展块和一个 drawing 锚点,整个东西在 Excel 打开包时作为一个整体被校验。少一块,你得到的不是降级运行的 slicer;你得到修复对话框,然后是一个 slicer 被静默移除的工作簿。正是这种失败模式让手写 slicer 很烦人,也是为什么这里有趣的工程是记账而不是渲染
为什么一个 slicer 要碰包的八个位置?
因为一个 slicer 是三个独立的概念,OPC 把它们逼进三个独立的住所:过滤器状态(workbook 作用域)、视图(worksheet 作用域)和图形(drawing 作用域)。HotXLS 每个表格 slicer 正好写八个落点,而每一个都承重
xl/slicers/slicerN.xml—— 每个 worksheet 一个部件,在该工作表http://schemas.microsoft.com/office/spreadsheetml/2009/9/main命名空间里持有每一个<slicer>视图xl/slicerCaches/slicerCacheN.xml—— workbook 级,每个被切片表列一个,携带实际的绑定- 在这两个部件各自的
[Content_Types].xml里有一个<Override>,使用application/vnd.ms-excel.slicer+xml和application/vnd.ms-excel.slicerCache+xml(ECMA-376 Part 2 要求每个部件都有内容类型,没有它们 Excel 拒绝这个包) - 一个类型为
.../office/2007/relationships/slicer的 worksheet 关系,加上 worksheetextLst里指向那个 rid 的<x14:slicerList>扩展 - 一个类型为
.../office/2007/relationships/slicerCache的 workbook 关系,加上 workbookextLst里列出相同 rid 的<x15:slicerCaches>扩展 - 工作表 drawing 里的
mc:AlternateContent锚点,给 slicer 位置和大小 - 缓存部件里把它绑定到 table id 和 table column id 的
<x15:tableSlicerCache>块
扩展块用标准 ECMA-376 Part 1 §18.2.10 的 extLst 机制:每个 <ext> 携带标识特性的 uri 属性,不识别那个 GUID 的读取器跳过该块。HotXLS 仅凭 GUID 区分表格 slicer 和透视 slicer——表格 slicerList 是 {3A4CF648-6AED-40f4-86FF-DC5316D8AED3},透视的是 {A8765BA9-456A-4dab-B4F3-ACF838C121DE},两个 workbook slicerCaches 扩展相应是 {46BE6895-7355-4a93-B00E-2C351335B9C9} 和 {BBE1A952-AA13-448e-AADC-164F8A28A991}。表格绑定本身住在 {2F2917AC-EB37-4324-AD4E-5DD8C200BD13} 下。这些常量在单元里暴露为 XlsxExtUriSlicerListX15、XlsxExtUriWbSlicerCachesX15 和 XlsxExtUriTableSlicerCache,所以写入路径里没有任何魔数字符串
哪些部件可以逐字重放,哪些必须重建?
HotXLS 用一条规则回答:只有它所引用的东西在保存期间不会被重新编号时,部件才能重放原始字节。表格 slicer 缓存过不了这个测试,所以 HotXLS 每次保存都从模型重建它们,无条件——即使用户从未碰过那个 slicer。原因是 tableId:<x15:tableSlicerCache> 元素用 <table> 的 id 属性识别它的表,而 HotXLS 在写入时对整个 workbook 重新编号表 id。早期实现把缓存部件在打开时存为原始字节、保存时原样推回去——这对大多数不透明部件是正确的策略。它产出的文件在常见情况下干净打开,却在有意思的情况下触发修复:在更早的工作表上加一张表、保存,缓存现在指向属于别的东西、或者什么都不属于的 table id。这个 bug 在普通打开-保存回归里不可见,因为 id 碰巧没有移动
透视形状的缓存走相反的路。它们的负载是 <pivotTables> 加一个描述缓存项、排序状态和条目索引的 <x14:data> 块——一个没有 HotXLS 侧重新编号、忠实建模又昂贵的结构。它们被保存为原始字节并精确重放,TXLSXSlicerCache.IsPivot 和 IsTableCache 把两者区分开。slicer 视图 走第三种策略,脏跟踪:未动过的 <slicer> 元素和它的 drawing 锚点逐字重放,一旦工作表上任何视图被修改,整个工作表部件就从模型重新生成。这就是判断的一般形状——引用稳定处逐字重放、写入器重新分配身份处从模型重建、两者都可接受处脏跟踪——而且它远远不限于 slicer,关于主题、extLst 块和 calcChain 的无损往返笔记 更深入地把这一点讲透了
从 Delphi 创建表格 slicer
TXLSXSlicers.Add 就是整套创建 API。给一个 TXLSXTable 和列名,它物化表列 id、找到或创建底层的 TXLSXSlicerCache、预留匹配的 defined name,并把视图锚到表格右侧。两参数重载选放置位置;六参数那个接收显式的 1-based 含端盒
var
Book: TXLSXWorkbook;
Sheet: TXLSXWorksheet;
Slicer: TXLSXSlicer;
begin
Book := TXLSXWorkbook.Create;
try
Book.Open('sales.xlsx');
Sheet := Book.Sheets[0];
// 默认位置:与表格相隔两列,
// 高 13 行,采用 19pt 默认行高
Slicer := Sheet.Slicers.Add(Sheet.Tables[0], 'Region');
Slicer.Caption := 'Sales region';
Slicer.StyleName := 'SlicerStyleLight1';
Slicer.ColumnCount := 2;
Book.SaveAs('sales-sliced.xlsx');
finally
Book.Free;
end;
end;
两条命名规则值得知道,因为它们是 Excel 的约定而不是 HotXLS 的发明。缓存命名为 Slicer_<column>,非标识符字符折叠成下划线,冲突时追加数字后缀——Slicer_Region,然后 Slicer_Region1,依此类推。而且每个缓存名也被预留为公式是 #N/A 的 workbook defined name,这正是 TXLSXSlicers.ReserveCacheDefinedName 做的。Excel 自己会创建那个名字,而且它很重要:缓存名和 defined name 共享同一个命名空间,所以已经有一个叫 Slicer_Region 的名字的工作簿会在下次保存时冲突。如果你同时构建表格和校验,关于数据校验、AutoFilter 和表格的文章里的 defined name 交互覆盖了相邻情况。共享遵循 Excel 用的同一条规则。在创建新缓存前会先查询 TXLSXSlicerCaches.FindForTableColumn(ATableId, ATableColumnId),所以同一表列上的两个 slicer 视图——一个在仪表板工作表、一个在明细工作表——共享单个缓存部件并保持筛选同步。删除视图时跨工作簿每个工作表做反向检查,只有当引用它的最后一个视图消失时才丢弃缓存和它预留的 #N/A 名字
关系 id 如何在两端保持稳定?
它们保持稳定是因为 HotXLS 通过重放它发出其他每个关系的精确顺序来计算 slicer rid,并把新关系追加在尾部。文件里已有的东西都不会移动。这听起来像过度工程,直到你调试过另一种做法。worksheet .rels 里的关系 id 被 worksheet 部件内部引用——被超链接、drawing、批注、表格部件——而 ECMA-376 Part 2 在关系图内按位置解析它们,不靠任何稳定身份。如果写入器把 slicer 关系插在中间,它后面的每个引用都偏移一位,工作表就悄悄开始指向错误的部件。所以 XlsxWorksheetSlicerRelNumber 走完整条确定性链——外部超链接、然后批注 VML、批注、页眉页脚图片、threaded comments、drawing、工作表背景、打印设置、表格、透视表、查询表、table single cells——然后才为 slicer 认领下一个数字。XlsxWorkbookSlicerCacheFirstRid 在 workbook 侧做同样的事:sheets、sharedStrings、styles、VBA、external links、theme、connections、pivot caches、person、calcChain、sheet metadata、XML maps、custom XML,然后才是 slicerCache 序列。这个一般问题在关于 OPC 关系解析的文章里彻底讲透了。workbook 侧扩展随后由 XlsxBuildWorkbookSlicerCachesExtXml 从这些数字构建,如果源文件已有 slicerCaches 扩展块,它们会先从原始 extLst 剥离,让模型针对当前 rid 重新发出,而不是留下一份过期副本
AlternateContent 锚点及其旧版 fallback
drawing 锚点是 slicer 标记真正变奇怪的地方,而奇怪是有意的。HotXLS 按 ECMA-376 Part 3 在 mc:AlternateContent 元素里发出图形,带一个 Excel 2013 及以后版本会走的 Choice 分支和一个给更老一切的 Fallback 分支
// XlsxSlicerAnchorPayloadXml 生成的内容,结构上:
//
// <mc:AlternateContent>
// <mc:Choice xmlns:sle15=".../drawing/2012/slicer" Requires="sle15">
// <xdr:graphicFrame>
// <a:graphicData uri=".../drawing/2010/slicer">
// <sle:slicer xmlns:sle=".../drawing/2010/slicer" name="Region"/>
// </a:graphicData>
// </xdr:graphicFrame>
// </mc:Choice>
// <mc:Fallback><xdr:sp>...notice shape...</xdr:sp></mc:Fallback>
// </mc:AlternateContent>
看这两个命名空间。Requires="sle15" token 解析到 2012 slicer 命名空间,它是版本门——不理解 2012 slicer 的读取器必须跳过整个 Choice 分支。但 graphicData/@uri 和里面的 sle:slicer 元素用的是 2010 命名空间,因为那是图形框架一直使用的绘图侧标识符。把它们搞反会产出一个 Excel 毫无怨言打开、然后渲染成空框的文件,这是一个令人难忘的没用症状。Fallback 分支持有一个带文本体的普通矩形,说明这个版本不支持表格 slicer——那是 2007 时代的读取器代替控件画的东西,也是老版 Excel 能无损往返工作簿而不破坏任何东西的原因。TXLSXSlicer 通过 SetBounds(ARow1, ACol1, ARow2, ACol2) 和单独的 FromRow / ToCol 属性把几何暴露为 1-based 含端盒,加上通常的随单元格移动和大小的 AnchorType。有一个具体情况下 HasAnchor 是 False:嵌套在组形状里的 slicer。它们随组锚点逐字重放,HotXLS 刻意不为它们合成第二个锚点,因为那样会把图形发两次
编辑既有 slicer
var
Slicer: TXLSXSlicer;
Cache: TXLSXSlicerCache;
begin
Slicer := Sheet.Slicers.FindByName('Region');
if Slicer <> nil then
begin
Slicer.SetBounds(2, 8, 16, 11); // 1-based, inclusive
Slicer.ShowCaption := 1; // -1 = attribute omitted
Slicer.LockedPosition := 1;
Slicer.RowHeightEmu := 19 * XlsxEmuPerPoint; // 241300 EMU
Cache := Slicer.Cache;
if Cache <> nil then
begin
Cache.SortOrder := 'descending';
Cache.CrossFilter := 'showItemsWithNoData';
end;
end;
// 移除某列上的最后一个视图会连带丢弃其缓存
// 连同保留的 #N/A 定义名称一起
Sheet.Slicers.Delete(Sheet.Slicers.IndexByName('Region'));
三态整数属性值得记一笔,因为它们看起来像建模事故,其实不是。ShowCaption、LockedPosition、ColumnCount、StartItem 和 CustomListSort 都用 -1 表示属性在源文件中不存在、写入时也应保持不存在,0 和 1 是显式值。Excel 在几个地方区分"不存在"和"显式默认值",保住这种区分正是让未动过保存的字节 diff 保持干净的原因。RowHeight 和 ColumnWidth 因相关原因保留为原始字符串:Excel 把小数写进这些 EMU 属性,经整数重新解析会在往返中丢精度。RowHeightEmu 是同一存储上的整数便捷访问器,XlsxSlicerDefaultRowHeight 为 241300 EMU——按 XlsxEmuPerPoint 12700 的比率是 19 点
这套能力的边界
两条诚实的限制。第一,HotXLS 完整建模表格 slicer,但不建模透视 slicer 的创建——你可以打开含透视 slicer 的工作簿、读它们的身份、跨保存保持原样、删除它们,但缓存侧以字节重放,也没有从零构建它的 API。第二,slicer 自己不筛任何东西。它是表列上的 UI,它驱动的可见行状态在 Excel 侧;HotXLS 写控件和绑定,不写计算出的筛选结果。如果你需要保存文件里行真的被筛过,还得同时设置表格 AutoFilter。HotXLS 在 <slicer> 或 <x15:tableSlicerCache> 上未建模的任何属性都原样带过,所以来自更新 Excel 构建的 slicer 即使被你移动过,仍会带着它不熟悉的属性完好地重新序列化。正是这个性质让脏重建路径安全可走:只有模型是它解析内容的超集时,从模型重建才可接受。ECMA-376 Part 1 §18.2.10 的 GUID 标记扩展机制,和 [MS-XLSX] 里的 slicer 部件定义,才是让这个保证从一开始就可表达的东西
表格 slicer 随 HotXLS Delphi Component 一起交付,它从 Delphi 5 到当前版本原生读写 XLSX、XLSM 和旧版 BIFF 工作簿,外加 C++Builder 和 Free Pascal,无需安装 Excel、无 OLE。完整 API 参考和试用版在 HotXLS Delphi Spreadsheet Component 产品页上