有一本預算活頁簿,工作表叫 FY2024,摘要格寫著 =FY2024!B7,Excel 卻讀不了這條公式。FY2024 本身也是儲存格位址,所以名稱得加上單引號,寫成 'FY2024'!B7;HotXLS 用的是 Excel 存檔時的同一條規則,它的 XLSX 活頁簿會在工作表改名或刪除時改寫每一條公式
公式裡哪些工作表名稱必須加引號?
只要公式解析器可能把它讀成工作表名稱以外的東西——數字、布林值或儲存格參照——工作表名稱就得加單引號。ECMA-376 Part 1 §18.17 的公式文法裡有加引號與不加引號兩種寫法,但實務上的依據是 Excel 自己寫進 sheet1.xml 的 <f> 元素內容,因為其他所有讀取器都是對著那份文字調校的。Excel 的 COM Range.Formula 回音和它存下來的 XML 對某些名稱並不一致:R1. 與 A1.x 從 .Formula 讀回來是裸的,檔案裡卻帶著單引號。從 COM 回音歸納出來的規則會被 Excel 自己的檔案推翻。HotXLS 跟的是存檔形式,以下情況一律要引號:
A-Z、a-z、0-9與底線以外的任何字元:空格、連字號、句點、括號- 開頭是數字:
2024、2nd_Half與3D全都要引號 - 任何大小寫的
TRUE或FALSE。TRUE1與TRUEX保持裸寫,因為兩者都讀不成布林值 - 在格線範圍內讀起來像 A1 儲存格的名稱:一到三個字母組成不大於
XFD的欄,後面接 1 到 1048576 的列號,允許前導零 - 開頭像 R1C1 參照的名稱:單獨的
R、C或RC,或後面再接一個數字 - 引號內的名稱若含單引號,要寫成兩個——
Bob's Data寫出來是'Bob''s Data'!A1
A1 規則是真實活頁簿最常撞上的那一條,因為太多商業用工作表名稱剛好是合法的儲存格位址。FY2024 是 FY 欄、第 2024 列。Jan2024 是 JAN 欄、第 2024 列,而 JAN 遠遠落在現代工作表 16,384 欄的範圍之內。Q1、CR1 與 Ra1 同樣都是位址,三個都必須加引號。邊界情況則反過來:A0 沒有第零列,XFE1 超出邊界一欄,XFD1048577 超出邊界一列,ABCD1 有四個字母,所以 Excel 對它們一律保持裸寫。前導零救不了名稱,A01 照樣要加單引號,因為解析器讀成 A1。只檢查字元類別的寫入器,遇到年度預算報表就會吐出 FY2024!B7,而 Excel 讀不回這條公式
像 R1C1 的名稱,以及寧可多加引號的理由
R1C1 形狀的名稱,沒有一條乾淨的規則可以從 Excel 的輸出反推。Excel 會給 R、C、RC、R1、C1、R1x、R1C1x 加引號,連 R1C16385 與 C99999 這種指向不存在欄位的也加;可是 R0、C0、RC0、R0C1、R1048577、Rx、Cx 與 R_1 它又保持裸寫。沒有任何格線上限與 token 形狀的組合能同時重現這兩份名單,所以 HotXLS 取超集:名稱是 R、C 或 RC,或以其中之一開頭再接一個數字,就加單引號。這個超集會多給幾個 Excel 保持裸寫的名稱加引號,R0 與 R1048577 就在其中。這樣做可以接受,理由很直白:任何工作表名稱的引號寫法都是合法的公式語法,凡是會解析公式的讀取器都吃得下;反過來,想逐一對上 Excel 的名單,就會建立在一個沒人講得清楚的規則上。Chart1、Region1、Cost2024 這類普通名稱兩道檢查都不觸發,維持裸寫。如果您也處理 R1C1 文字,HotXLS 的 R1C1 公式記法那篇說明了這些參照一開始是怎麼解析的
含非 ASCII 字母的名稱也會被多加上引號。Excel 在這邊比較寬鬆。Excel 寫出 Données!A1 與 数据!A1 時不加單引號,因為帶重音的字母與 CJK 字母在它的解析器裡算名稱字元。HotXLS 開檔時接受這種裸限定詞,畢竟那是 Excel 產生的;但寫出時會把這類名稱加上引號,例如 'Données'!A1。加引號的形式到哪裡都合法,而寫入端的檢查可以維持一份簡短、可預測的清單,不必去維護一張每個消費端都得同意的 Unicode 字母表
在 Delphi 裡組出工作表限定詞
lxStandard.XlsSheetNameNeedsQuotes 就是那條規則的公開形式,XLSX 寫入器與傳統引擎在輸出工作表參照時用的都是它。如果您自己的程式碼要用工作表名稱拼公式文字,例如一張摘要表指向每個區域或每個年度一張工作表,就讓每個名稱先過這個函式,而不是直接串 Name + '!'。這個函式只回答是或否,內部單引號要加倍得由呼叫端自己做,像下面這個輔助函式那樣;而 XLSX 的儲存格公式在指定時不帶開頭的 =
program SheetQualifiers;
{$APPTYPE CONSOLE}
uses
System.SysUtils, lxStandard, lxHandleX;
// 手拼公式文字用的限定詞:名稱需要時加上單引號,
// 名稱內部的單引號則加倍
function SheetQualifier(const Name: WideString): WideString;
begin
if XlsSheetNameNeedsQuotes(Name) then
Result := '''' + StringReplace(Name, '''', '''''', [rfReplaceAll]) + '''!'
else
Result := Name + '!';
end;
const
Candidates: array[0..10] of WideString = (
'Summary', '2024', 'FY2024', 'Q1', 'TRUE', 'TRUE1',
'R1C1', 'Region1', 'XFE1', 'Données', 'Bob''s Data');
var
Book: TXLSXWorkbook;
Map, Sheet: TXLSXWorksheet;
I: Integer;
begin
Book := TXLSXWorkbook.Create;
try
Map := Book.Sheets.Add('Map');
for I := Low(Candidates) to High(Candidates) do
begin
Sheet := Book.Sheets.Add(Candidates[I]);
Sheet.Cells[1, 1].Value := (I + 1) * 10;
// 用工作表實際拿到的名稱,而不是要求的那個
Map.Cells[I + 1, 1].Value := Sheet.Name;
Map.Cells[I + 1, 2].Formula := SheetQualifier(Sheet.Name) + 'A1';
Writeln(Format('%-12s %s', [Sheet.Name, Map.Cells[I + 1, 2].Formula]));
end;
Book.SaveAs('sheet-map.xlsx');
finally
Book.Free;
end;
end.
// 預期輸出:
// Summary Summary!A1
// 2024 '2024'!A1
// FY2024 'FY2024'!A1
// Q1 'Q1'!A1
// TRUE 'TRUE'!A1
// TRUE1 TRUE1!A1
// R1C1 'R1C1'!A1
// Region1 Region1!A1
// XFE1 XFE1!A1
// Données 'Données'!A1
// Bob's Data 'Bob''s Data'!A1
XLSX 檔案裡的工作表名稱不能含 : \ / ? * [ ],不能以單引號開頭或結尾,長度上限 31 個字元,不分大小寫必須唯一,也不能叫 History,那是 Excel 保留的。TXLSXSheets.Add 與 TXLSXWorksheet.Name 的 setter 都會強制這些規則:禁止字元換成底線,撞名加上 (2) 後綴,保留字或空名稱退回 Sheet。所以您指定的值跟讀回來的值可能不同,用要求的名稱拼出的限定詞會指向一張不存在的工作表。每次指定之後都把 Name 讀回來,用那個值加引號
Sheet := Book.Sheets.Add('Totals');
Sheet.Name := 'Q1/Q2 Sales'; // 工作表名稱不允許 '/'
Writeln(Sheet.Name); // Q1_Q2 Sales
Writeln(SheetQualifier(Sheet.Name) + 'C9'); // 'Q1_Q2 Sales'!C9
工作表刪除之後,Excel 存的是什麼?
工作表被刪除時,Excel 會把整段參照——限定詞連同儲存格或範圍——換成 #REF!。SUM(Data!A1:B2)+1 存成 SUM(#REF!)+1,COUNT(Data!C:C) 存成 COUNT(#REF!),原本指向 Data!$A$1:$B$2 的定義名稱存成單純的 #REF!,圖表的數列範圍則存成 <c:f>#REF!</c:f>。COM 這邊的呈現同樣會誤導人:Range.Formula 顯示 =#REF!A1+1,好像儲存格那部分還留著,而 Excel 存下來的 XML 裡根本沒有儲存格部分。照抄 COM 顯示的文字,會得到 Excel 自己從不寫出的公式——要給其他工具讀的檔案,這賭注很差。TXLSXSheets.DeleteByName 寫的是存檔形式。同一套塌成 #REF! 的邏輯也管那些列或欄消失的參照,工作表內的個案可看插入與刪除列如何調整公式參照那篇
三維參照的行為不一樣。Excel 會縮小三維跨度,而不是摧毀它。工作表順序是 Data、Mid、Keep 時,刪掉 Data 會把 SUM(Data:Keep!A1) 變成 SUM(Mid:Keep!A1):消失的那一端往內縮一張工作表,以刪除前的頁籤順序計算。跨度裡只剩兩張工作表時,結果就是單表形式 SUM(Keep!A1)。刪掉 Mid 完全不會改動公式文字,因為跨度仍然指著兩張活著的工作表,只是中間少涵蓋一張。只有當兩端都指向被刪的那張工作表時,參照才變成 #REF!。死掉的參照會改變公式回傳的結果,所以 HotXLS 在刪除過程中改寫過的每條公式,其快取結果都標成過期;讀值之前請先呼叫 Recalculate。改名則不動快取值,因為數字不會變
uses
System.SysUtils, lxHandleX;
procedure ShowFormulas(Book: TXLSXWorkbook; const Caption: string);
var
Report: TXLSXWorksheet;
Row: Integer;
begin
Writeln(Caption);
Report := Book.Sheets.SheetByName('Report');
for Row := 1 to 4 do
Writeln(' B', Row, ': ', Report.Cells[Row, 2].Formula);
Writeln(' DataBlock: ', Book.DefinedNames.FindByName('DataBlock').Formula);
end;
var
Book: TXLSXWorkbook;
Report: TXLSXWorksheet;
begin
Book := TXLSXWorkbook.Create;
try
Book.Sheets.Add('Data');
Book.Sheets.Add('Mid');
Book.Sheets.Add('Keep');
Report := Book.Sheets.Add('Report');
Book.DefinedNames.Add('DataBlock', 'Data!$A$1:$B$2');
Report.Cells[1, 2].Formula := 'SUM(Data:Keep!A1)';
Report.Cells[2, 2].Formula := 'SUM(Data!A1:B2)+1';
Report.Cells[3, 2].Formula := 'Keep!A1*2';
Report.Cells[4, 2].Formula := 'INDIRECT("Data!A1")';
Book.Sheets.DeleteByName('Data');
ShowFormulas(Book, 'After deleting Data:');
Book.Sheets.SheetByName('Keep').Name := '2025';
ShowFormulas(Book, 'After renaming Keep to 2025:');
Book.Recalculate; // 失去 Data 的儲存格帶著過期的快取值
Book.SaveAs('report.xlsx');
finally
Book.Free;
end;
end.
// 預期輸出:
// 刪除 Data 之後:
// B1: SUM(Mid:Keep!A1)
// B2: SUM(#REF!)+1
// B3: Keep!A1*2
// B4: INDIRECT("Data!A1")
// DataBlock: #REF!
// Keep 改名為 2025 之後:
// B1: SUM('Mid:2025'!A1)
// B2: SUM(#REF!)+1
// B3: '2025'!A1*2
// B4: INDIRECT("Data!A1")
// DataBlock: #REF!
工作表改一次名,整個活頁簿的參照都要改寫
在 XLSX 活頁簿裡指定 TXLSXWorksheet.Name,會把新名稱帶進每一處公式可能出現的地方,DeleteByName 走的是同一批位置來套用 #REF! 塌陷。清單遠超出儲存格公式:
- 每張工作表的儲存格公式。共用公式在每個跟隨儲存格上都展開保存,陣列公式則活在根儲存格上,所以兩者都不用特別處理就涵蓋到了
- 條件格式的公式,包含資料橫條、色階與圖示集裡以公式為門檻的那種
- 資料驗證公式,例如清單來源放在另一張工作表
- 表格的計算結果欄與合計列公式
- 定義名稱,也就是建立在定義名稱與跨工作表公式之上的模型骨幹;改名時,來源範圍讀自被改名工作表的樞紐快取也會跟著改
- 圖表範圍:數列名稱、類別、值、泡泡大小與資料標籤範圍,誤差線的正負範圍,以及圖表標題與座標軸標題背後的儲存格參照
三維跨度只要有一端需要引號,整個跨度就一起加:把 Keep 改名為 2025 會產生 'Mid:2025'!A1,永遠不會是 Mid:'2025'!A1,因為跨度是一個限定詞。跟這次改名無關的限定詞,一律照原樣重寫。如果別處的公式寫著 'Sheet1'!B4,那些技術上不必要的單引號在改名後依然在。順手把這些限定詞正規化對 Excel 無害,但這樣一來,改一張工作表就會動到活頁簿裡的每一條公式,兩版產生的報表一比對,真正的改動就會被幾百筆表面差異埋掉
看起來像工作表參照,其實不是的文字
有三種文字在模式比對下看起來像 Name! 限定詞,實際上不是。字串常值裡的 "Data!A1" 只是文字,所以 INDIRECT("Data!A1") 在改名或刪除之後仍然指著舊名稱,這點跟 Excel 一致;如果報表會為 INDIRECT 拼工作表名稱,就把那些名稱放在某個儲存格裡,讓程式碼跟著改名一起更新。限定詞前面帶方括號活頁簿索引的,像 [1]Data!A1,屬於某個剛好也有一張叫 Data 的工作表的外部活頁簿,改本地 Data 表的名稱絕不能動到它;引號限定詞裡含 [ 的也比照處理。而 #REF! 本身就長得像一個叫 REF 的限定詞,這就是為什麼 HotXLS 在把 REF! 當成工作表名稱之前,會先檢查前面有沒有 #。少了這道檢查,把一張叫 REF 的工作表改名,就會把活頁簿裡每一個 #REF! 都改寫掉
最容易弄壞生成公式的工作表名稱,其實是 FY2024、Q1、2025 這種普通名稱,而不是帶空格或標點的名稱。只要您的 Delphi 程式碼在拼公式文字,就讓每個名稱過一次 XlsSheetNameNeedsQuotes。這樣一來,HotXLS Delphi 試算表元件裡的改名或刪除,會讓每一條公式都停在 Excel 自己會存的那個形式