三组工作表设置与单元格的值毫无关系,但与文件离开您的代码后的行为密切相关;工作表保护决定了用户在您交付工作簿后可以编辑哪些单元格;页面设置固定了方向、纸张大小和页边距;打印设置(重复标题行、缩放和手动分页符)控制了任意长度 of 网格在纸上的呈现方式;在查看器中观察数据时,这三者都不会显现出来,而一旦出错,这三者在实际使用中都会静默失效;HotXLS 作为原生的 Delphi 和 C++Builder 电子表格库,公开了 .xls 和 .xlsx 的完整设置面,这意味着它也重现了内置在该设置面中的每一条反直觉的 Excel 规则
当人们第一次对生成的工作表进行保护时,其中的第一条规则几乎会让每个人都踩坑:调用 Protect 后,突然间任何人都无法在任何单元格中输入内容了,包括您围绕其构建工作簿的输入列;您的代码中没有任何地方触及过这些列,而这恰恰就是原因所在
每个单元格在创建时都是被锁定的
ECMA-376 将 locked 定义为单元格格式化记录的一部分,而不是保护本身的一个属性,并且它默认值为 true;工作表保护仅仅是使该标志强制执行的开关;因此,整个网格从存在的那一刻起就携带了一个休眠的锁定标志,而对 Protect 的调用会一次性激活所有这些标志;解决方法是刻意设置顺序:构建布局、显式解锁用户必须编辑的范围,最后进行保护
Book := TXLSXWorkbook.Create;
try
Sheet := Book.Sheets.Add('Timesheet');
// ... header row, name column, and rate formulas written here ...
Sheet.Range['B2:B50'].SetLocked(False); // staff type hours here
Sheet.Range['F2:F50'].SetFormulaHidden(True); // keep the rate math private
Sheet.Protect('review-2026'); // now the lock flags bite
Book.SaveAs('timesheet.xlsx');
finally
Book.Free;
end;
SetFormulaHidden 执行了一项独立且容易被忽略的操作:当保护激活时,单元格仍显示其计算后的值,但公式栏显示为空;当公式中嵌入了账单费率、利润率或评分权重,而您不希望将其提供给单击总计单元格的每个收件人时,这一点就很重要;在 XLS 接口上,相同的意图通过 IXLSRange.Locked 和 FormulaHidden 在每个范围内进行表达;工作表还携带了 15 个 Allow* 标志(AllowSort、AllowAutoFilter、AllowFormatCells 等),因此受保护的工作表仍可被排序和过滤,而不是被冻结成一个密封的展品
保护密码实际保护了什么
这两种格式都将工作表和工作簿的保护密码存储为 4 位十六进制数字的传统哈希值;16 位意味着无数个字符串都可以与任何给定的密码相撞,而且密码移除工具只需搜索一下即可找到;应将保护视为防范意外编辑的“安全带”,而不是“访问控制”;对于阻止审阅者在公式列上输入内容,它是正确的工具,而对于任何涉及“机密”一词的情况,它是错误的工具
往上一级,XLSX 接口上的 ProtectWorkbook 会锁定工作簿结构,防止添加、重命名、删除或重新排序工作表;只要工作表列表本身是与根据名称或位置对工作表进行索引的下游解析器达成的契约,就应设置该选项;另一个重命名了的工作表会损坏另一侧的导入,这与删除列的后果完全相同;XLS 接口在工作簿级别使用 TXLSWorkbook.Protect 并在每个工作表级别调用 Protect,此外还为需要在修改任何内容前检查继承文件的代码提供了一个 isProtected 属性
当需求是真正的机密性时,机制就会完全改变;SaveAsEncrypted 在 ECMA-376 标准加密方案下生成 AES 加密包,在AES 保护的 XLSX 输出指南中进行了深入介绍;传统的 XLS 接口通过 EncryptionPassword 和 Open 的密码重载来写入和读取 RC4 加密的 .xls 文件;这种区别并不是学术性的:受保护的工作表是以明文形式传输的,因此任何 zip 工具都可以读取其单元格的值,而加密包在没有密码的情况下是无法读取的;一条写着“工资单文件必须被保护”的审计意见几乎总是意味着“加密”,无论它恰巧使用了什么词汇
页面设置是文档契约的一部分
打印行为在屏幕上是不可见的,这就是为什么它在发布时经常失效的原因;当客户打印工作簿或将其导出为 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'; // bare reference: no sheet name here
Sheet.PrintTitleRows := '$1:$1'; // header row repeats on every page
Sheet.FitToWidth := 1;
Sheet.FitToHeight := 0; // grow downward as the data grows
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 的组合意为“始终将列自适应显示在一页的宽度内,然后根据数据需要向下延伸为多页”,这是行数多变任何报表的正确默认设置;陷阱在于对照一个 30 行的测试文件来调整固定的百分比或“适应页面”配对:将相同的设置提供给 600 行的生产数据,输出要么会爆炸成几十个被裁剪过的页面,要么会缩小到无法看清;缩放宽度,允许长度增长,并通过 PrintTitleRows 重复表头行,这样第 17 页依然可以独立阅读
手动分页符遵循与生成的工作簿中其他所有内容相同的重生成原则;AddRowBreak(BeforeRow) 在章节边界前开始新的一页,但当生成器重新运行且行发生平移时,陈旧的分页符就会落在表格中间;首先调用 ClearAllPageBreaks,然后重新添加根据生成器自身的行计数器计算出的分页符,而不是修补旧的位置;在 XLS 接口上,等效的控制活在 Sheet.PageSetup 上(方向、纸张大小、页边距、页眉和页脚字符串、自适应页数),其中 RepeatRows 和 RepeatColumns 涵盖了打印标题
在客户检查之前先检查结果
保护和打印的 bug 共享一个特性:它们很容易通过手动验证,但几乎从未被验证过;在 Excel 中打开生成的文件,并在上面花上 90 秒时间;输入一个输入单元格,确认它接受击键,输入一个锁定单元格,确认出现保护提示,检查隐藏的公式是否将公式栏保留为空;然后针对生产规模的数据集(而不是 30 行的样本)运行打印预览,并读取页数、重复的标题行和页脚编号;预览是能带来回报的步骤,因为打印几何形状取决于没有屏幕渲染的设置,而且除了物理打印机之外,它是缩放错误唯一可见的地方
最后一项设置使得审查更加完善:FreezePane(ACol, ARow) 在审阅者滚动时保持表头块可见;这是屏幕行为而不是打印行为,但审阅者是同时对整个交付成果进行评判的;作为一个以设计师维护布局开始的工作簿,绝大部分内容都是免费获得的:模板报表生成工作流将页面设置保留在模板中(由人针对真实的打印机进行了调整),剩下的代码用于填充数据,并在布局稳定后重新应用保护
HotXLS 是适用于 Delphi 和 C++Builder 的原生 Object Pascal 电子表格库,完整的保护和页面设置 API 参考位于 HotXLS Component 产品页面中