想像一個幾乎什麼都不做的工作:開啟一份每月活頁簿、把今天的日期寫進某一格、再存回去。讓這樣的工作透過服務執行夠多次,抱怨還是會出現。巨集不見了,或連動的匯率現在讀到 #REF!,而營運團隊確信是你的程式碼把它們刪掉的。它並沒有刪掉任何東西。通常發生的事是:一份啟用巨集的活頁簿以普通的 .xlsx 名稱送出,而 Excel 遵守 ECMA-376 的內容型別規則:一個內容型別宣告不含 VBA 的封包無法載入 VBA 專案,無論那些位元組是否就擺在那裡。檔案沒有壞。它被重新命名成 Excel 必須忽略其中一部分的狀態
巨集與外部活頁簿連結,是自動化最可靠會弄丟的兩樣東西,原因相同。兩者都存在於編輯程式碼實際碰觸的儲存格格線之外,所以用列與權來推理的程式碼會把它們丟掉,卻從未發出任何刪除動作。HotXLS 是一個原生的 Delphi 與 C++Builder 程式庫,不需安裝 Excel 即可讀寫 XLS 與 XLSX,它把這兩項資產當作刻意攜帶的承載,而不是碰巧複製到的資料。以下說明每一項各自需要你的儲存路徑提供什麼,以及保證在哪裡止步
為什麼這兩項資產在重寫下行為不同
VBA 專案是一個不透明的二進位。在 OOXML 封包中它是 vbaProject.bin 檔案;在舊式 BIFF 檔案中它是一個 OLE storage。弄丟它的方式恰好有兩種:寫入器從未把它複製進輸出,或輸出得到了一個禁止它的檔案型別。兩種失敗都是全面且無聲的。專案要嘛在、要嘛不在
外部連結根本不是一個 blob。它是一小張關係圖:一個指向另一個活頁簿的目標路徑或 URL、該目標所公開的工作表名稱清單,以及這些工作表中上次所見值的選用快取,好讓 Excel 在目標離線時還能顯示點東西。這三部分在重寫下有不同的生命週期,而一個程式庫可以忠實保留其中一部分,同時悄悄丟掉其他部分。這種不對稱正是值得精確掌握的部分,因為儲存格編輯程式碼裡沒有什麼會把它浮現出來
在 XLSX 重寫中攜帶 VBA 專案
在 XLSX 端,TXLSXWorkbook 逐字保留巨集承載。VbaProject 屬性把原始的 vbaProject.bin 位元組放在一個 AnsiString 裡,而空字串就是模型表示「沒有巨集」的方式。環繞它的是三個操作:HasVbaProject 回答是否有專案存在,ClearVbaProject 刻意移除它,LoadVbaProjectFromFile 則注入一個從範本抽取出來的專案。最後這個呼叫的價值超出表面。它讓產生的活頁簿可以接上一個標準巨集專案,而不必把一整份範本檔案拖過整個管線
var
Book: TXLSXWorkbook;
Sheet: TXLSXWorksheet;
begin
Book := TXLSXWorkbook.Create;
try
Sheet := Book.Sheets.Add('Data');
Sheet.Cells[1, 1].Value := 'Refreshed ' + DateTimeToStr(Now);
Book.LoadVbaProjectFromFile('macros\vbaProject.bin');
if not Book.HasVbaProject then
raise Exception.Create('VBA payload failed to load');
// 副檔名 .xlsm 並非裝飾:它選擇了
// 封包內啟用巨集的內容型別。
Book.SaveAs('monthly-report.xlsm');
finally
Book.Free;
end;
end;
儲存這一行就是整個問題轉折之處。持有 VBA 專案的活頁簿必須以啟用巨集的語意來寫入,而 HotXLS 在目標名稱以 .xlsm 結尾時套用它們。改給它 .xlsx,Excel 就會拒絕巨集,即使位元組實際上存在於封包中、而且本可以正常還原序列化。副檔名不是裝飾;它選擇了那個告訴 Excel「VBA 專案可以存在」的內容型別。多數時候你只需要把承載攜帶過去。當你需要讀進它,例如為稽核報表列出模組名稱時,ParsedVBAProject 公開一個已解析的模組模型,而 VbaProject 則維持原始未動過的位元組
從舊式 XLS 活頁簿重用巨集
BIFF 外觀層映照了同一套工具,只多了一個步驟。HasVBAProject 探測已載入的檔案,SaveVBAProjectToFile 把專案 storage 寫到磁碟,LoadVBAProjectFromFile 則把一個讀回另一個活頁簿。這趟經由檔案的繞道,讓一件常見的現代化工作變得直截了當:把巨集從 2003 年代的模型中抽出,種進新產生的 XLS 輸出,執行時不需要原始範本
var
Src, Dst: IXLSWorkbook; // 介面參照:不需手動釋放
begin
Src := TXLSWorkbook.Create;
if Src.Open('legacy-model.xls') <= 0 then
raise Exception.Create('Cannot open legacy model');
if Src.HasVBAProject then
Src.SaveVBAProjectToFile('extracted-vba.bin');
Dst := TXLSWorkbook.Create;
Dst.Sheets.Add.Name := 'Report2026';
Dst.LoadVBAProjectFromFile('extracted-vba.bin');
Dst.SaveAs('report-with-macros.xls');
end;
記憶體模型是這裡的陷阱,而且它與 XLSX 類別方向相反。TXLSWorkbook 透過參考計數的 IXLSWorkbook 介面持有,所以你絕不手動釋放它;XLSX 的 TXLSXWorkbook 則是普通物件,你必須用 try..finally 包起來釋放。在同一個單元裡混用兩種慣例,重複釋放的當機就會接踵而來。還有一個值得遵守的邊界:把抽取與注入留在單一檔案格式內。BIFF 的專案 storage 與 OOXML 的 vbaProject.bin 是表親,不是同一個容器,而一條必須在兩種格式中都輸出巨集的管線,應該為每種格式各自保留一份獨立的巨集範本
外部連結:地圖存活,快取值不會
對於 XLSX 活頁簿,HotXLS 透過 ExternalLinks 集合公開外部連結。每個 TXLSXExternalLink 帶有一個 Target(遠端活頁簿的路徑或 URL),加上一個命名它所參照工作表的 SheetNames 清單。兩者在開啟後儲存的循環中都完好存活,你也可以從無到有建立一個連結:
var
Link: TXLSXExternalLink;
begin
Link := Book.ExternalLinks.Add('\\fileserver\finance\fx-rates-2026.xlsx');
Link.SheetNames.Add('FX');
if Book.ExternalLinks.Count > 0 then
Writeln(Format('%d external link(s): delivery requires reachable targets',
[Book.ExternalLinks.Count]));
end;
邊界位在目標清單更深一層。HotXLS 往返保留連結地圖,也就是目標與工作表名稱,但它不解析也不重寫 OOXML 保留在連結 sheetDataSet 元素裡的快取儲存格值。那份快取正是讓 Excel 在來源檔案離線時能顯示上個已知數字的東西,而產生的活頁簿出貨時並不帶有它。後果落在收件者身上,不是你。開啟這樣一份檔案,而目標連不到——一台脫離 VPN 的筆電、一個被改名的共用資料夾——而依賴該連結的公式就會解析成 #REF!,或卡在一個更新提示後面。於是兩條規則由此而生。不要承諾一份產生的活頁簿能在離線時顯示它的外部連結值。而且把一個非零的 ExternalLinks.Count 當作交付前置條件而非功能:每個目標都必須能從檔案實際被開啟的地方連得到
XLS 讀取器逐位元組保留什麼
對於它不建立模型的結構,BIFF 端有另一個答案:讓它們原封不動。樞紐快取與樞紐檢視(SX* 記錄家族)、QueryTable 定義、外部資料連線、自訂檢視、頁首圖片與佈景主題記錄,全部以原始記錄區塊的形式通過開啟後儲存的循環,不解析、不修改。外部參照本身則透過底層的 EXTERNSHEET 與 SupBook 記錄往返。在 XLS 端沒有為它們提供具型別的建立 API,但既有的連結能在編輯中毫髮無傷地存活
逐位元組保留是一個貨真價實的保證,但帶著一道銳利的邊。因為沒有任何東西去讀取被保留的結構,你的編輯無法毀損它。基於同樣理由,也沒有任何東西去更新它。在一個被保留的樞紐快取或查詢表格所指向的區域中插入列,結構會維持它原本的座標,而下方的資料卻位移了。檔案仍是有效的 XML 或 BIFF;意義已悄悄偏離對齊,而沒有任何錯誤發出來告訴你。站得住腳的版面安排是:把你產生的編輯放在沒有保留結構的工作表上,這與我們關於工作表保護與頁面設定的文章中保護鎖定與列印設定工作表的紀律是一樣的
驗證你實際寫出的檔案
兩種失敗模式在寫入時都是無聲的,所以真正要緊的斷言,是透過重新開啟輸出來做的,而不是信任產生它的程式碼。三項檢查幾乎涵蓋一切。重新開啟檔案,確認只要預期有巨集,HasVbaProject 仍回傳 true,這會在一個測試中同時抓到被丟掉的承載與錯誤的副檔名。讀取 ExternalLinks.Count 並與重寫前的計數比較。然後在 Excel 中停用巨集開啟檔案一次,因為 Excel 的內容型別驗證比任何程式庫都嚴格,而 Excel 正是你的客戶據以評判這份檔案的程式
這些都不需要在讀入時做完整解析。當活頁簿大量到來,而你只需要分診哪些帶有受管內容時,我們關於工作表列出與輕量活頁簿檢查的文章中的輕量探測,讓你能在第一次重寫執行之前,就把帶巨集與帶連結的檔案路由進更嚴格的管線
有些問題常出現到值得直接回答。HotXLS 絕不執行它所保留的巨集:程式庫裡沒有 VBA 執行階段,只有用來儲存、複製、抽取與注入專案的機制,把它當作資料。在一台伺服器上,這是一個值得明說的安全特性,因為一個惡意的巨集在通過管線時會保持惰性,直到桌面版 Excel 開啟檔案且使用者啟用內容。把 .xlsm 轉成 .xlsx 又保留巨集是不可能的,而這是格式的規則,不是程式庫的限制:.xlsx 的內容型別宣告的是無巨集活頁簿,所以唯一誠實的結果要嘛留在 .xlsm,要嘛呼叫 ClearVbaProject 出貨一份真的沒有巨集的檔案。無聲的重新命名是唯一沒有人滿意的選擇。而當連結儲存格在重寫後顯示 #REF!,原因就是上面討論的缺失值快取:新檔案帶著目標,但不帶快取的數字,所以 Excel 必須在開啟時解析來源,而一個連不到或環境相對的路徑會擊敗它。要嘛保證目標可連,要嘛在交付前把計算好的值寫進儲存格並完全卸除這個相依
編輯別人的活頁簿,多半是在保留你沒寫、也不完全了解的東西。此處描述的 VBA 與外部連結往返設施,隨HotXLS Delphi Component(供 Delphi 與 C++Builder 使用)一同出貨,並附帶讓你在檔案送達那一刻就能偵測受管內容的稽核屬性