HotXLS 可以讓像 =[Rates.xlsx]FX!B4*C10 這樣的公式,直接對照另一個已在記憶體中開啟的活頁簿求值,而不是對照一個過期的快取值。你透過 RegisterExternalWorkbook 以名稱註冊第二個活頁簿,每次重新計算都會直接從該物件讀取被參照的儲存格。取消註冊後,求值就會回退至該檔案本身已攜帶的外部連結快取
這填補了一個明確且令人困擾的落差。Excel 活頁簿經常互相參照:一張匯率表、一份會計科目表、一份由另一個團隊維護的價目表。當這類檔案是由函式庫而非 Excel 開啟時,這些參照通常只會解析成 Excel 上次重新計算時快取下來的值,那個值可能已經是好幾個月前的,而無論重新計算多少次都無法修正,因為資料根本不在那裡
跨活頁簿參照在檔案內部是什麼樣子?
外部參照並不是以你在公式列看到的文字形式儲存。活頁簿會維護一張支援活頁簿表,每個公式權杖指向該表中的一個索引,另有一個獨立的快取值區塊,保存每個被參照儲存格的最後已知結果。這就是為什麼在缺少其他相關檔案的情況下開啟一個檔案,仍然會顯示數字
剖析文字形式有其自身的細節。詞法分析器會把括號中的活頁簿名稱視為單一識別碼的一部分,因此 [Book.xlsx]Sheet!A1 會成為單一權杖,而不是一個不成對的括號後面接著工作表參照。名稱接著會被正規化以供查找:引號、路徑前綴、副檔名與數字索引形式,全都會摺疊成一個大寫的基底名稱,因此 'C:\Rates\[Rates.xlsx]FX'!B4 與 [rates.xlsx]FX!B4 會解析到同一個已註冊的活頁簿
註冊活頁簿並重新計算
註冊是依名稱進行的,而這個名稱就是發出參照的活頁簿公式中所出現的那個名稱:
uses
lxHandleX;
var
Main, Rates: TXLSXWorkbook;
begin
Main := TXLSXWorkbook.Create;
Rates := TXLSXWorkbook.Create;
try
if (Main.Open('valuation.xlsx') <> 1) or
(Rates.Open('rates.xlsx') <> 1) then
Exit;
// valuation.xlsx 中寫著 [Rates.xlsx]... 的公式
// 現在會從即時物件讀取,而不是從快取的外部值讀取
Main.RegisterExternalWorkbook('Rates.xlsx', Rates);
Main.Recalculate;
Writeln(VarToStr(Main.Sheets[0].Cells[10, 3].Value));
// 變更來源後再次重新計算:新值會一路傳遞下去
Rates.Sheets[0].Cells[4, 2].Value := 1.0842;
Main.Recalculate;
Writeln(VarToStr(Main.Sheets[0].Cells[10, 3].Value));
finally
Main.UnregisterExternalWorkbook('Rates.xlsx');
Rates.Free;
Main.Free;
end;
end;
註冊持有的是一個弱參照。HotXLS 不會取得已註冊活頁簿的所有權,也不會釋放它,這代表生命週期由你自行管理,順序也很重要:務必在釋放來源活頁簿之前先取消註冊,或至少不要在釋放之後仍執行重新計算
活頁簿未註冊時會發生什麼事?
求值會回退至檔案中儲存的外部連結快取,這正是在缺少其他相關檔案的情況下開啟活頁簿時的行為。正是這個回退機制,讓取消註冊是安全的,而不是具破壞性的
取消註冊之後的行為值得精確說明,因為它常讓人感到意外。當一個參照既無法從已註冊的活頁簿解析,也無法從快取解析時,求值會產生一個錯誤,而這個錯誤不會被寫回儲存格。儲存格會保留其最後一次的計算值。重新註冊來源並再次重新計算,該值就會再次更新
這項「失敗時保留原值」的規則是刻意設計的。另一種做法——來源一旦無法取得,就立刻把錯誤值寫入每一個相依儲存格——會讓一張暫時遺失的匯率表,把整份活頁簿變成滿是錯誤標記,之後即使成功重新計算也還得修補。保留最後一個有效值,代表一個中斷的處理流程,留下的仍是一份讀起來合理的文件
「已變更」狀態的傳播止於活頁簿邊界
編輯已註冊來源中的某個儲存格,並不會把主活頁簿中的相依儲存格標記為已變更。漸進式重新計算追蹤的是單一活頁簿內部的相依關係,並不會跨越活頁簿邊界進行監看。Excel 的行為也是如此,這就是為什麼它的外部參照只會在開啟檔案或明確重新整理時更新,而不是持續更新
實務上的結果是,變更來源值之後,必須明確地對發出參照的活頁簿執行一次重新計算,就像上面範例中那樣。若你的應用程式在迴圈中同時編輯兩份活頁簿,建議在最後統一重新計算一次,而不是每次編輯後都算一次;如此一來,漸進式重新計算與相依關係圖一文所描述的漸進式引擎,就能在每個活頁簿內部只執行最少量的工作
這項功能適用的三種情境
合併彙總是最明顯的一種。一份母活頁簿參照十二份月度檔案;把這十二份全部註冊,重新計算一次,就能在完全不開啟 Excel 的情況下讀取彙總後的數字。每一份的註冊名稱都必須與母活頁簿公式實際寫的名稱相符,這一點值得對照公式文字逐一確認,而不是假設它就等於檔案名稱
情境分析是第二種。註冊一個來源,把一組假設寫入其中,重新計算相依模型,記錄輸出結果,然後重複這個流程。由於來源是一個即時物件,各次迭代之間不需要寫入任何檔案,一百個情境的代價只是一百次重新計算,而不是一百次的儲存與重新載入循環
交付前驗證是第三種,也是最不明顯的一種。開啟一份活頁簿,註冊其相關檔案,重新計算,再把結果與檔案原本攜帶的值互相比對。若出現不一致,代表快取值已經過期,這是一項值得在客戶收到檔案之前就抓出來的缺陷。這種比對,天然適合搭配比較兩份 Excel 活頁簿的差異一文所描述的活頁簿差異比對
在依賴這項功能之前先做好參照檢查
先執行一次公式分析。它會在單一次走訪中回報缺少的函式、外部參照與循環相依成分,讓你在動手尋找檔案之前,就能確切知道文件預期哪些活頁簿名稱:
var
Issues: TArray<TXLSXFormulaIssue>;
I: Integer;
begin
Issues := Main.AnalyzeFormulaIssues;
for I := 0 to High(Issues) do
if Issues[I].Kind = xlsxFormulaIssueExternalReference then
Writeln(Format('sheet %d R%dC%d needs [%s] (%s)',
[Issues[I].Sheet, Issues[I].Row, Issues[I].Col,
Issues[I].ExternalWorkbook, Issues[I].SourceFileName]));
end;
另外兩個習慣能省下不少時間。保持註冊名稱的一致性——以純檔案名稱進行註冊,讓正規化機制自行處理公式中帶引號或路徑前綴的各種變體。並把參照到你無法提供的檔案的活頁簿,當成唯讀成品來對待:對它重新計算會保留快取值,但任何改變相依儲存格的編輯,都會讓文件處於一種部分值是最新、部分值是歷史值的狀態,而檔案本身完全無法標示哪個是哪個。在活頁簿之間複製公式是另一種操作,有自己的一套重新綁定規則,涵蓋於跨活頁簿複製與公式重新綁定一文,而已定義名稱則再增加一層複雜度,說明於已定義名稱與跨工作表公式一文
即時跨活頁簿求值、漸進式重新計算與公式分析,在 Delphi 與 C++Builder 中運作於同一套引擎;完整功能清單請見HotXLS Delphi 試算表元件頁面