HotXLS speichert jedes BIFF8-AutoFilter-Kriterium als AUTOFILTER-Record, der zwei 10-Byte-DOPER-Strukturen trägt, und der DOPER-Typ entscheidet, wie Excel vergleicht. Seit v2.384.45 schreibt TXLSWorksheet.ApplyAutoFilter einen Vergleich wie '>=100' als IEEE-Zahl-DOPER, sodass Excel numerische Zellen matcht, statt Text zu vergleichen. Der Bugreport, der die Änderung ausgelöst hat, war kurz und zum Haareraufen: Ein nächtlicher Export setzte einen Filter auf eine Betragsspalte, die Datei öffnete sich ohne Murren, der Dropdown-Pfeil zeigte das Kriterium, und der Filter matchte null Zeilen. Nichts war korrupt. Die Bytes waren gültiges BIFF8, nur die falsche Art von gültig – und genau diese Klasse von Fehlern geht dieser Artikel durch, zusammen mit zwei älteren Byte-Level-Fehlern, die in v2.384.18 behoben wurden
Was steckt eigentlich in einem BIFF8-AutoFilter?
Ein BIFF8-AutoFilter besteht aus drei Record-Typen, nicht aus einem, und nur der Record pro Feld trägt Kriterien. AUTOFILTERINFO ($009D, [MS-XLS] §2.4.8) notiert, wie viele Spalten der Filterbereich abdeckt. FILTERMODE ($009B) ist ein bodyloser Marker, den HotXLS nur ausspielt, wenn mindestens ein Feld ein aktives Kriterium hat. Danach bekommt jedes aktive Feld seinen eigenen AUTOFILTER-Record ($009E, §2.4.6): einen nullbasierten Feldindex, ein grbit-Wort, dessen untere zwei Bits wJoin tragen, zwei exakt 10 Byte große DOPERs und einen optionalen Tail, der die Zeichen eines String-DOPERs aufnimmt. Der Feldindex ist auf der Platte nullbasiert, obwohl ApplyAutoFilter Felder ab 1 nummeriert – das wird relevant, sobald Sie das erste Mal in einem Hex-Dump einen Record suchen. Das erste Byte jedes DOPERs, vt, sagt, welche Art Operand folgt:
$04ist ein IEEE-754-Double in den verbleibenden 8 Bytes, so legt Excel einen numerischen Vergleich ab$06ist ein String, dessen Länge in einem einzigencch-Byte steckt, während die Zeichen selbst in den Record-Tail wandern$08ist ein Bes-Wert, ein Boolean oder Fehlercode, gepackt in zwei Bytes$0Cund$0Etragen keinen Operanden und bedeuten alle Leerzellen matchen bzw. alle Nicht-Leerzellen matchen
Das zweite Byte, grbitSgn, hält den Vergleich: 1 bis 6 stehen für <, =, <=, >, <> und >=. HotXLS hält beide Bytes nachträglich über AutoFilterColumns sichtbar, dessen Items Criteria1 und Criteria2 als TXLSAutofilterDOPER-Objekte mit DataType, grbitSgn und Value bereitstellen, sodass Sie per Assertion prüfen können, was geschrieben wird, statt zu raten
Warum matchte ein '>=100'-Filter in Excel keine Zeilen?
Der Filter matchte nichts, weil der Operand als Text gespeichert war, und Excel vergleicht einen String-DOPER mit der Zelle als Text. Vor v2.384.45 hat CreateFilterDoper in lxFilter.pas das >=-Präfix korrekt abgeschnitten und das Vorzeichen auf 6 gesetzt, dann aber stets einen vtString-DOPER mit den Zeichen 100 gebaut. Eine Zahlzelle mit 250 erfüllt nie einen Textvergleich gegen "100", also flog jede Zeile raus. Keine Exception, keine Diagnose, kein Reparaturhinweis von Excel. Die Regel seit v2.384.45 ist bewusst eng: Beginnt das Kriterium mit einem Vergleichsoperator und lässt sich der Rest nach invariant-culture-Regeln als Zahl parsen, schreibt HotXLS einen vtIEEENumber-DOPER mit demselben Vorzeichen. Ein nackter Wert ohne Operator bleibt in der String-Form, denn genau so speichert Excel selbst einen aus der Dropdown-Liste gewählten Eintrag
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;
// Feld 2 = zweite Spalte von A1:B100 (API-seitig 1-basiert)
Sh.ApplyAutoFilter('A1:B100', 2, '>=100');
Doper := Sh.AutoFilterColumns.Find(2).Criteria1;
// Ab v2.384.45: DataType = 4 (IEEE-Zahl), grbitSgn = 6 (>=)
// Vor dem Fix: DataType = 6 (String), was nichts matchte
Assert(Doper.DataType = 4);
Wb.SaveAs('orders.xls');
end;
Beim Parsen wohnen die restlichen scharfen Kanten. Der Operand läuft durch TryStrToFloat mit Punkt als Dezimaltrennzeichen, daher wird '>=1.5' zu einer Zahl, während '>=1,5' ein String-DOPER bleibt und wieder stumm nichts matchet – ganz egal, was das Windows-Locale sagt. Datumswerte sind dieselbe Falle in anderem Kostüm: '>=2026-01-01' ist keine Zahl, wird also als Text geschrieben, während Excel Datumzellen als Seriennummern ablegt. Für Gleichheit auf einer Zahl liefern sowohl '=100' als auch ein numerischer Variant wie 100 einen IEEE-DOPER mit Vorzeichen 2, während der nackte String '100' einen Textmatch produziert. Bauen Sie numerische Operanden im Code, statt sie für Menschen zu formatieren:
var
Fmt: TFormatSettings;
Since: TDateTime;
begin
Fmt := TFormatSettings.Create;
Fmt.DecimalSeparator := '.';
// Schwelle mit Nachkommastelle: immer mit Punkt formatieren
Sh.ApplyAutoFilter('A1:D500', 3, '>' + FloatToStr(1499.5, Fmt));
// Datumswerte: gegen die Seriennummer vergleichen, die Excel in der Zelle ablegt.
// Ein Delphi-TDateTime entspricht der Serial des 1900-Systems für Daten nach März 1900
Since := EncodeDate(2026, 1, 1);
Sh.AutoFilterColumns.SetFieldCriteria(4, '>=' + IntToStr(Trunc(Since)),
xlAnd, Unassigned);
end;
Wie verknüpfen AND und OR zwei Bedingungen?
Die wJoin-Bits des AUTOFILTER-grbit stehen 0 für AND und 1 für OR, und HotXLS hatte diese beiden Konstanten bis v2.384.18 vertauscht. Ein Between-Filter wie mindestens 100 und unter 500 wurde als mindestens 100 oder unter 500 gespeichert, was praktisch jede Zahl matcht und wirkt, als hätte der Filter schlicht nicht gegriffen. Die öffentlichen Operator-Konstanten bergen eine zweite Portierungsfalle. In HotXLS ist xlAnd 0 und xlOr ist 1, während Excel-Automation sie mit 1 und 2 nummeriert. XlAutoFilterOperator ist ein schlichtes Byte, also kompiliert aus einem VBA-Makro übersetzter Code mit Literalzahlen anstandslos, und eine Literal-1, die in COM AND bedeutete, heißt hier OR. Nehmen Sie die benannten Konstanten, und das Problem kann gar nicht erst entstehen:
// Betrag zwischen 100 (inklusive) und 500 (exklusiv)
Sh.ApplyAutoFilter('A1:D500', 3, '>=100', xlAnd, '<500');
with Sh.AutoFilterColumns.Find(3) do
begin
Assert(Operator = xlAnd); // wJoin = 0 auf der Platte
Assert(Criteria2.grbitSgn = 1); // 1 = kleiner als
end;
Booleans, Leerzellen und die 255-Zeichen-Grenze
Ein Boolean-Kriterium wird als Bes-Wert gespeichert ([MS-XLS] §2.5.10), und Bes stellt das Wertbyte bBoolErr voran und das fError-Flag dahinter. HotXLS schrieb beide bis v2.384.18 in umgekehrter Reihenfolge, also landete bei einem Filter auf TRUE die 1 im Fehlerflag, und Excel las das Kriterium als Fehlercode. Writer und Reader waren gemeinsam vertauscht, weshalb HotXLS seine eigenen Dateien ohne Murren round-tripte, während Excel widersprach – eine Erinnerung daran, dass ein selbstkonsistenter Roundtrip nichts über Spezifikationskonformität beweist. Leerzellen brauchen gar keinen Operanden: Ein allein übergebenes '=' erzeugt einen alle-Leerzellen-DOPER ($0C), ein allein übergebenes '<>' einen alle-Nicht-Leerzellen-DOPER ($0E)
String-Kriterien stoßen im DOPER-Layout an eine harte Grenze. Das cch-Längenfeld ist ein einzelnes Byte, ein String-Operand kann also nicht über 255 Zeichen hinaus, und CreateFilterDoper schneidet längeren Text nach dem Abtrennen des Operators ab, statt das Längenbyte überlaufen und den Record-Tail desynchronisieren zu lassen. Die Kürzung bleibt stumm, und ein Filter auf einer langen Beschreibungsspalte kann anders matchen als der volle Text, den Sie übergeben haben. In BIFF8 legt der Tail jeden String als Ein-Byte-Flag gefolgt von UTF-16-Code-Units ab, und die deklarierte Record-Größe muss diese Bytes exakt zählen – dieselbe Buchführungsdisziplin wie in wie Längenangaben von BIFF-Records in einem Delphi-XLS-Writer driften beschrieben
Warum löscht ein zweiter ApplyAutoFilter-Aufruf den ersten?
Jeder ApplyAutoFilter-Aufruf definiert den gesamten Filterbereich neu, also überlebt nur das Kriterium des letzten Aufrufs. Intern ruft er SetAutoFilter, das vor dem Neuaufbau des Bereichs jedes Feld leerräumt – für eine Spalte korrekt, für zwei überraschend. Um mehrere Spalten zu filtern, rufen Sie ApplyAutoFilter einmal auf, um Bereich und erstes Kriterium festzulegen, und legen die übrigen danach über AutoFilterColumns.SetFieldCriteria an, das Bereich und andere Felder in Ruhe lässt. Beide Wege ignorieren eine Feldnummer außerhalb des Bereichs, ohne eine Exception zu werfen, prüfen Sie also durch Zurücklesen, idealerweise nach dem erneuten Öffnen der gespeicherten Datei:
Sh.ApplyAutoFilter('A1:D500', 1, 'North'); // Bereich + Feld 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);
Denken Sie daran, dass der AUTOFILTER-Record eine gespeicherte Definition ist: HotXLS schreibt die Kriterien und wertet sie auf dem klassischen XLS-Worksheet nicht aus, also muss eine Pipeline, die die passenden Zeilen auf dem Server braucht, sie dort selbst berechnen, während die XLSX-Fassade zeilenweise Auswertung bietet, wie in HotXLS-Datenvalidierung, AutoFilter und Tabellen in Delphi gezeigt. Sobald Excel Zeilen ausblendet, hängen Summen unterhalb des Bereichs davon ab, wie SUBTOTAL und AGGREGATE ausgeblendete und gefilterte Zeilen behandeln – die nächste Stelle, an der ein numerischer Filter, der stumm nichts matcht, als falsche Zahl auftaucht
HotXLS liest und schreibt BIFF8-XLS- und XLSX-Arbeitsmappen nativ aus Delphi und C++Builder, eingeschlossen AutoFilter-Kriterien mit numerischen, Boolean- und AND/OR-DOPERs, die Excel wie beabsichtigt auswertet. Features, Editionen und einen Trial-Download finden Sie unter HotXLS Delphi spreadsheet component