HotXLS zapisuje każde kryterium AutoFiltera BIFF8 jako rekord AUTOFILTER niosący dwie 10-bajtowe struktury DOPER, a typ DOPERa decyduje o tym, jak Excel porównuje. Od v2.384.45 TXLSWorksheet.ApplyAutoFilter zapisuje porównanie typu '>=100' jako DOPER liczbowy IEEE, więc Excel dopasowuje komórki liczbowe zamiast porównywać tekst. Zgłoszenie błędu, które wymusiło tę zmianę, było krótkie i irytujące: nocny eksport nakładał filtr na kolumnę kwot, plik otwierał się bez protestu, strzałka listy rozwijanej pokazywała kryterium, a filtr łapał zero wierszy. Nic nie było uszkodzone. Bajty były poprawnym BIFF8, tylko niewłaściwego rodzaju poprawnym, i właśnie tej klasie awarii przyglądamy się w tym artykule, razem z dwoma starszymi błędami na poziomie bajtów naprawionymi w v2.384.18
Co faktycznie zapisuje AutoFilter BIFF8?
AutoFilter BIFF8 to zestaw trzech typów rekordów, nie jeden, i tylko rekord per pole trzyma kryteria. AUTOFILTERINFO ($009D, [MS-XLS] §2.4.8) zapisuje, ile kolumn obejmuje zakres filtra. FILTERMODE ($009B) to marker bez treści, który HotXLS wysyła tylko wtedy, gdy co najmniej jedno pole ma aktywne kryterium. Potem każde aktywne pole dostaje własny rekord AUTOFILTER ($009E, §2.4.6): indeks pola liczony od zera, słowo grbit, którego dwa najniższe bity to wJoin, dwa DOPERy po dokładnie 10 bajtów każdy i opcjonalny ogon trzymający znaki ewentualnego DOPERa tekstowego. Indeks pola jest na dysku liczony od zera, choć ApplyAutoFilter numeruje pola od 1, co boli przy pierwszym polowaniu na rekord w zrzucie hex. Pierwszy bajt każdego DOPERa, vt, mówi, jaki operand przychodzi dalej:
$04to double IEEE 754 zapisany w pozostałych 8 bajtach, czyli tak Excel trzyma porównanie liczbowe$06to łańcuch, którego długość mieszka w pojedynczym bajciecch, a same znaki lądują w ogonie rekordu$08to wartość Bes, czyli Boolean albo kod błędu spakowany w dwa bajty$0Ci$0Enie niosą operandu i znaczą odpowiednio: dopasuj wszystkie puste i dopasuj wszystkie niepuste
Drugi bajt, grbitSgn, trzyma porównanie: wartości od 1 do 6 mapują się na <, =, <=, >, <> i >=. HotXLS pokazuje oba bajty po fakcie przez AutoFilterColumns, którego elementy wystawiają Criteria1 i Criteria2 jako obiekty TXLSAutofilterDOPER z polami DataType, grbitSgn i Value, więc możesz robić asercje na tym, co zostanie zapisane, zamiast zgadywać
Dlaczego filtr '>=100' nie pasował do żadnego wiersza w Excelu?
Filtr nie łapał niczego, bo operand był zapisany jako tekst, a Excel porównuje DOPER tekstowy z komórką jak tekst. Przed v2.384.45 CreateFilterDoper w lxFilter.pas poprawnie zdejmował prefiks >= i ustawiał znak na 6, po czym zawsze budował DOPER vtString ze znakami 100. Komórka liczbowa z 250 nigdy nie spełni porównania tekstowego z "100", więc wypadał każdy wiersz. Żadnego wyjątku, żadnej diagnostyki, żadnej propozycji naprawy od Excela. Reguła od v2.384.45 jest celowo wąska: jeśli kryterium zaczyna się od operatora porównania, a reszta parsuje się jako liczba według reguł invariant culture, HotXLS pisze DOPER vtIEEENumber z tym samym znakiem. Nagie wartości bez operatora zostają w formie tekstowej, bo tak Excel sam zapisuje element wybrany z listy rozwijanej
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;
// Pole 2 = druga kolumna zakresu A1:B100 (numeracja od 1 po stronie API)
Sh.ApplyAutoFilter('A1:B100', 2, '>=100');
Doper := Sh.AutoFilterColumns.Find(2).Criteria1;
// v2.384.45+: DataType = 4 (liczba IEEE), grbitSgn = 6 (>=)
// Przed poprawką: DataType = 6 (łańcuch), który nie łapał niczego
Assert(Doper.DataType = 4);
Wb.SaveAs('orders.xls');
end;
W parsowaniu mieszkają pozostałe ostre krawędzie. Operand przechodzi przez TryStrToFloat z kropką jako separatorem dziesiętnym, więc '>=1.5' staje się liczbą, a '>=1,5' zostaje DOPERem tekstowym i znowu po cichu nie łapie niczego, niezależnie od tego, co mówią ustawienia regionalne Windows. Daty to ta sama pułapka w innym przebraniu: '>=2026-01-01' nie jest liczbą, więc ląduje jako tekst, podczas gdy Excel trzyma komórki dat jako liczby seryjne. Przy równości na liczbie zarówno '=100', jak i liczbowy Variant taki jak 100 dają DOPER IEEE ze znakiem 2, a sam łańcuch '100' daje dopasowanie tekstowe. Operandów liczbowych buduj w kodzie, zamiast formatować je dla ludzi:
var
Fmt: TFormatSettings;
Since: TDateTime;
begin
Fmt := TFormatSettings.Create;
Fmt.DecimalSeparator := '.';
// Próg z ułamkiem: zawsze formatuj z kropką
Sh.ApplyAutoFilter('A1:D500', 3, '>' + FloatToStr(1499.5, Fmt));
// Daty: porównuj z numerem seryjnym, który Excel trzyma w komórce.
// Delphi TDateTime równa się numerowi seryjnemu systemu 1900 dla dat po marcu 1900
Since := EncodeDate(2026, 1, 1);
Sh.AutoFilterColumns.SetFieldCriteria(4, '>=' + IntToStr(Trunc(Since)),
xlAnd, Unassigned);
end;
Jak AND i OR łączą dwa warunki?
Bity wJoin grbita rekordu AUTOFILTER mają 0 dla AND i 1 dla OR, a HotXLS miał te dwie stałe zamienione miejscami aż do v2.384.18. Filtr typu between, czyli co najmniej 100 i poniżej 500, zapisywał się jako co najmniej 100 albo poniżej 500, co w praktyce łapie każdą liczbę i wygląda, jakby filtr w ogóle nie zadziałał. Publiczne stałe operatorów dodają drugą pułapkę przy przenoszeniu kodu. W HotXLS xlAnd to 0, a xlOr to 1, natomiast automatyzacja Excela numeruje je 1 i 2. XlAutoFilterOperator to zwykły Byte, więc kod przetłumaczony z makra VBA z literalnymi liczbami kompiluje się bez syku, a literalna 1, która w COM znaczyła AND, tu znaczy OR. Używaj stałych nazwanych, a problem nie ma prawa się wydarzyć:
// Kwota między 100 (włącznie) a 500 (wyłącznie)
Sh.ApplyAutoFilter('A1:D500', 3, '>=100', xlAnd, '<500');
with Sh.AutoFilterColumns.Find(3) do
begin
Assert(Operator = xlAnd); // wJoin = 0 na dysku
Assert(Criteria2.grbitSgn = 1); // 1 = mniejsze niż
end;
Wartości Boolean, puste komórki i limit 255 znaków
Kryterium Boolean jest zapisywane jako wartość Bes ([MS-XLS] §2.5.10), a Bes kładzie bajt wartości bBoolErr pierwszy, a flagę fError drugą. HotXLS zapisywał je w odwrotnej kolejności przed v2.384.18, więc filtr po TRUE wkładał 1 do flagi błędu, a Excel czytał kryterium jako kod błędu. Writer i reader były zamienione razem, dlatego HotXLS robił round-trip własnych plików bez narzekań, podczas gdy Excel się nie zgadzał — przypomnienie, że samospójny round-trip nie dowodzi zgodności ze specyfikacją. Puste komórki w ogóle nie potrzebują operandu: samo '=' daje DOPER dopasowujący wszystkie puste ($0C), a samo '<>' DOPER dopasowujący wszystkie niepuste ($0E)
Kryteria tekstowe trafiają na twardy limit w układzie DOPERa. Pole długości cch to pojedynczy bajt, więc operand tekstowy nie może przekroczyć 255 znaków, a CreateFilterDoper obcina dłuższy tekst po zdjęciu operatora, zamiast pozwolić bajtowi długości się przepełnić i rozsynchronizować ogon rekordu. Obcinanie jest ciche, więc filtr na długiej kolumnie opisów może dopasowywać inaczej niż pełny tekst, który podałeś. W BIFF8 ogon trzyma każdy łańcuch jako jedno-bajtową flagę, po której następują jednostki kodu UTF-16, a zadeklarowany rozmiar rekordu musi liczyć te bajty co do jednego — ta sama dyscyplina księgowania, którą opisuje dryf deklaracji długości rekordów BIFF w pisarzu XLS w Delphi
Dlaczego drugie wywołanie ApplyAutoFiltera kasuje pierwsze?
Każde wywołanie ApplyAutoFilter definiuje od nowa cały zakres filtra, więc przeżywa tylko kryterium z ostatniego wywołania. Wewnątrz woła SetAutoFilter, które czyści każde pole przed przebudową zakresu, co dla jednej kolumny jest poprawne, a dla dwóch zaskakuje. Żeby filtrować kilka kolumn, wołaj ApplyAutoFilter raz, żeby ustanowić zakres i pierwsze kryterium, a pozostałe dodawaj przez AutoFilterColumns.SetFieldCriteria, które zostawia zakres i inne pola w spokoju. Obie ścieżki ignorują numer pola poza zakresem bez podnoszenia wyjątku, więc weryfikuj przez odczyt zwrotny, najlepiej po ponownym otwarciu zapisanego pliku:
Sh.ApplyAutoFilter('A1:D500', 1, 'North'); // zakres + pole 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);
Pamiętaj, że rekord AUTOFILTER to zapisana definicja: HotXLS zapisuje kryteria i nie wylicza ich na klasycznym arkuszu XLS, więc pipeline, który potrzebuje pasujących wierszy na serwerze, musi policzyć je tam samodzielnie, natomiast fasada XLSX oferuje ewaluację na poziomie wierszy, co pokazuje walidacja danych, AutoFilter i tabele HotXLS w Delphi. Gdy Excel już ukryje wiersze, sumy pod zakresem zależą od tego, jak SUBTOTAL i AGGREGATE traktują wiersze ukryte i przefiltrowane, co jest kolejnym miejscem, gdzie liczbowy filtr cicho nie łapiący niczego pokazuje się jako zła liczba
HotXLS czyta i zapisuje skoroszyty BIFF8 XLS oraz XLSX natywnie z Delphi i C++Buildera, w tym kryteria AutoFiltera z DOPERami liczbowymi, logicznymi i AND/OR, które Excel wylicza zgodnie z intencją. Funkcje, edycje i wersję próbną znajdziesz na stronie arkuszowego komponentu HotXLS dla Delphi