HotXLSはBIFF8のAutoFilter条件をすべて、2つの10バイトDOPER構造を運ぶAUTOFILTERレコードとして保存します。そしてDOPERの型がExcelの比較の仕方を決めます。v2.384.45以降、TXLSWorksheet.ApplyAutoFilterは'>=100'のような比較をIEEE数値DOPERとして書き込むので、Excelはテキスト比較の代わりに数値セルをマッチさせます。きっかけのバグ報告は短く、しかも歯がゆいものでした。夜間バッチのエクスポートが金額列にフィルタを適用し、ファイルは文句なしに開き、ドロップダウンの矢印は条件を表示してくれるのに、フィルタは0行しかマッチしない。ファイルは破損していません。バイト列は正当なBIFF8です。ただし「間違った種類の正当さ」であって、本記事がたどるのはまさにこの種の障害です。合わせて、v2.384.18で修正された、さらに古いバイトレベルのミス2つも扱います
BIFF8のAutoFilterは実際に何を保存するのか
BIFF8のAutoFilterは1種類ではなく3種類のレコードのセットであり、条件を保持するのはフィールドごとのレコードだけです。AUTOFILTERINFO($009D、[MS-XLS] §2.4.8)がフィルタ範囲が何列をカバーするかを記録します。FILTERMODE($009B)は本体のないマーカーで、HotXLSは少なくとも1つのフィールドに有効な条件があるときだけ出力します。そして有効なフィールドそれぞれが自分のAUTOFILTERレコード($009E、§2.4.6)を持ちます。中身は0始まりのフィールドインデックス、下位2ビットがwJoinであるgrbitワード、ちょうど10バイトずつの2つのDOPER、そして文字列DOPERの文字を格納するオプションのテールです。ディスク上のフィールドインデックスは0始まりですが、ApplyAutoFilterはフィールドを1から数えます。初めてhexダンプでレコードを探すときに効いてくる違いです。各DOPERの先頭バイトvtは、続くオペランドの種類を次のとおり示します
$04はIEEE 754のdoubleで、残り8バイトに格納されます。Excelが数値比較を保存するときの形式です$06は文字列で、長さは1バイトのcchに入り、文字本体はレコードのテールへ押し込まれます$08はBes値で、真偽値かエラーコードが2バイトに詰め込まれます$0Cと$0Eはオペランドを持たず、すべての空白セルへの一致とすべての非空白セルへの一致を意味します
2バイト目のgrbitSgnが比較を保持します。1から6が<、=、<=、>、<>、>=に対応します。HotXLSは書き込んだ後もこの2バイトをAutoFilterColumns経由で見られるようにしていて、各アイテムはDataType、grbitSgn、Valueを持つTXLSAutofilterDOPERオブジェクトとしてCriteria1とCriteria2を公開します。推測する代わりに、書き込まれる内容をアサートできます
'>=100'のフィルタがExcelで1行もマッチしなかった理由
フィルタが何もマッチさせなかったのは、オペランドがテキストとして保存されており、Excelが文字列DOPERをセルに対してテキストとして比較するからです。v2.384.45より前、lxFilter.pasのCreateFilterDoperは>=接頭辞を正しく剥がしてsignを6に設定したものの、常に100という文字を保持するvtStringのDOPERを組み立てていました。250を保持する数値セルは"100"とのテキスト比較を決して満たさないので、全行が落ちました。例外も出ず、診断もなく、Excelの修復ダイアログも出ません。v2.384.45以降の規則は意図的に狭くしています。条件が比較演算子で始まり、残りがinvariant-culture規則で数値としてパースできるなら、HotXLSは同じsignで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の2列目(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もsign 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は2つの条件をどう結合するのか
AUTOFILTERのgrbitにおけるwJoinビットは、ANDが0でORが1です。HotXLSはこの2つの定数をv2.384.18まで逆にしていました。100以上かつ500未満のようなbetween型フィルタが「100以上または500未満」として保存され、実質すべての数値にマッチするので、フィルタが効いていないように見えたわけです。公開演算子定数には、もう1つ移植時の罠があります。HotXLSではxlAndが0でxlOrが1ですが、Excelオートメーションは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だけが食い違う。自分のファイルのラウンドトリップが通っても、仕様への適合は何も証明しない、という好例です。空白にはオペランドがまったく要りません。'='だけを渡せば全空白マッチのDOPER($0C)に、'<>'だけなら全非空白マッチのDOPER($0E)になります
文字列の条件にはDOPERレイアウト上のハードリミットがあります。cch長フィールドは1バイトなので、文字列オペランドは255文字を超えられず、CreateFilterDoperは演算子を剥がした後で長いテキストを切り詰めます。長さバイトをラップさせてレコードテールを同期崩壊させるよりはまし、という判断です。切り詰めは無音で起こるので、長い説明列へのフィルタは、渡した全文とは違うマッチの仕方をするかもしれません。BIFF8ではテールは各文字列を1バイトのフラグとそれに続くUTF-16コードユニットで保存し、宣言するレコードサイズはそれらのバイトを正確に数える必要があります。この帳簿づけの規律は、DelphiのXLSライターでBIFFレコード長宣言がどうズレるかで扱ったのと同じものです
2回目のApplyAutoFilter呼び出しが1回目を消す理由
ApplyAutoFilterの呼び出しはフィルタ範囲全体を再定義するので、生き残るのは最後の呼び出しの条件だけです。内部ではSetAutoFilterを呼び出し、範囲を組み立て直す前に全フィールドをクリアします。1列なら正しい挙動で、2列目からは不意を突かれる挙動です。複数列をフィルタするときは、ApplyAutoFilterを1回呼んで範囲と最初の条件を作り、残りは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ファサードは行レベルの評価を提供していて、DelphiにおけるHotXLSのデータ入力規則、AutoFilter、テーブルで示したとおりです。Excelが実際に非表示にした後は、範囲の下の合計はSUBTOTALとAGGREGATEが非表示行とフィルタ行をどう扱うか次第になります。黙って何もマッチしない数値フィルタが、次に「合計が合わない」として顔を出すのがまさにそこです
HotXLSはBIFF8のXLSとXLSXワークブックをDelphiとC++Builderからネイティブに読み書きします。数値・真偽値・AND/ORのDOPERを持つAutoFilter条件も、Excelが意図どおり評価できる形で書き込めます。機能、エディション、トライアルダウンロードはHotXLS Delphiスプレッドシートコンポーネントのページをご覧ください