Excel 拒绝打开列表数据验证中内联列表超过 255 字符的 XLSX 工作簿,哪怕这文件是 Excel 自己存的也不行。HotXLS(面向 Delphi 与 C++Builder 的原生 Excel 组件)能加载这些列表,并把跨表列表规则建模为 x14 扩展,但治本的办法到哪里都一样:把长列表放进工作表范围或定义名称
这类 bug 报告通常来自客户,而不是你的测试。某个模板的 Region 或 Product 列,下拉从八个条目长到六十个。别人放在共享盘上的 XLSB 副本一切正常。然后某个导出任务把同一条规则写进 XLSX,Excel 用修复提示或断然拒绝回应你。你的代码什么都没变,除了一根字符串的长度——这正是为什么值得在它找上门之前弄懂这条边界
255 字符的列表上限到底卡在哪?
上限卡在 XLSX 里列表验证的内联形态上——即存进 formula1 的带引号逗号分隔字符串——而且 Excel 在打开文件时就强制执行,不是只在数据验证对话框里键入列表时才管。对着 Excel 实测给出了干净的结果:253 字符的列表能打开,260 字符和 421 字符的列表被拒。421 字符那个案例最有启发意义,因为它来自 Excel 自己生成的工作簿。Excel 会写出自己都读不回去的文件
schema 在这里救不了你。ECMA-376 Part 1 §18.3.1.32 定义 dataValidation 元素及其公式时没提 255 字符的天花板,所以文件可以 schema 有效却仍被拒。这是应用约束,唯一可靠的参考是 Excel 自己的行为
HotXLS 不替你执行这个上限。TXLSXWorksheet.AddListValidation 给条目加引号并按原样存储,既不截断也不拒绝长列表,所以 400 字符的列表会按你的要求原样进保存的 XLSX,然后 Excel 拒绝这个结果。这是刻意的选择——悄悄裁剪列表会以没人察觉的方式破坏数据——但长度检查因此落在你这一侧。一个小助手能让它诚实:
const
// Excel 能打开 253 字符的列表,拒绝 260 和 421 字符的
// 留点余量,别贴着精确边缘做
MaxInlineListChars = 250;
function InlineListLength(const AItems: array of WideString): Integer;
var
I: Integer;
begin
Result := 0;
for I := 0 to High(AItems) do
Inc(Result, Length(AItems[I]));
if Length(AItems) > 1 then
Inc(Result, High(AItems)); // 分隔用的逗号
end;
把内嵌双引号按两个字符算,因为 AddListValidation 会双写它们以保持公式字符串有效
为什么 XLSB 装得下 XLSX 装不下的列表
XLSB 把内联列表存成单个 PtgStr 字符串 token,长度是 16 位字段,所以二进制格式没有 255 字符的墙。一个 60 条、419 字符的原生样例装在一个 token 里,在 Excel 里来去自如。同一列表放进 XLSX,就是上文那个 421 字符案例
把对话框那条 255 字符上限套到 XLSB 上的加载器会拒掉这个文件。HotXLS 的 XLSB 验证 token 检查改用 32,767 字符封顶字符串 token,与 Excel 单元格字符串同一上限。有两点限定值得明说。加载器还把完整的验证公式封在 16,384 字节,所以单个内联列表的实际上限比 token 检查单独显示的要低。另外,PtgStr 里以 NUL 字符分隔的列表不在支持范围之内,会被报告为不支持而不是瞎猜
坑在转换那一步。XLSB 里健康无恙的长列表,一旦有任何东西——包括 HotXLS——把工作簿存成 XLSX,就变成被拒的文件。如果你的管线转换客户的 XLSB 文件,请在 XLSX 保存之前对每条列表规则跑长度检查,别等客户来报
指向另一个工作表的列表会怎样?
来源在另一个工作表上的列表,Excel 存在 x14:dataValidations 扩展里,而不是普通 dataValidations 元素。规则位于工作表的 extLst 内、扩展 URI {CCE6A557-97BC-4b89-ADB6-D9C93CAAB3DF} 之下(定义见 [MS-XLSX]),其来源引用是 xm:f 元素,如 Lists!$A$2:$A$61。XLSB 把同样的规则放在自己的扩展记录里
HotXLS 把这些 XLSX 扩展规则解析进类型化的 DataValidations 集合,并用 TXLSXDataValidation.IsExtension 给每条打标。跨表列表因此与普通规则并肩出现,范围、类型和公式都可读,而且能挺过编辑、保存和重开。保存时,写入器把扩展规则挡在普通元素之外,在 worksheet 尾部其 schema 位置重建 x14 扩展。两条公式都保留,所以用 between 或 notBetween 的整数、小数、文本长度、日期和时间规则两个边界都在
类型化的扩展 profile 覆盖整条规则。除 type、operator、allowBlank、两个 show 标志、范围和两条公式之外,扩展规则还把输入提示标题与文本、错误警报标题与文本、警报样式、IME 模式和隐藏下拉设置完整带过一次加载与重存,转义规则与普通验证一致
uses
SysUtils, lxHandleX, lxDiagnostics;
procedure AuditListRules(const FileName: WideString);
var
Book: TXLSXWorkbook;
Sheet: TXLSXWorksheet;
Dv: TXLSXDataValidation;
S, I: Integer;
begin
Book := TXLSXWorkbook.Create;
try
if Book.Open(FileName) <> 1 then
raise Exception.Create('Cannot open ' + FileName);
for S := 1 to Book.Sheets.Count do // Sheets[] 是 1 基的
begin
Sheet := Book.Sheets[S];
for I := 0 to Sheet.DataValidations.Count - 1 do
begin
Dv := Sheet.DataValidations[I];
if Dv.ValidationType <> xlsxDvList then
Continue;
if Dv.IsExtension then
Writeln(Sheet.Name, ' ', Dv.Range, ': x14 list from ', Dv.Formula1)
else if (Dv.Formula1 <> '') and (Dv.Formula1[1] = '"') and
(Length(Dv.Formula1) - 2 > MaxInlineListChars) then
Writeln(Sheet.Name, ' ', Dv.Range, ': inline list of ',
Length(Dv.Formula1) - 2, ' chars, refused by Excel as XLSX');
end;
end;
finally
Book.Free;
end;
end;
读取下拉背后的条目、选中条目以及按这些规则检查键入输入,见 用 HotXLS 读取下拉项并校验输入;本文只谈存储格式
Excel 与 HotXLS 如何对待未知的验证类型
dataValidation 的 type 属性装着无法识别的 token 时,Excel 把它当作 none 类型的规则打开,保留其公式,保存时改写成不带 type 属性的裸 dataValidation 元素,丢弃未知属性。这类 token 来自手改 XML、第三方生成器和未来的 Office 功能,Excel 的答案是容忍,不是拒绝
HotXLS 遵循同样的容忍。直接丢掉规则会在重存时无声无息地失去它,所以读取器把它保留为带公式和范围的 xlsxDvNone,并为每条被规范化的规则记录一条 code 1307 的警告诊断,即 lxDiagnostics 里的 xlsDiagnosticValidationTypeNormalized 常量。保存时 none 规则写成裸元素,与 Excel 产出同形。none 规则什么都不校验,所以这条诊断是在提醒你:作者以为在生效的规则已经不再约束输入
for I := 0 to Book.Diagnostics.Count - 1 do
if Book.Diagnostics[I].Code = xlsDiagnosticValidationTypeNormalized then
Writeln('Untyped rule kept on sheet ', Book.Diagnostics[I].SheetName,
': ', Book.Diagnostics[I].Message);
为什么 Excel 会在一次往返后拒绝工作簿?
重放扩展片段用了新文件从未声明的命名空间前缀时,Excel 会拒绝重存的工作簿,因为结果不是良构 XML。扩展片段动辄携带 xr:uid 这类带前缀属性,而 xmlns:xr 声明在原 worksheet 根上、不在片段内。把片段原封不动拷进缺那条声明的新 worksheet 根,命名空间规则在 Excel 看第一个单元格之前就已经破了
HotXLS 在捕获这一步处理此事。读取时,它扫描每个重放片段里片段自身未声明的元素与属性前缀,经读取器把每个前缀解析到其 URI,并在保存时把这些声明写上 worksheet 根。流式保存路径经同一例程构建根,所以两种保存模式都产出良构输出,重存的文件在 Excel 里重开时每条验证规则都在,包括重放的跨表 x14 列表。HotXLS 如何携带它未建模的 extLst 内容,全貌见 主题、extLst 与 calcChain 的无损 XLSX 往返;Excel 修复提示背后的通用规则见 Excel 为什么弹修复提示
长列表该放进范围或定义名称
对任何可能增长的列表,稳健的设计是把条目存进工作表单元格,再经定义名称让验证指向它们。范围来源在两种格式里都没有 255 字符限制,条目可以不碰规则地编辑,定义名称还让规则留在普通 dataValidation 元素里——即使单元格在另一张表上——文件的每个消费方都读得到
// Lists 是专用工作表,例如 Book.Sheets.Add('Lists')
function AddListRule(Book: TXLSXWorkbook; Target, Lists: TXLSXWorksheet;
ACol: Integer; const ARange, AName: WideString;
const AItems: array of WideString): Integer;
var
I: Integer;
ColRef: WideString;
begin
if InlineListLength(AItems) <= MaxInlineListChars then
begin
Result := Target.AddListValidation(ARange, AItems);
Exit;
end;
for I := 0 to High(AItems) do
Lists.WriteAutoValue(I + 1, ACol, AItems[I]); // 1 基的行、列
ColRef := Chr(Ord('A') + ACol - 1); // 列 A..Z
Book.DefinedNames.Add(AName, Lists.Name + '!$' + ColRef + '$1:$' +
ColRef + '$' + IntToStr(Length(AItems)));
// 开头的 '=' 标记引用;没有它名称会被当文本加引号
Result := Target.AddListValidation(ARange, '=' + AName);
end;
那个开头的等号很重要。AddListValidation 把以 = 开头的参数当引用并剥掉符号,把含 ! 的参数不加引号当作范围,其余一律按内联列表加引号。传不带符号的 'StatusList',你会得到一个字面显示 StatusList 的单项下拉。两个小点:WriteAutoValue 按 Excel 单元格输入的方式转换文本,所以 TRUE 这样的条目会变成布尔值;含空格的表名在引用里需要单引号
对必须不带名称直指另一张表的规则,在 AddListValidation 返回的索引处的规则上设 IsExtension := True,HotXLS 就把它写成 x14 扩展,与 Excel 自己用的形态相同。定义名称这条路更简单,而且对从不解析扩展的工具也持续有效,所以它是这里的默认推荐
本文描述的全部行为随 HotXLS Delphi Excel component(面向 Delphi、C++Builder 与 Lazarus)提供,产品页列出了支持的验证类型、文件格式和完整诊断参考