技術文章

HotXLS 清單驗證:255 字元上限與 x14 擴充

Excel 拒絕開啟清單資料驗證帶有超過 255 字元行內清單的 XLSX 活頁簿,而且連 Excel 自己存出來的檔都一樣。HotXLS 這個給 Delphi 與 C++Builder 的原生 Excel 元件能載入這些清單,並把跨工作表的清單規則建成 x14 擴充,但治本的修法到處都一樣:把長清單放進工作表範圍或定義名稱

這類 bug report 通常來自客戶,不是來自你的測試。某個範本的 Region 或 Product 欄,下拉選項從八項長到六十項;別人留在共用磁碟上的 XLSB 副本一切正常;然後匯出作業把同一條規則寫進 XLSX,Excel 回你一個修復提示或直接拒收。你的程式碼什麼都沒改,改變的只有一個字串的長度——這正是這道邊界值得在被它咬到之前先搞懂的原因

255 字元清單上限實際作用在哪裡?

上限作用在 XLSX 清單驗證的行內形式上——存在 formula1 裡那條帶引號、逗號分隔的字串——而且 Excel 在開檔時就強制執行,不是只在資料驗證對話框裡輸入時才管。直接對 Excel 測試得到乾淨的結果:253 字元的清單開得了,260 字元與 421 字元的清單被退回。421 字元那個案例最有教育意義,因為它來自 Excel 自己寫出的活頁簿——Excel 會寫出一份 Excel 自己讀不回去的檔案

Excel 拒收超過 255 字元的 XLSX 行內清單,而 XLSB PtgStr token 帶得動長清單,以 HotXLS Delphi 容器結果矩陣呈現
這道牆是按容器而定的應用程式限制:Excel 拒收它自己都寫得出的 XLSX 行內形式,而 XLSB token 與範圍支撐的清單帶得動同樣的項目

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 檢查把字串 token 上限定在 32,767 字元,與 Excel 儲存格字串相同的天花板。有兩個但書值得直說:載入器同時把整條驗證公式封頂在 16,384 位元組,所以單一行內清單的實際上限比 token 檢查單獨暗示的低;以 NUL 字元分隔、裝在 PtgStr 裡的清單落在支援範圍之外,會被回報為不支援而不是靠猜

陷阱坐在轉換這一步。XLSB 裡完全健康的長清單,任何東西——HotXLS 也在內——把那份活頁簿存成 XLSX 的瞬間就變成被拒的檔案;如果你的管線會轉換客戶的 XLSB 檔,請在 XLSX 存檔之前對每條清單規則跑長度檢查,而不是等客戶回報

指向另一張工作表的清單會怎麼樣?

來源坐在別張工作表上的清單,Excel 存在 x14:dataValidations 擴充裡,不是普通的 dataValidations 元素;規則住在工作表 extLst 內、擴充 URI {CCE6A557-97BC-4b89-ADB6-D9C93CAAB3DF} 之下(定義於 [MS-XLSX]),來源參照是 Lists!$A$2:$A$61 這樣的 xm:f 元素;XLSB 把同一條規則放在它自己的擴充 record 裡

HotXLS 把這些 XLSX 擴充規則解析進型別化的 DataValidations 集合,並以 TXLSXDataValidation.IsExtension 標記每一條。跨工作表清單因此與普通規則並列出現,範圍、類型與公式都讀得到,而且在編輯、存檔、重開之後存活。存檔時,寫出器讓擴充規則遠離普通元素,並在工作表尾端按 schema 位置重建 x14 擴充;兩條公式都保留,所以使用 between 或 notBetween 的整數、小數、文字長度、日期與時間規則兩個邊界都在

Delphi 搭配 HotXLS 的跨工作表清單驗證 x14 擴充解剖:規則位於工作表 extLst、帶 xm:f 來源,解析進型別化集合
跨工作表清單規則活在 x14 擴充裡,HotXLS 保持它們型別化、以 IsExtension 標記,並在存檔時於工作表尾端重建

型別化的擴充 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,連同公式與範圍,並為每條被正規化的規則記一筆代碼 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 宣告坐在原工作表的根元素上,不在片段裡;把片段原樣複製進一個缺那條宣告的新工作表根,XML 命名空間規則在 Excel 看任何一格之前就已經壞了

HotXLS 在擷取這一步處理。讀取時,它掃描每個重放片段中片段自身未宣告的元素與屬性前綴,透過讀取器把每個前綴解析到它的 URI,並在存檔時把那些宣告寫上工作表根元素;串流存檔路徑也經由同一個常式建立根元素,所以兩種存檔模式都產出格式正確的輸出,重存的檔案在 Excel 裡帶著每一條驗證規則重開,包括重放的跨工作表 x14 清單。HotXLS 怎麼承載它沒有建模的 extLst 內容,整體圖像見主題、extLst 與 calcChain 的無損 XLSX 往返,Excel 修復提示背後的通用規則則見Excel 為什麼顯示修復提示

長清單該放範圍或定義名稱

對任何可能繼續成長的清單,穩健的設計是把項目放進工作表儲存格,驗證透過定義名稱指向它們。範圍來源在兩種格式裡都沒有 255 字元限制,項目可以在不動規則的情況下編輯,而定義名稱讓規則留在普通 dataValidation 元素裡——即使儲存格坐在另一張工作表上,所有消費這份檔案的工具都讀得到

Delphi 搭配 HotXLS 的清單驗證來源決策:短清單留在行內,長清單移到經定義名稱參照的範圍
在規則建立之前跑長度檢查;開頭的 = 正是讓定義名稱成為參照、而非單項文字清單的關鍵
// 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 使用,產品頁列出支援的驗證類型、檔案格式與完整診斷參考