HotXLS 用一個呼叫就能從 Delphi 與 C++Builder 建立 Excel 表格 slicer——Worksheet.Slicers.Add(Table, ColumnName)——並寫出 slicer 需要的全部八個套件位置:slicer 與 slicerCache 部件、兩個內容類型覆寫、工作表與活頁簿的關聯及其對應的 extLst 區塊,還有繪圖錨定
slicer 看起來像你拖到工作表上的控制項。在檔案裡它完全不是這回事。它是一份分散的記錄,攤在兩個新部件、兩張關聯圖、三個延伸區塊與一個繪圖錨定上,Excel 開啟套件時會把整件事當成一個單元驗證。漏掉任何一塊,你得到的不是功能降級的 slicer,而是修復對話框,後面跟著一張 slicer 被靜默移除的活頁簿。正是這種失敗模式讓手工產出 slicer 很煩人,也是這裡有趣的工程是簿記而不是渲染的原因
為什麼一個 slicer 要碰到套件裡的八個地方?
因為 slicer 是三個獨立的概念,OPC 把它們逼進三個獨立的家:篩選狀態(活頁簿範圍)、檢視(工作表範圍)與圖形(繪圖範圍)。HotXLS 每個表格 slicer 正好寫八個落點,每一點都承重
xl/slicers/slicerN.xml——每個工作表一個部件,把該表上每一個<slicer>檢視放在http://schemas.microsoft.com/office/spreadsheetml/2009/9/main命名空間xl/slicerCaches/slicerCacheN.xml——活頁簿層級,每個被切片的表格欄一個,載著真正的繫結- 在
[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的工作表關聯,加上工作表extLst裡指向那個 rid 的<x14:slicerList>延伸 - 一個類型為
.../office/2007/relationships/slicerCache的活頁簿關聯,加上活頁簿extLst裡列出相同 rids 的<x15:slicerCaches>延伸 - 工作表繪圖裡給 slicer 位置與大小的
mc:AlternateContent錨定 - cache 部件裡把它綁到 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},兩個活頁簿 slicerCaches 延伸則對應 {46BE6895-7355-4a93-B00E-2C351335B9C9} 與 {BBE1A952-AA13-448e-AADC-164F8A28A991}。表格繫結本身住在 {2F2917AC-EB37-4324-AD4E-5DD8C200BD13} 底下。這些常數以 XlsxExtUriSlicerListX15、XlsxExtUriWbSlicerCachesX15 與 XlsxExtUriTableSlicerCache 的名稱暴露在單元裡,所以寫入路徑沒有任何魔術字串
哪些部件可以逐字重播,哪些必須重建?
HotXLS 用一條規則回答:只有當部件參照的一切在儲存時都不會被重新編號,它才能重播原始位元組。表格 slicer cache 過不了這個測試,所以 HotXLS 每次儲存都無條件地從模型重建它們——即使使用者從未碰過 slicer。原因是 tableId:<x15:tableSlicerCache> 元素用 <table> 的 id 屬性識別它的表格,而 HotXLS 寫入時會全活頁簿重新編號表格 id。早期實作在開啟時把 cache 部件當成原始位元組存起來、儲存時推回去,這對大多數不透明的部件是正確的策略。它產生的檔案在常見情況下開得很乾淨,在有趣的情況下觸發修復:在較早的工作表加一張表格、儲存,cache 現在指向的 table id 屬於別的東西,或者誰也不屬於。這個 bug 在單純開檔存檔的回歸測試裡看不見,因為 id 剛好沒動
樞紐形的 cache 走相反的路。它們的內容是 <pivotTables> 加一個描述快取項目、排序狀態與項目索引的 <x14:data> 區塊——這種結構 HotXLS 這邊沒有重新編號,忠實建模又很貴。這些保留為原始位元組、原樣重播,TXLSXSlicerCache.IsPivot 與 IsTableCache 把兩者分開。slicer 檢視走第三種政策,髒追蹤:未動過的 <slicer> 元素與它的繪圖錨定逐字重播,表上任一檢視一被修改,整個工作表部件就從模型重新產生。這是判斷的一般形狀——參照穩定就逐字重播、寫入端重新指派身分就重建模型、兩者都可接受就做髒追蹤——而且它遠不止適用於 slicer,主題、extLst 區塊與 calcChain 的無損往返筆記有更深入的討論
從 Delphi 建立表格 slicer
TXLSXSlicers.Add 就是整個建立 API。給一個 TXLSXTable 與欄名,它會具體化表格欄 id、尋找或建立背後的 TXLSXSlicerCache、保留對應的定義名稱,並把檢視錨定到表格右側。兩個引數的多載選擇位置;六個引數的版本收明確的 1 起始含兩端方框
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 的發明。cache 命名為 Slicer_<欄名>,非識別字字元折成底線,碰撞時追加數字後綴——Slicer_Region,然後 Slicer_Region1,依此類推。而且每個 cache 名稱也都保留成一個公式為 #N/A 的活頁簿定義名稱,這就是 TXLSXSlicers.ReserveCacheDefinedName 做的事。Excel 自己會建立這個名稱,而且它很重要:cache 名稱與定義名稱共用同一個命名空間,所以一個已經有 Slicer_Region 名稱的活頁簿,下次儲存時就會碰撞。資料驗證、AutoFilter 與表格一文裡的定義名稱互動涵蓋鄰近的案例。共用遵循 Excel 用的同一條規則。建立新 cache 之前會先查 TXLSXSlicerCaches.FindForTableColumn(ATableId, ATableColumnId),所以同一個表格欄上的兩個 slicer 檢視——儀表板工作表一個、明細工作表一個——共用單一個 cache 部件,並且保持篩選同步。刪除檢視時會反向檢查活頁簿的每一張工作表,只有參照它的最後一個檢視消失,才丟掉 cache 與保留的 #N/A 名稱
兩端的關聯 id 怎麼保持穩定?
它們之所以穩定,是因為 HotXLS 以發出其他每一條關聯的完全相同順序重播,算出 slicer 的 rid,然後把新關聯接在尾巴。檔案裡既有的東西一概不動。這聽起來像過度工程,直到你除錯過另一種做法。工作表 .rels 裡的關聯 id 從工作表部件內部被參照——超連結、繪圖、註解、表格部件——而 ECMA-376 Part 2 在關聯圖內以位置解析它們,不是靠任何穩定身分。如果寫入端把 slicer 關聯插在中間,之後每一個參照都偏移一格,工作表會靜默開始指向錯誤的部件。所以 XlsxWorksheetSlicerRelNumber 走完整條決定性的鏈——外部超連結、接著註解 VML、註解、頁首頁尾圖片、討論串註解、繪圖、工作表背景、列印設定、表格、樞紐表、查詢表格、表格單一儲存格——最後才為 slicer 認領下一個號碼。XlsxWorkbookSlicerCacheFirstRid 在活頁簿那一側做同樣的事:工作表、sharedStrings、樣式、VBA、外部連結、主題、連線、樞紐快取、person、calcChain、工作表中繼資料、XML maps、自訂 XML,然後才是 slicerCache 群。這個一般問題在OPC 關聯解析那篇裡處理得很徹底。活頁簿那一側的延伸接著由 XlsxBuildWorkbookSlicerCachesExtXml 用這些號碼建出來,如果來源檔案原本就有 slicerCaches 延伸區塊,會先從原始 extLst 剝掉,讓模型對照目前的 rids 重新發出,而不是留下一份過期副本
AlternateContent 錨定與它的舊版後備
繪圖錨定是 slicer 標記真正變奇怪的地方,而這份奇怪是刻意的。HotXLS 依 ECMA-376 Part 3 把圖形放在 mc:AlternateContent 元素裡,Choice 分支由 Excel 2013 與之後的版本採用,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 命名空間,因為那是 graphic frame 一直以來使用的繪圖側識別符。把這兩者搞反,會產出一個 Excel 開起來不抱怨、渲染出來卻是空框架的檔案——這是個令人印象深刻的沒用症狀。Fallback 分支放著一個普通矩形,文字本體說明這個版本不支援表格 slicer——這就是 2007 時代的讀取端取代控制項畫出來的東西,也是舊版 Excel 能往返活頁簿而不破壞任何東西的原因。TXLSXSlicer 透過 SetBounds(ARow1, ACol1, ARow2, ACol2) 與個別的 FromRow / ToCol 屬性把幾何暴露成 1 起始含兩端的方框,加上負責常見「隨儲存格移動與縮放」行為的 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 在幾個地方把「不存在」與「明確的預設值」當成不同,保留這個區別正是未動過的儲存能維持乾淨位元組差異的原因。RowHeight 與 ColumnWidth 因為相關的理由保留為原始字串:Excel 在那些 EMU 屬性裡寫小數,用整數重新剖析會在往返時損失精確度。RowHeightEmu 是同一份儲存上的整數便捷存取器,XlsxSlicerDefaultRowHeight 是 241300 EMU——在 XlsxEmuPerPoint 比率 12700 下是 19 點
這到哪裡為止
兩條誠實的界線。第一,HotXLS 完整建模表格 slicer,但不建模樞紐 slicer 的建立——你可以開啟含樞紐 slicer 的活頁簿、讀它們的身分、跨儲存保持原樣、刪除它們,但 cache 那一側以位元組重播,而且沒有從零建立一個的 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 產品頁