技術文章

HotXLS:Delphi 中的工作表保護、頁面設定與列印

三組工作表設定與儲存格值無關,卻與檔案一旦離開你的程式碼後的行為息息相關。工作表保護決定了你交出活頁簿後,使用者能編輯哪些儲存格。頁面設定固定方向、紙張大小與邊界。列印設定(重複的標題列、縮放與手動分頁)則控制任意長度的格線如何落在紙上。這三者在檢視器中用肉眼審視資料時都不會現身,而它們一旦錯了,在現場都是悄悄出問題。HotXLS,一個原生的 Delphi 與 C++Builder 試算表程式庫,為 .xls 與 .xlsx 公開了完整的那一面,這也意味著它重現了烙印在那一面裡的每一條違反直覺的 Excel 規則

那些規則中的第一條,幾乎在每個人第一次保護產生的工作表時都會絆倒他們。呼叫 Protect,突然間沒人能在任何儲存格裡打字,包括你圍繞著它建置活頁簿的那些輸入欄。你的程式碼什麼也沒碰那些欄,而這正是它發生的原因

每個儲存格生來就是鎖定的

ECMA-376 把 locked 定義為儲存格格式記錄的一部分,而不是保護本身的屬性,而且它預設為 true。工作表保護只不過是讓這個旗標得以強制的開關。所以整個格線從它存在的那一刻就帶著一個鎖定旗標,蟄伏著,而對 Protect 的呼叫會一次把它們全部啟動。修復方法是有意識地設定順序:建置版面、明確解鎖使用者必須編輯的範圍、最後才保護

HotXLS 在 Delphi 的保護順序圖解:每個儲存格建立時 locked 即為 true,先用 SetLocked 解鎖輸入範圍,最後呼叫 Sheet.Protect,讓解鎖的儲存格仍可編輯
儲存格出廠即鎖定,先解鎖輸入範圍、最後才呼叫 Protect,它們才保得住可編輯
Book := TXLSXWorkbook.Create;
try
  Sheet := Book.Sheets.Add('Timesheet');
  // ... 在此寫入標題列、名稱欄與費率公式 ...
  Sheet.Range['B2:B50'].SetLocked(False);         // 員工工時輸入的地方
  Sheet.Range['F2:F50'].SetFormulaHidden(True);   // 讓費率計算保持隱密
  Sheet.Protect('review-2026');                   // 現在鎖定旗標產生效用
  Book.SaveAs('timesheet.xlsx');
finally
  Book.Free;
end;

SetFormulaHidden 做的是另一件獨立且容易忽略的事:在保護啟用期間,儲存格仍顯示其計算後的值,但資料編輯列什麼也不顯示。這在一個公式內嵌了計費費率、利潤或評分權重、而你不想交給每一個點選合計的收件者時很重要。在 XLS 外觀層上,同樣的意圖是透過 IXLSRange.LockedFormulaHidden 以範圍為單位表達的。那裡的工作表還帶有十五個 Allow* 旗標(AllowSortAllowAutoFilterAllowFormatCells 等等),所以一份受保護的工作表仍能被排序與篩選,而不是被凍結成一份密封的附件

保護密碼實際保護了什麼

兩種格式都把工作表與活頁簿的保護密碼儲存為一個 4 位十六進位的舊式雜湊。十六位元意味著無數字串與任何給定密碼碰撞,而移除工具一次搜尋就到。請把保護當作防止意外編輯的安全帶,而不是存取控制。它是阻止審查者把公式欄打字覆蓋過去的正確工具,也是任何涉及「機密」這個詞的事情的錯誤工具

往上一層,XLSX 外觀層的 ProtectWorkbook 會鎖定活頁簿結構,這會阻止加入、重新命名、刪除或重排工作表。每當工作表清單本身就是一份與某個依名稱或位置索引工作表的下游解析器的合約時,就設定它。一個被重新命名的工作表,會與一個被刪除的欄位一樣確定地弄壞另一端的匯入。XLS 外觀層以活頁簿層級的 TXLSWorkbook.Protect 與每個工作表的 Protect 呼叫映照了這種分層,外加一個供需要在修改任何東西之前檢查一份繼承檔案的程式碼使用的 isProtected 屬性

當需求是真正的機密性時,機制會完全改變。SaveAsEncrypted 會在 ECMA-376 Standard Encryption 方案下產生一個 AES 加密的封包,深入涵蓋於AES 保護的 XLSX 輸出指南中,而舊式的 XLS 外觀層則透過 EncryptionPasswordOpen 的密碼多載,寫入與讀取 RC4 加密的 .xls 檔案。這個差別不是學術上的。一份受保護的工作表以明文傳輸,所以任何 zip 工具都能讀取它的儲存格值,而一個加密封包沒有密碼就無法讀取。一條寫著「薪資檔案必須受保護」的稽核要求,幾乎總是指加密,無論它恰好用什麼詞彙

圖解對比:HotXLS 工作表保護存 16 位元舊式雜湊、儲存格值以明文保留且任何 zip 工具都讀得到,與 SaveAsEncrypted 的 AES 輸出——沒有密碼就無法讀取
工作表保護是防手滑的安全帶,明文值依然可讀;只有 AES 加密才藏得住內容

頁面設定是文件合約的一部分

列印行為在螢幕上是看不見的,這也是為什麼它出貨時經常是壞的。在客戶列印活頁簿、或為稽核者把它匯出成 PDF 的那一刻,邊界、縮放與重複的標題就變成了沒人測試過的功能需求。在 XLSX 外觀層上,這些設定直接掛在工作表上:

Sheet.PageLandscape := True;
Sheet.PaperSize := xlsxPaperA4;
Sheet.SetPageMargins(0.5, 0.5, 0.75, 0.75, 0.3, 0.3);
Sheet.CenterHeader := 'Monthly Timesheet';
Sheet.RightFooter := 'Page &P of &N';
Sheet.PrintArea := '$A$1:$F$60';     // 裸參照:這裡沒有工作表名稱
Sheet.PrintTitleRows := '$1:$1';     // 標題列在每一頁重複
Sheet.FitToWidth := 1;
Sheet.FitToHeight := 0;              // 資料增長時朝下延伸
Sheet.PrintGridlines := False;

其中有兩行藏著陷阱。頁首與頁尾字串使用 Excel 的格式化代碼:&P 代表目前頁碼、&N 代表總頁數,加上 &L&C&R 來明確定址三個區段。另一個陷阱是 PrintArea,它刻意接受一個裸參照。HotXLS 以不帶限定詞的方式儲存它,並在寫入檔案時加上工作表名稱作為前綴,所以你自己傳入 'Timesheet!$A$1:$F$60' 會產生一個雙重限定、格式錯誤的參照。同樣的謹慎往下一層也適用:列印區域與列印標題是以內建已定義名稱 _xlnm.Print_Area_xlnm.Print_Titles 保存的,所以絕不要透過 DefinedNames 手動加入 _xlnm.* 項目,否則兩個機制會在同一個位置上打架

能在生產資料量下存活的縮放

FitToWidth := 1 搭配 FitToHeight := 0 讀起來是「永遠把欄位塞進一頁寬,然後資料需要多少頁就往下長多少」,而它是任何列數會變動的報表的正確預設值。陷阱在於拿一份三十列的測試檔案去調校一個固定百分比或一組 fit-to-page 設定:把同樣的設定餵進六百列生產資料,輸出要嘛爆炸成幾十頁被裁切的頁面,要嘛縮小到無法閱讀。縮放寬度、讓長度增長,並透過 PrintTitleRows 重複標題列,好讓第十七頁本身仍然可讀

HotXLS 在 Delphi 的列印縮放圖解:FitToWidth 設 1 讓每一頁都維持一頁寬、FitToHeight 設 0 讓頁面向下增長、PrintTitleRows 重複表頭帶,以及 ClearAllPageBreaks 之後重新產生分頁
FitToWidth 1 配 FitToHeight 0 讓每頁都是一張紙寬,重複的標題列與重新生成的分頁維持可讀性

手動分頁遵循與產生活頁簿中其他一切相同的重新產生紀律。AddRowBreak(BeforeRow) 會在一個區段邊界之前開始新頁,但當產生器重新執行而列位移時,一個過時的分頁就會落在表格中間。請先呼叫 ClearAllPageBreaks,再重新加入從產生器自己的列計數器算出的分頁,而不是修補舊位置。在 XLS 外觀層上,對應的控制項位於 Sheet.PageSetup(方向、紙張大小、邊界、頁首與頁尾字串、fit-to-pages),並以 RepeatRowsRepeatColumns 涵蓋列印標題

在客戶之前先檢查結果

保護與列印的錯誤有一個共同特性:它們用手動驗證微不足道,卻幾乎從未被驗證。在 Excel 中開啟產生的檔案,花九十秒在它上面。在某個輸入儲存格打字,確認它接受按鍵;在某個鎖定的儲存格打字,確認保護提示出現;檢查一個隱藏公式是否讓資料編輯列空白。然後針對生產規模的資料集而非三十列的樣本執行列印預覽,讀出頁數、重複的標題列與頁尾編號。預覽是那個回本的步驟,因為列印幾何依賴那些沒有螢幕繪製的設定,而除了一台實體印表機之外,它是縮放錯誤唯一會現身的地方

最後一個設定補全了審查。FreezePane(ACol, ARow) 讓標題區塊在審查者捲動時保持可見。那是螢幕行為而非列印行為,但審查者會一次審判整份交付物。而一份以設計者維護版面起家的活頁簿,大部分都能免費得到這些:範本報表產生工作流程把頁面設定保留在範本裡——那是某個人對著一台真實印表機調校出來的地方——而把填入資料、並在版面定下來後重新套用保護的工作留給程式碼

HotXLS 是供 Delphi 與 C++Builder 使用的原生 Object Pascal 試算表程式庫;完整的保護與頁面設定 API 參考位於HotXLS Delphi Component 產品頁