Technický článek

Záznamy Selection a scrollování panelů BIFF8 v HotXLS

HotXLS ukládá výběry listů a scroll pozice per panel přes jedno panel-aware API na TXLSWorksheet i TXLSXWorksheet: SelectAreas, GetSelectedAreas, ScrollWindow a TryGetWindowScroll. U klasických souborů .xls zapisuje HotXLS záznamy Selection BIFF8 (0x001D) s nejvýše 1369 oblastmi každý, převádí logické názvy panelů na pane byty, které formát definuje, a drží každou scroll osu na záznamu Window2 nebo Pane, kde ji Excel čeká

Problém se typicky objeví v reconcilačním nebo auditním nástroji. Nástroj otevře export účetní knihy, najde každou buňku, která nesouhlasí se zdrojovým systémem, a uloží sešit s těmito buňkami už vybranými pod zmrazenou hlavičkou, takže recenzent skočí přímo k rozdílům místo je hledat scrollováním. Se čtyřiceti rozdíly to funguje hezky. Month-end soubor jich má 3000 a jediný záznam Selection s 3000 oblastmi existovat nemůže: jeho tělo by potřebovalo 18 009 bytů, víc než dvakrát tolik, co unese jeden záznam BIFF8. Scroll pozice má podobnou past. Na listu se zmrazenými panely je „to, na co se uživatel díval“ čtyři panely sdílející dvě řádkové a dvě sloupcové pozice, ne jedna souřadnice

Proč potřebuje velký výběr víc než jeden záznam Selection?

Velký výběr potřebuje několik záznamů, protože tělo záznamu BIFF8 má strop 8224 bytů a každá vybraná oblast stojí pevných šest bytů. [MS-XLS] §2.4.248 rozkládá záznam Selection na 9bytovou pevnou část (pane byte, rwAct a colAct aktivní buňky, irefAct aktivní oblasti a cref počtu oblastí) následovanou cref strukturami RefU, z nichž každá drží dva 16bitové řádky a dva 8bitové sloupce. Největší počet, který se vejde, je (8224 − 9) / 6 zaokrouhleno dolů, tedy 1369, a to dá tělo o 8223 bytech, jeden byte pod limitem. TXLSWorksheet.StoreSelectionGroup používá tuhle konstantu jako MaxAreasPerRecord a větší skupinu zapisuje jako sobě následující záznamy Selection pro tentýž panel, 1369 oblastí najednou

Detail, který kousne, je irefAct. Každý chunk opakuje tentýž aktivní řádek, aktivní sloupec a index aktivní oblasti a irefAct indexuje agregovanou sekvenci všech chunků, ne oblasti uvnitř záznamu, který ho nese. Výběr o jednu oblast za limitem to konkrétizuje: 1370 oblastí s poslední aktivní se stane dvěma záznamy, prvním s cref 1369 a druhým s cref 1, a oba nesou irefAct 1369. Ta hodnota je větší než vlastní počet oblastí druhého záznamu. Reader, který kontroluje irefAct proti cref v každém záznamu, odmítne platný soubor a reader, který nahrazuje svůj stav každým záznamem, zahodí prvních 1369 oblastí. Reader HotXLS přidává sobě následující záznamy stejného panelu do jedné skupiny, vyžaduje, aby se každý chunk shodl na aktivní buňce a indexu, a range check pustí až na EOF záznamu listu, jakmile je známá celá sekvence. Panel-first overload SelectAreas proto nemá žádný strop 1369 oblastí. Zvaliduje každou referenci A1 i aktivní index, než vezme write lock listu, a vrátí False s předchozím výběrem nezměněným, pokud je cokoli zborcené

Proč zapisuje HotXLS velký výběr listu jako několik záznamů Selection BIFF8: strop těla 8 224 bytů pojme 9 pevných bytů plus 1369 šestibytových oblastí RefU, takže 3000 oblastí se stane třemi záznamy stejného panelu o 1369, 1369 a 262 a irefAct indexuje agregovanou sekvenci, takže 1370 oblastí s poslední aktivní dává oběma záznamům irefAct 1369
Každý chunk opakuje tutéž aktivní buňku a index, reader HotXLS přidává sobě následující záznamy stejného panelu do jedné skupiny a range check běží až na EOF záznamu, jakmile je známá celá sekvence
var
  Book: TXLSWorkbook;
  Sheet: TXLSWorksheet;
  Diffs: TXLSSelectedAreas;
  I: Integer;
begin
  Book := TXLSWorkbook.Create;
  try
    Sheet := Book.Sheets.Add;
    Sheet.FreezePanes(1, 1);           // hlavičková řádka a sloupec A zůstávají na místě

    SetLength(Diffs, 3000);
    for I := 0 to High(Diffs) do
      Diffs[I] := Format('C%d', [I + 2]);

    // Zmrazení resetuje uložený výběr, proto vybírejte až po zmrazení.
    // 3000 oblastí se ukládá jako tři záznamy 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;

Jaký pane byte používá záznam Selection?

Záznam Selection identifikuje svůj panel číselným kódem, který definuje formát: 0 pro bottom-right, 1 pro top-right, 2 pro bottom-left a 3 pro top-left. Veřejná enumerace TXLSPanePosition je deklarovaná v pořadí čtení, xlspTopLeft, xlspTopRight, xlspBottomLeft, xlspBottomRight, takže Ord(xlspTopLeft) je 0, což je v souboru panel bottom-right. Přetypování enumu rovnou na pane byte by zapsalo každý výběr top-left na panel bottom-right bez jediné chyby. Každý panel-aware vstupní bod HotXLS převádí enum přes explicitní case statement, takže volající se s číselnými kódy nepotkávají vůbec. Kontroluje se i existence panelu: panel top-right existuje jen se svislým splitem, bottom-left jen s vodorovným a bottom-right jen s oběma. Pro panel, který aktuální split nebo zmrazená geometrie nemá, vrátí SelectAreas False a GetSelectedAreas vrátí prázdné pole s ActiveAreaIndex nastaveným na -1, aniž by v sešitu vytvořila panel, objekt výběru nebo buňku

Jak HotXLS mapuje TXLSPanePosition na pane byte záznamu Selection BIFF8: enum je deklarovaný v pořadí čtení, takže Ord(xlspTopLeft) je 0, zatímco soubor definuje 0 pro bottom-right, 1 pro top-right, 2 pro bottom-left a 3 pro top-left, takže každý panel-aware vstupní bod převádí přes explicitní case statement
Přetypování enumu rovnou na pane byte by zapsalo každý výběr top-left na panel bottom-right, takže HotXLS navíc kontroluje existenci panelu proti aktuální split nebo zmrazené geometrii, než zapíše

Kde žije scroll pozice každého panelu?

Scroll pozice každého panelu je rozdělená mezi dva záznamy, protože čtyři panely sdílí jen dvě řádkové a dvě sloupcové pozice. V klasickém sešitu je první viditelný řádek horních panelů a první viditelný sloupec levých panelů Window2.rwTop a Window2.colLeft, zatímco řádek dolních panelů a sloupec pravých panelů jsou Pane.rwTop a Pane.colLeft. ScrollWindow(xlspTopRight, R, C) proto zapisuje Window2.rwTop a Pane.colLeft a nastavení sloupce panelu top-right zároveň posune panel bottom-right, přesně jako když dva v Excelu sdílejí jeden vodorovný scrollbar. Veřejné metody používají čísla řádků a sloupců od jedničky. Chybějící panel vrátí False a nastaví oba výstupy dotazu na nulu a souřadnice mimo range se odmítne, než se změní kterákoli osa. Nic z toho nezávisí na tom, jak prohlížeč maluje mřížku. Renderovací control si drží vlastní TopRow a LeftCol, jak popisuje článek o vykreslování sešitů ve vlastní VCL mřížce, a to je runtime stav, ne to, co se ukládá

Kde žije každá scroll osa panelu v HotXLS: čtyři panely sdílí dvě řádkové a dvě sloupcové pozice, takže horní řádek a levý sloupec jsou Window2.rwTop a Window2.colLeft, zatímco dolní řádek a pravý sloupec jsou Pane.rwTop a Pane.colLeft, a ScrollWindow(xlspTopRight, 1, 6) zapíše jedno pole Window2 plus jedno pole Pane, takže bottom-right následuje
XLSX roztahuje stejná data přes atributy sheetView a pane topLeftCell a slepení obou vrstev do jedné je přesně tím, jak horní nebo levá scroll pozice potichu zmizí při načtení

XLSX roztahuje stejná data přes dva elementy: sheetView/@topLeftCell (ECMA-376 Part 1, §18.3.1.87) pro okno jako celek a dětský pane/@topLeftCell (§18.3.1.66) pro dolní pravou stranu splitu. Oba atributy mohou být přítomné zároveň. HotXLS čte vnější atribut nejdřív do polí na úrovni okna, nechá dětský pane přepsat jen pole na úrovni panelu a obojí zapisuje zpět odděleně. Slepení obou vrstev do jedné je přesně tím, jak horní nebo levá scroll pozice potichu zmizí při načtení. Kopie listů nesou obě vrstvy v obou enginech. Starší vstupní body si drží původní chování: klasické vlastnosti ScrollRow a ScrollColumn a zero-based XLSX SetPaneScroll a GetPaneScroll. Samotnou geometrii zmrazení a splitu nastavíte listovými volbami popsanými v článku o ochraně listu, page setupu a tisku

var
  Row, Col: Integer;
begin
  Sheet.FreezePanes(1, 1);

  // Bottom-right: dolní řádková osa (Pane.rwTop) a pravá sloupcová osa (Pane.colLeft)
  Sheet.ScrollWindow(xlspBottomRight, 500, 3);

  // Top-right sdílí pravou sloupcovou osu, takže tohle posune i bottom-right na sloupec 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]));
    // Bottom-right začíná na řádku 500, sloupci 6
end;

Co se stane, když je záznam Selection corrupt?

Když je záznam Selection corrupt, HotXLS ho nechá jako opaque byty, nahlásí diagnostický kód 1304 (xlsDiagnosticSelectionRecordInvalid) a při uložení zapíše původní tělo zpět bajt po bajtu. Než se záznam připojí ke skupině svého panelu, reader ho kontroluje v pořadí. Pane byte musí být 3 nebo méně. Záznamy jednoho panelu musí být ve streamu kontiguální. Přítomných musí být 9 pevných bytů. cref musí být mezi 1 a 1369 a tělo musí mít přesně 9 + cref × 6 bytů. Každý chunk ve skupině se musí shodnout na aktivní buňce a irefAct, irefAct nesmí mít nastavený sign bit, aktivní sloupec musí být na mřížce a žádná oblast nesmí mít přeházené meze. Problémy v jednom fyzickém záznamu se hlásí jednou na záznam. Rozpory, které vyjdou najevo až po agregaci, jako irefAct mířící za celkový počet oblastí nebo aktivní buňka mimo indexovanou oblast, se hlásí jednou na skupinu u EOF. Neplatná skupina zůstane pro typované API neviditelná: GetSelectedAreas vrátí pro ten panel prázdné pole s indexem -1, zatímco každý jiný panel funguje dál

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.Add(Format('%s: record $%.4x kept opaque (%s)',
        [D.SheetName, D.RecordId, D.Message]));
  end;
end;

Jak výběry přežijí vkládání řádků a sloupců?

Výběry přežijí strukturální editace, protože vložení nebo smazání celých řádků či sloupců přemapuje každou reprezentovanou skupinu panelů v Classic i XLSX engine přes jeden sdílený remapper. Přeživší oblasti si drží pořadí a aktivní oblast si drží identitu. Když se aktivní oblast smaže, aktivní se stane první přeživší následník, a když nic nenásleduje, poslední přeživší předchůdce. Když se smažou všechny oblasti, skupina se slepí do jedné buňky na hranici mazání a aktivní buňka, která už nepadá do vybrané oblasti, se přesune do jejího levého horního rohu, takže index a souřadnice si nikdy neodporují. Meze jsou záměrné. Neplatné classic skupiny remapper vynechá místo toho, aby je přepsal do vymyšleného výběru, takže jejich původní byty stále projdou round-trip. Editace jednoho panelu vymění jen záznamy toho panelu a ostatní nechá bajtově identické. ODS nedostane panel výběr stav vůbec, protože ODF nemá žádnou ekvivalentní strukturu view listu, která by ho nesla

Pokud vaše aplikace zapisuje soubory .xls, které uživatelé otvírají a potřebují se v nich zorientovat — ať už kvůli označeným buňkám, pokračování tam, kde skončili, nebo sdílení zmrazeného dashboardu — panel-aware API pro výběr a scrollování je součástí komponenty HotXLS pro tabulky v Delphi a funguje stejně pro XLS i XLSX