定義名稱是一個標籤,它代表一個常數、一個儲存格範圍或一個公式運算式,在活頁簿裡儲存一次,並在每個需要它的地方以符號方式參照。在公式裡寫 TaxRate,引擎就把它解析成這個名稱定義所持有的任何東西,無論是字面值 0.08,還是範圍 Data!$A$2:$D$100。跨工作表參照是正交的想法:Data!D2 藉由以工作表名稱限定位址,來觸及另一張工作表上的儲存格。把兩者放在一起,一張摘要工作表就能透過一個從不提及字面位址的名稱,對一張明細工作表加總,而這恰恰就是你在一份由產生器組裝、之後由會計師審計的活頁簿裡所想要的
HotXLS 是 losLab 用於 XLS 與 XLSX 檔案的原生 Delphi 函式庫,它公開了兩種格式的名稱表,具備建立、尋找與刪除的存取,再加上一個能在處理程序內解析名稱與跨工作表參照的公式引擎。這兩種格式保有各自分開的類別階層,而它們名稱 API 之間的差異,正是會絆倒從一邊移植到另一邊的程式碼的那一部分
兩個不共用介面的名稱存放區
在 XLS 那一側,TXLSWorkbook.GetNames 回傳一個 IXLSNames 集合,其 Add(Name, RefersTo, Visible) 多載會把一個名稱寫進 BIFF 名稱表。個別項目以 IXLSName 物件回傳,帶著 Name、RefersTo、一個已解析的 RefersToRange,以及一個 Delete 方法。在 XLSX 那一側,TXLSXWorkbook.DefinedNames 是一個 TXLSXDefinedNames 集合,帶有 Add、FindByName 與 DeleteByName
查詢慣例的分歧方式,會在移植時浮現,而非在編譯時。XLS 集合的預設 Item 屬性接受一個 Variant,所以 Names[0] 與 Names['TaxRate'] 都能對它解析。XLSX 集合沒有這樣的預設屬性;你呼叫 FindByName('TaxRate'),它在名稱不存在時回傳 nil。為一個外觀寫的程式碼,能對另一個外觀編譯成功純屬意外,而失敗傾向於以執行階段的 nil 存取出現,而不是 IDE 裡的一條紅色波浪線
範圍是第一個決定,而不是之後才加的旗標
一個定義名稱若非活頁簿範圍——對每張工作表上的公式可見,就是工作表範圍——只對它所屬工作表上的公式可見。在 XLSX API 裡,這個區別是單一個選用參數。DefinedNames.Add(AName, AFormula) 建立一個活頁簿層級名稱,而 Add(AName, AFormula, ASheetIndex) 則把它繫結到一張工作表。讀回來時,TXLSXDefinedName.SheetIndex 在活頁簿範圍時回傳 -1,否則回傳 0 起算的工作表索引
範圍同時充當你的碰撞政策,而這正是在你寫下第一個名稱之前就該定下它的理由。Excel 允許每張工作表各有一個工作表局部 Total,再加上一個活頁簿層級的 Total,而某張工作表上的公式會先解析局部的那個。生成的活頁簿應該刻意倚賴這一點。好幾張工作表都會消費的業務假設,例如稅率、匯率與申報週期,屬於活頁簿範圍。只有一張工作表的公式會參照的輔助範圍,設成工作表範圍更安全,在那裡沒有東西能遮蔽它們,它們也遮蔽不了任何東西
var
Book: TXLSXWorkbook;
Data, Summary: TXLSXWorksheet;
begin
Book := TXLSXWorkbook.Create;
try
Data := Book.Sheets.Add('Data');
Summary := Book.Sheets.Add('Summary');
// ... 以明細列填滿 Data!A2:D100 ...
Book.DefinedNames.Add('TaxRate', '0.08'); // 活頁簿範圍,一個常數
Book.DefinedNames.Add('DataBlock', 'Data!$A$2:$D$100'); // 活頁簿範圍,一個範圍
Book.DefinedNames.Add('LocalNote', 'Summary!$B$1', 1); // 只限於工作表索引 1 的範圍
// XLSX 公式不帶前導 '='
Summary.Cells[2, 2].Formula := 'SUM(Data!D2:D100)*TaxRate';
Book.SaveAs('model.xlsx');
finally
Book.Free;
end;
end;
一個定義名稱不一定要指向一個範圍。上面的 TaxRate 指的是單純的常數 0.08,而這是發布一個業務假設最乾淨的方式。它在 Excel 的名稱管理員裡出現一次,每個公式都以符號方式參照它,而下個季度的費率變更,只是對產生器的一次單行編輯,而不是在十四組裝好的公式字串裡搜尋
只屬於其中一側的等號
公式輸入通道是移植的程式碼最常壞掉的地方,因為兩個外觀對等號意見不合。XLS 儲存格透過 Value 接收帶有前導 = 的公式。XLSX 儲存格有一個專屬的 Formula 屬性,接受的是沒有前綴的運算式。把 '=SUM(A1:A10)' 寫進 TXLSXCell.Formula,等號就變成已儲存運算式文字的一部分,而非一個標記,而這個檔案的行為,將不會與同一個字串在 XLS 那一側的行為相同
var
Book: IXLSWorkbook; // 以介面計數:不要 Free
Names: IXLSNames;
begin
Book := TXLSWorkbook.Create;
// 假設名為 'Data' 的工作表已持有明細列
Names := Book.GetNames;
Names.Add('TaxRate', '0.08');
Names.Add('Helper', 'Data!$A$2:$A$100', False); // False = 從名稱管理員中隱藏
// XLS 公式透過 Value,帶有 '=' 前綴
Book.Sheets[1].Cells.Item[2, 2].Value := '=SUM(Data!A2:A100)*TaxRate';
Book.SaveAs('model.xls');
end;
那個片段還展示了兩個 XLS 那一側的怪癖。工作表集合是 1 起算的,所以 Sheets[1] 是第一張工作表,對比於 0 起算的 XLSX Sheets[0]。而第三個 Add 參數建立的是一個隱藏名稱:存在於檔案中且可被公式使用,卻在 Excel 的名稱管理員裡隱形。隱藏名稱是產生器內部管線的正確載具,那些終端使用者絕不該意外編輯或刪除的東西
跨工作表參照,以及當列移動時會怎樣
兩個公式引擎都接受標準的跨工作表語法。單純的工作表名稱直接限定成 Data!A1;一個帶空白或標點符號的名稱需要單引號,如 'Sheet With Space'!A1。在一個名稱的 RefersTo 文字裡,幾乎每一次都該使用絕對參照,例如 Data!$A$2:$D$100。一個定義名稱內部的相對參照,會相對於使用它的儲存格來解析,這是 Excel 刻意的功能,卻在它意外發動時成為可靠的混淆來源
結構性編輯是跨工作表記帳掙得它存在價值的地方,而 XLSX 那一側在這些編輯中保持名稱一致。InsertRows 與 DeleteRows 連同儲存格、合併、超連結與圖表錨點一起搬移定義名稱範圍,所以一個指向 Data!$A$2:$D$100 的名稱,在產生器於它上方開一道缺口之後,仍然涵蓋那個資料區塊。公式帶著一個記載下來的但書:列插入只調整以正在編輯的工作表為目標的參照。一個參照 Data!D2:D100 的 Summary 公式,在列進入 Data 時會被改寫,這正是你通常想要的情況。去驗證它而非假設它,因為引擎會以低成本告訴你:
// 計算引擎在處理程序內解析名稱與跨工作表參照
V := Book.Calculate('SUM(Data!D2:D100)*TaxRate');
if VarIsNumeric(V) then
Log('net total checks out: ' + FloatToStr(V));
Calculate 針對目前的活頁簿狀態評估一個任意運算式,而不儲存任何東西,這讓它成為產生器測試的天然斷言基本元素。在 Pascal 裡從來源資料算出預期的彙總,評估活頁簿自己的公式,再比較兩者。公式引擎文章涵蓋了引擎評估什麼、何時評估,以及如何用自訂函式擴充它
屬性層所擁有的 _xlnm 名稱
在一個低階檢視器裡打開一份生成檔案的名稱表,你會發現一些你從沒寫過的項目:_xlnm.Print_Area、_xlnm.Print_Titles,以及它們的親戚。這是 OOXML(ECMA-376 / ISO 29500)儲存列印範圍與重複標題列的方式,做為帶有保留識別碼的定義名稱。HotXLS 透過專屬的工作表屬性來管理它們,所以設定 PrintArea 或 PrintTitleRows 會為你寫入對應的 _xlnm.* 項目
陷阱是親手伸進那個保留命名空間。透過 DefinedNames.Add 加一個 _xlnm.Print_Area 項目,同時又設定 PrintArea 屬性,活頁簿就為一個保留名稱帶了兩個互相衝突的定義——這是一種 Excel 以沒有任何產品該依賴的方式來解析的狀態。把每一個以 _xlnm. 開頭的識別碼都當成屬於屬性層。要檢視列印設定,就讀屬性,而不是名稱表。保護與頁面設定文章在脈絡裡涵蓋了列印範圍屬性
在定案一個設計之前值得知道的兩個邊界
定義名稱不會跟著便利的 XLS 對 XLSX 橋樑一起過去。SaveXLSWorkbookAsXLSX 複製儲存格內容與基本格式,而名稱表不在它記載的複製清單上,所以一份依賴其名稱的活頁簿會在這次跨越中失去它們。在轉換之後透過 DefinedNames.Add 重新建立名稱。這一步比聽起來不那麼像苦差事,因為它給了你一個時刻,把名稱的範圍正規化,而不是把 XLS 檔案剛好有的東西原樣帶過去
另一個邊界是公式字串與工作表名稱之間的漂移。Excel 在一次互動式重新命名期間,會改寫公式與名稱內部的工作表參照,所以使用者在 Excel 裡編輯的檔案能自行保持一致。暴露點在產生器那一側:當 Pascal 程式碼從一個工作表名稱字面值組裝公式字串時,在一個地方重新命名工作表卻忘了另一個地方,就會產生一個指向一張不再存在的工作表的參照。把工作表名稱放在單一個 Delphi 常數裡,並把它餵給 Sheets.Add 與你的公式組裝兩者,這兩者就永遠不會不一致。這正是主張為報表的輸出儲存格命名、而非寫死位址的同一種直覺:一個總計儲存格被命名的範本,在設計師於它上方插入三列之後依然能運作,而一個寫到字面 B17 的產生器,則會悄悄地把它的數字放錯地方。範本報表生成文章正是建立在這個 pattern 之上
兩種格式完整的定義名稱 API,連同公式引擎參考,隨HotXLS Delphi Component一起出貨