Narzędzie do uzgadniania znajduje 3 000 komórek, które rozmijają się z księgą, i chce, by zapisany .xls otwierał się z wszystkimi zaznaczonymi pod zamrożonym wierszem nagłówka. Na papierze to jeden rekord Selection. W bajtach to 9 + 3 000 × 6 = 18 009, a ciało rekordu BIFF8 kończy się na 8 224. Ten sam skoroszyt podnosi drugie pytanie, które brzmi banalnie i nie jest: w arkuszu zamrożonym na B2 "pozycja przewinięcia" to cztery okienka dzielące dwa offsety wierszy i dwa offsety kolumn, a te cztery liczby mieszkają w dwóch różnych rekordach
Oba problemy to arytmetyka formatu: zaznaczenie jest cięte na rekordy, które reader musi zszyć z powrotem, okienko jest nazywane kodem, który nie podąża za kolejnością czytania, a każda oś przewijania ląduje w rekordzie, z którego czyta ją Excel
Dlaczego 3 000 zaznaczonych obszarów potrzebuje trzech rekordów Selection
[MS-XLS] §2.4.248 rozkłada rekord Selection (0x001D) na 9-bajtową część stałą, po której idą obszary. Część stała trzyma bajt okienka, rwAct i colAct komórki aktywnej, irefAct dla indeksu obszaru aktywnego i cref dla liczby obszarów. Każdy obszar to struktura RefU z dwoma 16-bitowymi wierszami i dwiema 8-bitowymi kolumnami. Największa mieszcząca się liczba to (8 224 − 9) / 6 z zaokrągleniem w dół, co daje 1 369 obszarów i ciało o długości 8 223 bajty — jeden bajt pod limitem
HotXLS używa 1 369 jako swojego sufitu na rekord. Gdy TXLSWorksheet zapisuje grupę zaznaczenia z większą liczbą obszarów, pisze kolejne rekordy Selection dla tego samego okienka, po 1 369 obszarów naraz, i każdy rekord powtarza ten sam bajt okienka, komórkę aktywną i indeks aktywny. Zaznaczenie uzgodnienia na 3 000 komórek wychodzi jako trzy rekordy trzymające 1 369, 1 369 i 262 obszarów. Wołający nigdy nie widzą tego podziału; oddają tablicę referencji A1
var
Book: TXLSWorkbook;
Sheet: TXLSWorksheet;
Diffs: TXLSSelectedAreas;
I: Integer;
begin
Book := TXLSWorkbook.Create;
try
Sheet := Book.Sheets.Add;
Sheet.FreezePanes(1, 1); // wiersz nagłówka i kolumna A zostają na miejscu
SetLength(Diffs, 3000);
for I := 0 to High(Diffs) do
Diffs[I] := Format('C%d', [I + 2]);
// Zamrożenie resetuje zapisane zaznaczenie, więc zaznaczaj po zamrożeniu.
// 3000 obszarów zapisuje się jako trzy rekordy Selection: 1369 + 1369 + 262
if not Sheet.SelectAreas(xlspBottomRight, Diffs, 0) then
raise Exception.Create('Selection rejected');
Book.SaveAs('reconciliation.xls');
finally
Book.Free;
end;
end;
Kolejność wołań ma tu znaczenie. FreezePanes przebudowuje układ okienek i czyści każde zaznaczenie per okienko, które arkusz woził, więc wcześniejsze zaznaczenie byłoby już nieobecne przy zapisie. Na niezamrożonym arkuszu prawe dolne okienko jeszcze nie istnieje, i to samo wywołanie SelectAreas po prostu zwróciłoby False. Najpierw zamroź, potem zaznaczaj
Na co wskazuje irefAct, gdy zaznaczenie rozciąga się na kilka rekordów?
W podzielonym zaznaczeniu irefAct indeksuje całą zagregowaną sekwencję obszarów przez wszystkie porcje, i każda porcja niesie tę samą wartość. Zaznaczenie o jeden obszar za limitem pokazuje to konkretnie: 1 370 obszarów z ostatnim aktywnym staje się dwoma rekordami — pierwszym z cref 1 369 i drugim z cref 1 — i oba niosą irefAct 1 369. W drugim rekordzie ten indeks jest większy niż własna liczba obszarów record'a. Wygląda to na zepsute, jeśli czytasz rekord po rekordzie, i dwa rodzaje readerów się na tym potykają
- Reader sprawdzający
irefActwzględemcrefwewnątrz każdego rekordu odrzuci w drugiej porcji w zupełności poprawny plik - Reader nadpisujący swój stan zaznaczenia przy każdym rekordzie Selection zachowa tylko ostatnią porcję i zgubi pierwsze 1 369 obszarów
Reader HotXLS dokleja kolejne rekordy tego samego okienka do jednej grupy i wymaga, by każda porcja zgadzała się co do aktywnego wiersza, aktywnej kolumny i irefAct. Kontrola zakresu na irefAct i sprawdzenie, że komórka aktywna naprawdę siedzi w obszarze, na który wskazuje, ruszają dopiero, gdy przychodzi rekord EOF worksheet'a i znana jest cała sekwencja. Sprawdzanie wcześniej byłoby prostszym kodem i złą odpowiedzią dla każdego zaznaczenia ponad 1 369 obszarów
Po stronie zapisu overload SelectAreas przyjmujący okienko najpierw nie ma własnego sufitu 1 369 obszarów. Parsuje każdą referencję, zanim dotknie arkusza: znaczniki $ są przyjmowane i zdejmowane, odwrócony zakres jak C9:A2 jest normalizowany do A2:C9, a pusty albo nieparsowalny wpis zawala całe wywołanie. Indeks aktywny musi wypadać wewnątrz tablicy i trzymać się na poziomie 32 767 lub niżej, bo irefAct to 16-bitowe pole ze znakiem. Każda porażka zwraca False i zostawia poprzednie zaznaczenie dokładnie takie, jakie było
Kody okienek nie podążają za kolejnością czytania
Rekord Selection nazywa swoje okienko numerycznym kodem zdefiniowanym w formacie: 0 dla prawego dolnego, 1 dla prawego górnego, 2 dla lewego dolnego i 3 dla lewego górnego. Publiczna enumeracja TXLSPanePosition jest zadeklarowana w kolejności czytania — xlspTopLeft, xlspTopRight, xlspBottomLeft, xlspBottomRight — więc Ord(xlspTopLeft) to 0, a 0 w pliku znaczy prawy dolny. Pisarz rzucający enuma prosto na bajt okienka wyląduje każde zaznaczenie lewego górnego na okienku prawym dolnym, a plik pozostaje strukturalnie poprawny, bo oba to legalne kody okienek
Każdy punkt wejścia HotXLS świadomy okienek konwertuje enuma przez jawną instrukcję case, więc kod aplikacji nigdy nie dotyka kodów numerycznych. Istnienie okienka jest sprawdzane także względem bieżącej geometrii zamrożenia albo podziału. Lewe górne okienko istnieje zawsze. Prawe górne potrzebuje pionowego podziału, lewe dolne poziomego, a prawe dolne obu. Dla okienka, którego arkusz nie ma, SelectAreas zwraca False, a GetSelectedAreas zwraca pustą tablicę z ActiveAreaIndex ustawionym na -1, i żadne z tych wywołań nie tworzy po drodze okienka, zaznaczenia ani komórki
Arkusz trzyma też starszy dwuargumentowy SelectAreas(Areas, ActiveAreaIndex), który pisze do okienka aktualnie aktywnego i zwraca status Integer. Na arkuszu z zamrożonymi okienkami to często zgadywanie stanu, którego sam nie ustawiałeś. Używaj overloadu przyjmujągo najpierw TXLSPanePosition dla wszystkiego, co chodzi na arkuszach zamrożonych albo podzielonych; okienko jest wtedy częścią wywołania, a wynik False mówi ci, że geometria i żądanie się rozmijają. Odczyt wyniku z powrotem ma ten sam kształt:
var
Areas: TXLSSelectedAreas;
Active: Integer;
begin
Areas := Sheet.GetSelectedAreas(xlspBottomRight, Active);
if Active < 0 then
Memo1.Lines.Add('No usable selection on the bottom-right pane')
else
Memo1.Lines.Add(Format('%d areas, active area %s',
[Length(Areas), Areas[Active]]));
end;
Gdzie mieszka pozycja przewinięcia każdego pane'a?
Pozycja przewinięcia każdego okienka jest rozdzielona między dwa rekordy, bo cztery okienka dzielą między sobą tylko dwa położenia wierszy i dwa położenia kolumn. W klasycznym skoroszycie pierwszy widoczny wiersz górnych pane'ów i pierwsza widoczna kolumna lewych pane'ów to Window2.rwTop i Window2.colLeft. Wiersz dolnych pane'ów i kolumna prawych pane'ów to Pane.rwTop i Pane.colLeft. Prawy górny bierze więc wiersz z Window2, a kolumnę z Pane, i lewy dolny robi odwrotnie
ScrollWindow i TryGetWindowScroll podążają za tym układem dokładnie i używają numerów wierszy i kolumn liczonych od 1. ScrollWindow(xlspTopRight, R, C) pisze Window2.rwTop i Pane.colLeft, co znaczy, że ustawienie kolumny prawego górnego przesuwa też okienko prawe dolne. To zgadza się z Excelem, gdzie oba prawe okienka dzielą jeden poziomy scrollbar. Nieistniejące okienko każe ScrollWindow zwrócić False, a TryGetWindowScroll ustawia oba wyjścia na zero. Wiersz albo kolumna poza zakresem są odrzucane, zanim ruszy którakolwiek oś, więc zła kolumna nie zostawi po sobie półzastosowanego wiersza
var
Row, Col: Integer;
begin
Sheet.FreezePanes(1, 1);
// Prawy dolny: dolna oś wierszy (Pane.rwTop) i prawa oś kolumn (Pane.colLeft)
Sheet.ScrollWindow(xlspBottomRight, 500, 3);
// Prawy górny dzieli prawą oś kolumn, więc to też przesuwa prawy dolny na kolumnę 6
Sheet.ScrollWindow(xlspTopRight, 1, 6);
if Sheet.TryGetWindowScroll(xlspBottomRight, Row, Col) then
Memo1.Lines.Add(Format('Bottom-right starts at row %d, column %d', [Row, Col]));
// Prawy dolny startuje od wiersza 500, kolumny 6
end;
XLSX trzyma te same dwie warstwy w dwóch elementach: sheetView/@topLeftCell (ECMA-376 Part 1, §18.3.1.87) dla okna jako całości i potomny pane/@topLeftCell (§18.3.1.66) dla prawej dolnej strony podziału. Oba atrybuty mogą występować naraz. Loader trzymający tylko jeden z nich gubi albo górne wiersze, albo lewe kolumny każdego zamrożonego arkusza, którego dotknie. TXLSXWorksheet czyta zewnętrzny atrybut do pól poziomu okna najpierw, pozwala potomkowi pane nadpisać wyłącznie pola poziomu okienka i zapisuje oba z powrotem osobno. ScrollWindow i TryGetWindowScroll mają na TXLSXWorksheet te same sygnatury i znaczenie, a kopiowanie arkusza wozi obie warstwy w każdym z formatów
Węższe właściwości nadal tam są dla kodu, któremu potrzeba jednej pary osi: ScrollRow i ScrollColumn na klasycznym arkuszu oraz liczone od zera SetPaneScroll i GetPaneScroll dla okienka prawego dolnego na XLSX. Kontrolka renderująca trzyma swoje TopRow i LeftCol, jak opisuje artykuł o renderowaniu skoroszytów we własnej siatce VCL, i ten stan runtime jest osobny od tego, co ląduje w zapisie. Samą geometrię zamrożenia i podziału konfiguruje się ustawieniami poziomu arkusza, które omawia artykuł o ochronie arkusza, ustawieniach strony i drukowaniu
Uszkodzone rekordy Selection są trzymane bajt w bajt
Gdy rekord Selection jest zniekształcony, HotXLS trzyma jego ciało jako nieprzejrzyste bajty, zgłasza kod diagnostyczny 1304 (xlsDiagnosticSelectionRecordInvalid) jako ostrzeżenie i przy zapisie odtwarza oryginalne ciało bez zmian. Pliki od nieznanych eksporterów albo pisanych ręcznie writerów BIFF mogą takie rekordy wozić. Zanim rekord dołączy do grupy swojego pane'a, reader sprawdza bajt okienka równy 3 lub mniejszy, sąsiedztwo rekordów per okienko, cref między 1 a 1 369 z ciałem o dokładnie 9 + cref × 6 bajtach, porcje zgadzające się co do komórki aktywnej i irefAct bez bitu znaku, aktywną kolumnę na siatce i brak odwróconych obszarów. Problemy wewnątrz jednego fizycznego rekordu są zgłaszane raz na rekord; sprzeczności, które wychodzą dopiero po agregacji — jak irefAct wskazujący za łączną liczbę obszarów albo komórka aktywna poza indeksowanym obszarem — są zgłaszane raz na grupę przy EOF
Nieprawidłowa grupa zostaje poza typowanym API: GetSelectedAreas zwraca dla tego okienka pustą tablicę z indeksem -1, a pozostałe okienka dalej działają normalnie
var
I: Integer;
D: TXLSDiagnostic;
begin
if Book.Open('supplier-upload.xls') <> 1 then
Exit;
for I := 0 to Book.Diagnostics.Count - 1 do
begin
D := Book.Diagnostics[I];
if D.Code = xlsDiagnosticSelectionRecordInvalid then
// Log to twój własny TStrings; HotXLS tylko wypełnia Diagnostics
Log.Add(Format('%s: record $%.4x kept opaque (%s)',
[D.SheetName, D.RecordId, D.Message]));
end;
end;
Naprawa rekordu — przez zaciśnięcie irefAct albo wyrzucenie odwróconego obszaru — była drugą opcją. Zachowanie go to bezpieczniejszy domyślny: naprawione zaznaczenie to zaznaczenie, którego nikt nie wybrał, a oryginalne bajty przepadłyby. Komunikat diagnostyczny nazywa sprawdzian, który zawodzi — jak "the area count does not match the record length" albo "records for the same pane are not contiguous" — a RecordId jest dla tego kodu zawsze $001D
Wstawianie wierszy i kolumn remapuje zaznaczenie każdego pane'a
Wstawienie albo usunięcie pełnych wierszy lub kolumn remapuje zaznaczenie każdego okienka przez jedną wspólną rutynę, w obu silnikach — klasycznym i XLSX. Ocalałe obszary zachowują swoją kolejność i przesuwają się z komórkami, które pokrywają, a obszar aktywny zachowuje tożsamość. Gdy obszar aktywny zostanie usunięty, aktywnym staje się pierwszy ocalały obszar za nim; gdy nic za nim nie idzie — ostatni ocalały przed nim. Gdy usunięte zostaną wszystkie obszary, grupa zapada się do pojedynczej komórki na granicy usuwania. Komórka aktywna, która już nie wypada wewnątrz wybranego obszaru, przesuwa się na lewy górny róg tego area'a, więc indeks i współrzędna nigdy nie mogą sobie przeczyć w zapisanym pliku
Grupy oflagowane jako nieprawidłowe przy wczytaniu remapper zostawia w spokoju, więc ich oryginalne bajty wciąż przechodzą rundę po edycji. Zmiana zaznaczenia na jednym okienku przepisuje tylko rekordy tego pane'a i zostawia rekordy pozostałych pane'ów identyczne bajt w bajt. Pliki ODS nie wozi żadnego stanu zaznaczenia okienek, bo OpenDocument nie ma równoważnej struktury widoku arkusza per okienko; zaznaczenia i pozycje przewinięcia okienek dotyczą XLS i XLSX
Jeśli twoja aplikacja zapisuje skoroszyty, które ludzie otwierają, żeby przejrzeć oflagowane komórki, wrócić do miejsca, w którym skończyli, albo odczytać zamrożony dashboard, wywołania zaznaczania i przewijania świadome okienek przychodzą z komponentem arkuszowym HotXLS dla Delphi i zachowują się tak samo dla wyjścia .xls i .xlsx