您將一份完成的活頁簿交給同事,並要求他們對其進行篩選,而不是重寫。因此,您保護了工作表。在較舊的 HotXLS 組建中,這個動作會在檔案中寫入一件事:<sheetProtection sheet="1" objects="1" scenarios="1"/>,每次都是寫死的。工作表被鎖定了,密碼雜湊被加上了,使用者什麼都不能做,甚至連您實際想開放的排序和篩選都不行。Excel 自帶的「保護工作表」對話方塊正是為了這個原因而有 15 個核取方塊,但引擎卻無法表達其中任何一個。這正是 v2.91.0 保護模型所要填補的落差
HotXLS 是一個適用於 Delphi 和 C++Builder 的原生 VCL 試算表元件,能在未安裝 Excel 的情況下讀寫 XLS 和 XLSX。本文討論的是工作表保護在 XLSX 方面的內容:新的 TXLSXSheetProtectionOption 列舉、切換各項權限的 AllowOption 屬性,以及那個讓每個手寫 <sheetProtection> 元素的開發者都會絆倒的 OOXML 編碼規則
工作表保護實際上防護了什麼
首先是邊界問題,因為它決定了您應該對這一切有多信任。OOXML 試算表格式 (ECMA-376) 中的工作表保護是一種互動原則,而不是加密。它告訴遵循標準的應用程式,在工作表受保護時應拒絕哪些編輯動作。儲存格的值仍然以純文字形式存在於 xl/worksheets/sheetN.xml 中;解壓縮 .xlsx,它們就在那裡。選用的密碼會儲存為一個短的舊式雜湊值,而不是一個能打亂任何東西的密鑰。任何將檔案重新命名、開啟該部分並刪除 <sheetProtection> 該行的人,都能讀取和編輯所有內容
因此,保護功能回答的是「阻止我的同事不小心破壞公式」,而不是「對有心人士保密這份資料」。這是用不同工具解決的不同問題。如果您需要機密性,您需要的是受 AES 保護的 XLSX 輸出中所涵蓋的活頁簿層級加密,那才會真正加密套件。工作表保護和活頁簿加密可以乾淨地組合在一起,但只有第二個才是真正的鎖。保持這條界線清晰,這頁剩下的內容就只是管線配置而已
15 種選項與 AllowOption 屬性
現在每個工作表都帶有一組 TXLSXSheetProtectionOption 值,描述在工作表受保護時,使用者仍然可以做什麼。這些成員一對一對應到 OOXML 屬性以及 Excel 對話方塊的核取方塊:
xlsxSpoEditObjects、xlsxSpoEditScenarios— 編輯繪圖物件與假設狀況分析情境xlsxSpoFormatCells、xlsxSpoFormatColumns、xlsxSpoFormatRows— 設定儲存格、欄、列的格式xlsxSpoInsertColumns、xlsxSpoInsertRows、xlsxSpoInsertHyperlinks— 插入欄、列、超連結xlsxSpoDeleteColumns、xlsxSpoDeleteRows— 刪除欄、列xlsxSpoSelectLockedCells、xlsxSpoSelectUnlockedCells— 將選取範圍移至鎖定或未鎖定的儲存格上xlsxSpoSort、xlsxSpoAutoFilter、xlsxSpoPivotTables— 排序範圍、使用自動篩選下拉式選單、操作樞紐分析表
您可以透過 TXLSXWorksheet 上帶有索引的 AllowOption 屬性來讀寫個別位元。AllowOption[Opt] = True 表示允許該動作;將其設為 False 則禁止。整個集合也可以透過 SheetProtectionOptions(一個純 Pascal 的 set of,型別為 TXLSXSheetProtectionOptions)一次存取,這樣您就可以儲存它、還原它或整批替換它
預設值很重要且是刻意的:一個新建立的工作表一開始會允許所有選項。建構函式會以完整範圍 [Low(TXLSXSheetProtectionOption)..High(TXLSXSheetProtectionOption)] 為 SheetProtectionOptions 提供種子 (seed)。您從那裡開始,透過排除您想禁止的動作來縮小範圍,而不是從無到有建立一個權限集合。這個選擇使得寫入器的編碼規則(如下所示)能與 Excel 的行為一致
保護工作表但開放排序和篩選
以下是一個端到端 (end to end) 的常見案例:保護一份完成的報表,使其版面配置無法被重塑,但讓讀者可以進行排序和篩選。請注意,Protect 和選項是獨立的。Protect 會將工作表切換到受保護的狀態,並儲存選用的密碼雜湊;它不會動到選項集合。您要分開調整 AllowOption,而這些切換會在工作表被保護並儲存後生效
var
wb: TXLSXWorkbook;
sh: TXLSXWorksheet;
begin
wb := TXLSXWorkbook.Create;
try
sh := wb.Sheets.Add('Protected');
sh.Cells[1, 1].Value := 'Region'; sh.Cells[1, 2].Value := 'Units';
sh.Cells[2, 1].Value := 'North'; sh.Cells[2, 2].Value := 120;
sh.Cells[3, 1].Value := 'South'; sh.Cells[3, 2].Value := 98;
// Protect with a password. This only sets the protected state + hash;
// the option set is left at its all-permitted default.
sh.Protect('HotXLS-2026');
// Narrow: keep sort + AutoFilter, forbid reshaping and reformatting.
sh.AllowOption[xlsxSpoSort] := True;
sh.AllowOption[xlsxSpoAutoFilter] := True;
sh.AllowOption[xlsxSpoFormatCells] := False;
sh.AllowOption[xlsxSpoFormatColumns] := False;
sh.AllowOption[xlsxSpoFormatRows] := False;
sh.AllowOption[xlsxSpoInsertRows] := False;
sh.AllowOption[xlsxSpoDeleteRows] := False;
if wb.SaveAs('protection.xlsx') <> 1 then
Writeln('SaveAs failed');
finally
wb.Free;
end;
end;
從該程式碼片段可以讀出兩件事。儘管 Sort 和 AutoFilter 的預設值都是 True,但這兩行仍然被明確寫出;這是給下一個維護者的文件,不是功能性需求。且因為預設值是寬容的,唯一會改變輸出檔案的行,是將選項設為 False 的那幾行。這不是這個 API 的偶然,而是 OOXML 傳輸格式 (wire format) 的展現,這也是下一節要談的
編碼規則:省略代表允許,attr=0 代表禁止
這是整個功能中唯一違反直覺的事實,而這正是手寫 <sheetProtection> 通常出錯的地方。在 OOXML 中,每個針對各個動作的屬性都是一個禁止旗標,而它的缺席代表允許。遺失的屬性表示允許該動作。寫為 "0" 的屬性表示在工作表受保護時禁止該動作。在格式正確的檔案中,沒有 formatCells="1" 來表示「允許設定格式」;您只要省略該屬性即可。(缺席屬性的預設值為 OOXML 的布林值預設值 true,而且這些屬性的命名,使得「true」表示相應的編輯動作是允許的。)
HotXLS 寫入器完全反映了這一點。它會發出 sheet="1" 以開啟保護,接著走訪選項集合,只為您設定為 False 的選項寫入 attr="0"。允許的動作對輸出沒有任何貢獻。因此,上一節的活頁簿在序列化後會變成類似這樣,只帶有被禁止的動作加上密碼雜湊:
// Conceptual output for the snippet above (attributes elided for brevity):
// <sheetProtection sheet="1"
// formatCells="0" formatColumns="0" formatRows="0"
// insertRows="0" deleteRows="0"
// password="...4-hex..."/>
// Note what is NOT there: no sort, no autoFilter, no selectLockedCells.
// Their absence is exactly what tells Excel those actions stay allowed.
如果您習慣了舊的寫死字串,並期望看到每個屬性都被拼寫出來,這看起來很稀疏,幾乎像是錯的。但它是正確的。一個列出 sort="1" 和 autoFilter="1" 的檔案對遵循標準的讀取器來說意思是一樣的,但 Excel 本身會寫入最小化的純禁止形式,而與之匹配能保持差異 (diffs) 最小,並讓雙向轉換 (round-trips) 變得無聊。objects 和 scenarios 屬性遵循相同的規則:預設允許,因此它們只有在您禁止它們時才會顯示為 "0",這與舊版無條件發出的 objects="1" scenarios="1" 正好相反
讀回保護設定:雙向轉換的保真度
一個只能寫但不能讀的權限模型是一扇單向門,常見的症狀就是「載入-編輯-儲存」循環會默默地放寬權限。HotXLS 解決了這個問題。當 ParseWorksheetXml 遇到 <sheetProtection> 元素時,它會將工作表設為受保護狀態、擷取密碼雜湊(如果存在),然後反向使用相同的慣例,將每個動作屬性解碼回 AllowOption:存在且等於 "0" 的屬性會禁止該動作;缺席的屬性則將選項保留在其允許的預設值
var
wb: TXLSXWorkbook;
sh: TXLSXWorksheet;
begin
wb := TXLSXWorkbook.Create;
try
wb.LoadFromFile('protection.xlsx');
sh := wb.Sheets[1]; // XLSX sheets are 1-based
if sh.IsProtected then
begin
Writeln('Protected; password hash present: ',
sh.SheetProtectHash <> '');
Writeln('Sort allowed: ', sh.AllowOption[xlsxSpoSort]);
Writeln('AutoFilter allowed: ', sh.AllowOption[xlsxSpoAutoFilter]);
Writeln('FormatCells allowed:', sh.AllowOption[xlsxSpoFormatCells]);
end;
finally
wb.Free;
end;
end;
載入寫入器產生的檔案,您會看到 Sort 和 AutoFilter 回傳為 True,FormatCells 回傳為 False — 您儲存的集合原封不動。這種對稱性就是重點:在受保護、部分允許的工作表中編輯一個儲存格並重新儲存,您沒有動到的那 14 個權限會存活下來,而不是崩塌回舊版全有或全無的預設值
實務注意事項與限制
在您將此功能接入報表管線之前,有幾件事值得了解:
- 密碼在設計上很弱。 XLSX 工作表保護儲存了一個 16 位元的舊式雜湊值(也就是 Excel 使用了數十年的同一個),保留在這裡是為了互通性。它能阻止意外的編輯;它無法抵禦攻擊者。請勿將其視為機密守護者。為了獲得真正的保護,請加密活頁簿
- 在保護前設定選項是可以的。 無論工作表目前是否受保護,都可以指派
AllowOption;這些切換只是描述一旦Protect生效時,保護功能會允許什麼。UnProtect會清除保護狀態和雜湊,但會為下一次保留您的選項集合 - 鎖定儲存格 (Locked) 語義依然適用。 保護功能只會阻擋對設定了
Locked屬性(活頁簿預設值)之儲存格的編輯。保留輸入區域的可編輯性是儲存格樣式 (cell-style) 的工作,而不是保護選項;這兩個層面在 Excel 中的組合方式與此相同 - 這是 XLSX 引擎。 選項模型反映了 XLS 引擎較舊的
Allow*屬性,但這裡的列舉和屬性名稱(xlsxSpo*、AllowOption)屬於lxHandleX中的TXLSXWorksheet。如果您也在相同的工作表上驅動列印版面配置,那麼保護與版面設定列印的演練涵蓋了這些設定如何與列印範圍和頁首並存,而資料驗證、自動篩選和表格則自然地與鎖定報表上維持開放的xlsxSpoAutoFilter搭配
細緻的保護模型以及其餘的 XLSX 讀寫引擎皆隨附於適用於 Delphi 與 C++Builder 的 HotXLS 元件 中;產品網頁包含了完整的工作表 API,包含完整的保護選項參考