HotXLS 把每個 BIFF8 AutoFilter 條件存成一條 AUTOFILTER 記錄,裡面帶著兩個 10 位元組的 DOPER 結構,而 DOPER 的型別就決定了 Excel 怎麼做比較。從 v2.384.45 起,TXLSWorksheet.ApplyAutoFilter 會把 '>=100' 這類比較寫成 IEEE 數字 DOPER,於是 Excel 用數值比對儲存格,而不是把東西當文字比。促成這次修改的 bug 回報很短,也讓人很火大:夜間排程的匯出對金額欄套了一個篩選,檔案開起來毫無異狀,下拉選單的箭頭也顯示了條件,篩選結果卻是零列。檔案沒有損壞,位元組是完全合法的 BIFF8,只是合法錯了方向——而這正是本文要帶您走的整類失敗,連同 v2.384.18 修掉的兩個更早的位元組級錯誤一起
BIFF8 AutoFilter 實際上存了什麼?
BIFF8 的 AutoFilter 是三種記錄型別的組合,不是一種,而且只有每欄各一的那種記錄才放條件。AUTOFILTERINFO($009D,[MS-XLS] §2.4.8)記錄篩選範圍涵蓋幾欄。FILTERMODE($009B)是個沒有內容的標記,HotXLS 只有在至少一個欄位有生效條件時才會寫出它。然後每個生效的欄位都有自己的一條 AUTOFILTER 記錄($009E,§2.4.6):從零起算的欄位索引、一個低兩位是 wJoin 的 grbit 字組、剛好各 10 位元組的兩個 DOPER,外加一段選填的尾段,放字串 DOPER 的字元。磁碟上的欄位索引從零起算,ApplyAutoFilter 的 API 卻從 1 數起,第一次在十六進位傾印裡找記錄時這件事就會咬您一口。每個 DOPER 的第一個位元組 vt 說明後面跟的是哪種運算元:
$04是 IEEE 754 倍精準數,放在其餘 8 個位元組裡,Excel 的數值比較就是這樣存的$06是字串,長度放在單一cch位元組裡,字元本體則推進記錄尾段$08是 Bes 值,布林或錯誤碼裝在兩個位元組裡$0C與$0E不帶運算元,分別代表符合全部空白與符合全部非空白
第二個位元組 grbitSgn 放比較運算子:1 到 6 對應 <、=、<=、>、<> 與 >=。事後 HotXLS 透過 AutoFilterColumns 讓這兩個位元組都看得到,其成員以 TXLSAutofilterDOPER 物件暴露 Criteria1 與 Criteria2,帶著 DataType、grbitSgn 與 Value,所以您可以對即將寫出的內容下斷言,不必靠猜
為什麼 '>=100' 篩選在 Excel 中比不到任何一列?
篩選比不到東西,是因為運算元被存成文字,而 Excel 拿字串 DOPER 跟儲存格做的是文字比較。v2.384.45 之前,lxFilter.pas 裡的 CreateFilterDoper 會正確剝掉 >= 前綴、把符號設成 6,然後一律造出裝著字元 100 的 vtString DOPER。放著 250 的數值儲存格永遠不會滿足對 "100" 的文字比較,於是每一列都被濾掉。沒有例外、沒有診斷訊息,Excel 也不會跳出修復提示。v2.384.45 起的規則刻意收得很窄:條件以比較運算子開頭,且剩餘部分按 invariant-culture 規則解析得出數字,HotXLS 就用同一個符號寫 vtIEEENumber DOPER。不帶運算子的裸值維持字串形式,因為 Excel 自己從下拉清單挑項目時就是這樣存的
var
Wb: IXLSWorkbook;
Sh: TXLSWorksheet;
Doper: TXLSAutofilterDOPER;
begin
Wb := TXLSWorkbook.Create;
Sh := Wb.Sheets.Add;
Sh.Cells[1, 1].Value := 'Region';
Sh.Cells[1, 2].Value := 'Amount';
Sh.Cells[2, 1].Value := 'North';
Sh.Cells[2, 2].Value := 250;
// 欄位 2 = A1:B100 的第二欄(API 側從 1 數起)
Sh.ApplyAutoFilter('A1:B100', 2, '>=100');
Doper := Sh.AutoFilterColumns.Find(2).Criteria1;
// v2.384.45+:DataType = 4(IEEE 數字),grbitSgn = 6(>=)
// 修正前:DataType = 6(字串),什麼都比不到
Assert(Doper.DataType = 4);
Wb.SaveAs('orders.xls');
end;
解析這一步還藏著剩下的利邊。運算元會走 TryStrToFloat、以句點作小數點,所以 '>=1.5' 會變成數字,而 '>=1,5' 留在字串 DOPER,又悄悄比不到任何東西,Windows 地區設定說什麼都一樣。日期是同一個陷阱換了戲服:'>=2026-01-01' 不是數字,於是被寫成文字,Excel 的日期儲存格存的卻是序號。要對數字做等於比較,'=100' 和 100 這類數值 Variant 都會產生符號 2 的 IEEE DOPER,裸字串 '100' 產生的則是文字比對。數值運算元請在程式裡建構,別按給人看的格式去排版:
var
Fmt: TFormatSettings;
Since: TDateTime;
begin
Fmt := TFormatSettings.Create;
Fmt.DecimalSeparator := '.';
// 帶小數的門檻:一律用句點格式化
Sh.ApplyAutoFilter('A1:D500', 3, '>' + FloatToStr(1499.5, Fmt));
// 日期:跟 Excel 存在儲存格裡的序號比較。
// 1900 年 3 月之後的日期,Delphi TDateTime 就等於 1900 系統序號
Since := EncodeDate(2026, 1, 1);
Sh.AutoFilterColumns.SetFieldCriteria(4, '>=' + IntToStr(Trunc(Since)),
xlAnd, Unassigned);
end;
AND 和 OR 怎麼合併兩個條件?
AUTOFILTER 的 grbit 裡,wJoin 位元是 0 代表 AND、1 代表 OR,而 HotXLS 直到 v2.384.18 都把這兩個常數寫反了。「至少 100 且低於 500」這種 between 式篩選被存成「至少 100 或低於 500」,實際效果是每個數字都符合,看起來就像篩選根本沒套上。公開的運算子常數還埋著第二個移植陷阱:HotXLS 的 xlAnd 是 0、xlOr 是 1,Excel automation 卻編號成 1 和 2。XlAutoFilterOperator 只是個普通 Byte,所以從 VBA 巨集翻過來、帶著字面數字的程式碼照樣編譯通過,而 COM 裡代表 AND 的字面 1,到了這裡就變成 OR。用具名常數,這問題就不可能出現:
// 金額介於 100(含)與 500(不含)之間
Sh.ApplyAutoFilter('A1:D500', 3, '>=100', xlAnd, '<500');
with Sh.AutoFilterColumns.Find(3) do
begin
Assert(Operator = xlAnd); // 磁碟上 wJoin = 0
Assert(Criteria2.grbitSgn = 1); // 1 = 小於
end;
布林、空白與 255 字元上限
布林條件存成 Bes 值([MS-XLS] §2.5.10),而 Bes 把值位元組 bBoolErr 放前面、fError 旗標放後面。HotXLS 在 v2.384.18 之前的順序正好相反,篩選 TRUE 時把 1 塞進錯誤旗標,Excel 就把條件讀成錯誤碼。寫入端和讀取端是一起寫反的,所以 HotXLS 對自家檔案來回存取都沒怨言,Excel 卻不買單——這也提醒我們,自洽的 round trip 證明不了任何規格符合性。空白根本不需要運算元:單獨傳 '=' 產生符合全部空白的 DOPER($0C),單獨傳 '<>' 產生符合全部非空白的 DOPER($0E)
字串條件在 DOPER 版面上有硬上限。cch 長度欄位只有一個位元組,所以字串運算元不能超過 255 個字元,CreateFilterDoper 會在剝掉運算子後截斷更長的文字,而不是讓長度位元組迴繞、把記錄尾段弄到失步。截斷是無聲的,對長描述欄做的篩選,行為可能跟您傳進去的完整文字不一樣。BIFF8 的尾段把每個字串存成一個位元組的旗標加 UTF-16 code unit,宣告的記錄大小必須把這些位元組算得分毫不差,這正是Delphi XLS 寫入器裡 BIFF 記錄長度宣告的漂移一文談過的同一套帳目紀律
為什麼第二次呼叫 ApplyAutoFilter 會抹掉第一次?
每次呼叫 ApplyAutoFilter 都會重新定義整個篩選範圍,所以只有最後一次呼叫的條件留得下來。它內部呼叫 SetAutoFilter,在重建範圍前先清掉每個欄位——對單欄來說正確,對兩欄來說就很意外。要篩好幾欄,先呼叫一次 ApplyAutoFilter 建立範圍與第一個條件,再透過 AutoFilterColumns.SetFieldCriteria 加入其他條件,它不會動範圍和其他欄位。兩條路徑對範圍外的欄位編號都直接忽略、不丟例外,所以要用讀回來的方式驗證,最好在重新開啟存好的檔案之後:
Sh.ApplyAutoFilter('A1:D500', 1, 'North'); // 範圍 + 欄位 1
Sh.AutoFilterColumns.SetFieldCriteria(3, '>=100', xlAnd, Unassigned);
Sh.AutoFilterColumns.SetFieldCriteria(4, True, xlAnd, Unassigned);
Wb.SaveAs('orders.xls');
Wb := TXLSWorkbook.Create;
Wb.Open('orders.xls');
Assert(Wb.Sheets[1].AutoFilterColumns.Find(1).Active);
Assert(Wb.Sheets[1].AutoFilterColumns.Find(3).Criteria1.DataType = 4);
記住 AUTOFILTER 記錄是一份儲存下來的定義:HotXLS 寫出條件,但不會在傳統 XLS 工作表上求值,所以管線若要在伺服器端拿到符合的列,就得自己在那裡算;XLSX facade 則提供列級求值,如HotXLS 的資料驗證、AutoFilter 與表格所示。等 Excel 真的把列藏起來,範圍下方的各種合計就取決於SUBTOTAL 與 AGGREGATE 怎麼對待隱藏列與篩選列——悄悄比不到東西的數值篩選,下一個現形的地方就是這裡,而且是以錯誤數字的樣貌出現
HotXLS 讓 Delphi 與 C++Builder 原生讀寫 BIFF8 XLS 與 XLSX 活頁簿,包括 Excel 能照預期求值的數值、布林與 AND/OR DOPER AutoFilter 條件。功能、版本與試用下載請見HotXLS Delphi 試算表元件