一個多年來發出 .xlsx 的 Delphi 報表後端,接到了一項新需求:某個公部門客戶的採購規則強制規定 OpenDocument 試算表輸出,而那個帳號上的分析師把他們的編輯以從 LibreOffice 儲存的 .ods 檔案傳回。所以現在同一份程式碼既要寫 ODS,也要讀它。HotXLS,losLab 供 Delphi 與 C++Builder 使用的原生 Object Pascal 試算表程式庫,在完全不安裝 Excel 或 LibreOffice 的情況下處理兩個方向。它不做的是讓兩個方向對稱。匯出攜帶的遠多於匯入能復原的,而一支假設並非如此的團隊,會看著公式與格式在某處於客戶的修訂與下一份報表之間蒸發,卻沒有錯誤可指
ODS 支援位於 XLSX 外觀層,而非 XLS 那一個
HotXLS 在單一套件裡附帶兩個獨立的類別階層:lxHandle 單元中的 TXLSWorkbook 供二進位 BIFF8 .xls 檔案使用,lxHandleX 單元中的 TXLSXWorkbook 供 OOXML .xlsx 封包使用。每一個 OpenDocument 進入點——OpenODS、SaveAsODS、GetODSSheetNames——都掛在 TXLSXWorkbook 上。這個位置不是任意的。一個如 OASIS ODF 1.3 所指定的 ODS 封包,是一個帶有 mimetype 成員、一份資訊清單與一個 content.xml 主體的 zip 封存,這使它成為 OOXML zip 的結構表親;BIFF8 則是一條 1990 年代的二進位記錄串流,兩者毫無共通之處
這個位置有一個實用的鋒利邊:一份舊式 .xls 活頁簿無法一次呼叫就變成 .ods。你得先把 BIFF 內容透過 lxXlsxExport 單元的 SaveXLSWorkbookAsXLSX 橋接進 XLSX 模型,透過 TXLSXWorkbook 重新開啟結果,再從那裡匯出。這座橋不是無損的,而在你基於它建置之前,值得先了解缺口。它複製值、公式、數字格式、字型、填色與欄寬。它丟棄框線、合併區域、註解、圖表與條件格式。一份格式繁重的 .xls 來源,抵達 ODS 時會比它離開時看起來更樸素,而那是橋接的屬性,不是 ODS 寫入器的
匯入端的偵測是自動的。普通的 Open 方法透過 mimetype 成員辨識 ODS 封包,當該成員不存在時,會退而檢查頂層的 content.xml,所以一條通用的「無論使用者上傳什麼都開啟」的程式碼路徑,不需要自己嗅探副檔名。開啟之後,SourceFormat 屬性回報是哪個分支被觸發
以 TODSExportOptions 匯出為 ODS
匯出呼叫本身是一行;圍繞它的選項物件帶著審查者之後會問起的決定:
var
Book: TXLSXWorkbook;
Opts: TODSExportOptions;
begin
Book := TXLSXWorkbook.Create;
try
Book.Open('quarterly-report.xlsx');
Opts := TODSExportOptions.Create; // 由呼叫者擁有並釋放
try
Opts.Generator := 'ReportService 4.2'; // meta:generator 覆寫
Opts.IncludeCharts := True;
Opts.IncludeImages := True;
Book.SaveAsODS('quarterly-report.ods', Opts);
finally
Opts.Free;
end;
finally
Book.Free;
end;
end;
選項物件由呼叫者擁有。HotXLS 不會釋放它,這也是為什麼內層的 try..finally 在那裡、而且不是選用的。兩個改變輸出而非只是為它貼標籤的屬性,值得更仔細地看。把 IncludeCharts := False 不只是隱藏圖表:它會把圖表子文件與它們的資訊清單項目從封包中剝除,這正是一個會被圖表絆倒的資料管線消費者所想要的。Generator 覆寫 ODF 的 meta:generator 字串,否則它讀作 HotXLS/<version>;當下游工具對檔案產生者做指紋辨識以路由支援時,請覆寫它。如果這些都不適用,就完全略過選項物件。呼叫 SaveAs(FileName, xlsxOpenDocumentSpreadsheet) 與使用預設值的 SaveAsODS 相同,而兩者的串流多載都讓你能把封包直接寫進 HTTP 回應而無暫存檔
匯入路徑讀取什麼——又刻意略過什麼
在向任何人承諾往返保真之前,請仔細讀這一段。HotXLS 中的 ODS 匯入刻意是一條輕量級路徑。它保留純量儲存格值,以及每個公式在儲存時所攜帶的快取結果,並把重複的列與欄展開進格線。它不帶過來樣式、ODS 公式運算式或繪圖
公式的選擇最可能咬人,而它是刻意的。一個 ODF 儲存格並排儲存兩樣東西:公式運算式,以 ODF 1.3 Part 4 定義的 OpenFormula 方言寫成;以及產生它的應用程式為它算出的最後一個值。把 OpenFormula 翻譯成 Excel 公式語法,本身就是一個方言轉換問題,在函數詞彙、參照語法與錯誤模型上有真實的邊緣案例。改讀快取值能避開那一整類無聲誤譯,所以你匯入的數字,正是傳送者最後看到的數字。代價是它們以數字到達,而不是以產生它們的活公式
需要圍繞它設計的失敗模式由此直接衍生:一份其合計在 LibreOffice 上次儲存時是正確的試算表,匯入時帶有正確的數字,但那些數字現在是常數。編輯一個輸入儲存格、重新計算,什麼也不動——公式沒了,只剩下它的最終結果。如果工作流程在匯入後需要活公式,請透過 Cell.Formula 從你自己的業務規則以程式方式重新建立它們——在 XLSX 外觀層上,它接受運算式而不帶前導等號
圍繞不對稱往返來設計
匯出從完整的記憶體內活頁簿模型繪製:值、樣式,以及——如果你要求——圖表與圖片。匯入只回傳值。所以 .xlsx 到 .ods 這一段是高保真的,而 .ods 到 .xlsx 那一段帶回值與快取結果,但沒有樣式、也沒有活公式。把兩者串起來,不對稱就會疊加。一趟完整的 .xlsx 到 .ods 到 .xlsx 循環,在出去時忠實寫下一切,在回來時卻失去樣式與公式,即使兩個步驟都沒有任何地方出錯
Book := TXLSXWorkbook.Create;
try
Book.Open('vendor-revision.ods'); // 格式自動偵測
if Book.SourceFormat = xlsxOpenDocumentSpreadsheet then
begin
// 在 ODS 匯入之後,值與快取的公式結果是存在的;
// 樣式與活的公式則不是。在儲存前,重建
// 下游管線所依賴的一切。
Book.Sheets[0].Cells[2, 5].Formula := 'SUM(B2:D2)';
Book.SaveAs('vendor-revision.xlsx');
end;
finally
Book.Free;
end;
由此產生的架構模式是:把內送 .ods 檔案當作資料來源,而非就地編輯的文件。把規範活頁簿保留在 .xlsx,從客戶修訂中讀出值,並依需求從規範副本發出新鮮的 ODS。驗證兩邊都該做——在 LibreOffice Calc(參考 ODF 消費者)與 Excel(它讀 ODS 已多年,但在圖表與樣式支援的邊緣與 LibreOffice 意見不一致)中開啟匯出的檔案。工作表數量、少數幾個關鍵儲存格與圖表是否存在,對每個匯出設定檔構成充分的煙霧檢查
在投入匯入之前先分診 ODS 檔案
當一個端點接受上傳時,列工作表名稱遠比完整解析便宜,而且能提早抓到結構上的意外:
Names := TStringList.Create;
Book := TXLSXWorkbook.Create;
try
if Book.GetODSSheetNames('incoming.ods', Names) <= 0 then
raise Exception.Create('not a readable ODS package');
if Names.IndexOf('Data') < 0 then
raise Exception.Create('revision is missing the Data sheet');
finally
Book.Free;
Names.Free;
end;
回傳慣例會絆倒人:HotXLS 的呼叫一般在成功時回傳一個正計數或 1,在失敗時回傳 -1,並在失敗時清空清單,所以請測試 <= 0,而不是拿來與某個特定的正值比較。GetODSSheetNames 既不重設也不填入活頁簿執行個體,所以單一探測物件就能審查整個目錄的內送檔案。像這樣的結構檢查,能在閘口抓到最常見的現實世界失敗——某個分析師在把修訂傳回前重新命名或刪除了某個工作表——在那裡錯誤訊息仍能點名檔案與缺失的工作表,而不是在三層深處以一個 nil 參考浮現
如果你打算圍繞這個建置更廣的轉換管線,活頁簿稽核與轉換工作台模式示範了如何在選擇目標格式之前先盤點檔案特徵,而大型活頁簿效能指南則讓批次匯出保持在合理的記憶體範圍內
HotXLS 是一個附帶完整原始碼的原生 Delphi 與 C++Builder 試算表程式庫;完整的功能清單與授權詳情位於HotXLS Delphi Component 產品頁