Technischer Artikel

BIFF8-AutoFilter-DOPER-Kriterien in Delphi mit HotXLS

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:

  • $04 ist ein IEEE-754-Double in den verbleibenden 8 Bytes, so legt Excel einen numerischen Vergleich ab
  • $06 ist ein String, dessen Länge in einem einzigen cch-Byte steckt, während die Zeichen selbst in den Record-Tail wandern
  • $08 ist ein Bes-Wert, ein Boolean oder Fehlercode, gepackt in zwei Bytes
  • $0C und $0E tragen 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

Anatomie des HotXLS-AUTOFILTER-Records mit nullbasiertem Feldindex, dem grbit-Wort, dessen untere zwei Bits wJoin tragen, und zwei 10-Byte-DOPER-Strukturen, deren vt-Byte einen IEEE-Zahl-, String-, Bes-Boolean-, Leer- oder Nicht-Leer-Operanden wählt, während grbitSgn den Vergleichsoperator kodiert, den Excel anwendet
Jeder AUTOFILTER-Record trägt zwei 10-Byte-DOPERs, und das vt-Byte entscheidet, ob Excel ein Kriterium als Zahl, Text, Boolean oder Leerzellen-Test vergleicht – lesen Sie beide über AutoFilterColumns zurück, bevor Sie speichern

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;
HotXLS schreibt dasselbe >=100-AutoFilter-Kriterium als vtString-DOPER, der keine numerischen Zellen matcht, oder als vtIEEENumber-DOPER mit grbitSgn 6, den Excel numerisch gegen den Betrag 250 auswertet – der stille Null-Zeilen-Fehler, den CreateFilterDoper in v2.384.45 behoben hat
Beide Male waren die Bytes gültiges BIFF8 – nur das Operanden-Typbyte änderte sich, weshalb Excel die Datei öffnete, das Kriterium im Dropdown zeigte und trotzdem null Zeilen matchte

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;
HotXLS-wJoin-Bitlayout für AutoFilter-Kriterien, wo 0 zwei DOPERs mit AND und 1 mit OR verknüpft, eine Zahlenlinie, die zeigt, wie die vor v2.384.18 vertauschten Konstanten einen Between-Filter zu einem alles-matchenden OR aufweiteten, und der xlAnd-xlOr-Nummernkonflikt mit der Excel-Automation
Vertauschte wJoin-Konstanten machten aus einem Between-Filter einen, der jede Zahl matcht, und übersetzter VBA kompiliert trotzdem, weil XlAutoFilterOperator ein schlichtes Byte ist – eine Literal-1, die unter COM-Automation AND bedeutete, heißt hier OR

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