Odborný článok

BIFF8 Selection records a posúvanie panelov v HotXLS

HotXLS ukladá výbery a scroll pozície pracovného hárku na pane cez jedno pane-aware API na TXLSWorksheet aj TXLSXWorksheet: SelectAreas, GetSelectedAreas, ScrollWindow a TryGetWindowScroll. Pre klasické súbory .xls zapisuje HotXLS BIFF8 Selection records (0x001D) po najviac 1369 oblastiach, konvertuje logické názvy panelov na pane bajty, ktoré formát definuje, a drží každú scroll os na recordi Window2 alebo Pane, kde ju Excel očakáva

Problém sa zvyčajne objaví v nástroji na odsúhlasenie alebo audit. Nástroj otvorí export účtovnej knihy, nájde každú bunku, ktorá nesúhlasí so zdrojovým systémom, a uloží workbook s týmito bunkami už označenými pod zamrznutým hlavičkovým riadkom, takže recenzent pristane priamo na rozdieloch namiesto ich hľadania posúvaním. Pri štyridsiatich rozdieloch to funguje pekne. Súbor na konci mesiaca ich má 3 000 a jeden Selection record s 3 000 oblasťami nemôže existovať: jeho telo by potrebovalo 18 009 bajtov, viac než dvojnásobok toho, čo jeden BIFF8 record unesie. Scroll pozícia má podobnú pascu. Na hárku so zamrznutými panelmi je "kde sa užívateľ díval" štyri panely zdieľajúce dve riadkové a dve stĺpcové pozície, nie jedna súradnica

Prečo potrebuje veľký výber viac než jeden Selection record?

Veľký výber potrebuje viac recordov preto, lebo telo BIFF8 recordu je obmedzené na 8224 bajtov a každá vybraná oblasť stojí pevných šesť bajtov. [MS-XLS] §2.4.248 rozkladá Selection record na 9-bajtovú pevnú časť (pane bajt, rwAct a colAct pre aktívnu bunku, irefAct pre aktívnu oblasť a cref pre počet oblastí), za ktorou nasleduje cref štruktúr RefU, každá s dvomi 16-bitovými riadkami a dvomi 8-bitovými stĺpcami. Najväčší počet, ktorý sa zmestí, je (8224 − 9) / 6 zaokrúhlené nadol, teda 1369, a to dáva telo s 8223 bajtmi, jeden bajt pod limitom. TXLSWorksheet.StoreSelectionGroup používa tú konštantu ako MaxAreasPerRecord a zapisuje väčšiu skupinu ako po sebe idúce Selection records pre ten istý pane, vždy 1369 oblastí

Pichľavý detail je irefAct. Každý chunk opakuje ten istý aktívny riadok, aktívny stĺpec a aktívny index oblasti a irefAct indexuje agregovanú postupnosť všetkých chunkov, nie oblasti vnútri recordu, ktorý ho nesie. Konkrétne to ukáže výber o jednu oblasť za limitom: 1370 oblastí s poslednou aktívnou dá dva recordy, prvý s cref 1369 a druhý s cref 1 a oba nesú irefAct 1369. Tá hodnota je väčšia než počet oblastí samotného druhého recordu. Reader, ktorý kontroluje irefAct proti cref v každom recorde, odmietne platný súbor a reader, ktorý pri každom recorde nahradí svoj stav, zahodí prvých 1369 oblastí. Reader v HotXLS pripája po sebe idúce same-pane recordy do jednej skupiny, vyžaduje, aby sa každý chunk zhodol v aktívnej bunke a indexe, a kontrolu range robí až na EOF recordi pracovného hárku, keď je známa celá postupnosť. Pane-first overload SelectAreas preto nemá strop 1369 oblastí. Validuje každú A1 referenciu a aktívny index skôr, než si vezme write lock pracovného hárku, a pri čomkoľvek zdeformovanom vráti False s predchádzajúcim výberom nezmeneným

Prečo zapisuje HotXLS veľký výber pracovného hárku ako viacero BIFF8 Selection recordov: telo s limitom 8 224 bajtov pojme 9 pevných bajtov plus 1369 šesťbajtových oblastí RefU, takže 3 000 oblastí dá tri same-pane recordy 1369, 1369 a 262 a irefAct indexuje agregovanú postupnosť, takže 1370 oblastí s poslednou aktívnou dá obom recordom irefAct 1369
Každý chunk opakuje tú istú aktívnu bunku a index, reader HotXLS pripája po sebe idúce same-pane recordy do jednej skupiny a kontrola range beží až na EOF recordi, keď je známa celá postupnosť
var
  Book: TXLSWorkbook;
  Sheet: TXLSWorksheet;
  Diffs: TXLSSelectedAreas;
  I: Integer;
begin
  Book := TXLSWorkbook.Create;
  try
    Sheet := Book.Sheets.Add;
    Sheet.FreezePanes(1, 1);           // hlavičkový riadok a stĺpec A zostávajú

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

    // Zamrznutie resetuje uložený výber, vyberajte preto po zamrznutí.
    // 3000 oblastí sa uloží ako tri Selection recordy: 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;

Ktorý pane bajt používa Selection record?

Selection record identifikuje svoj pane číselným kódom, ktorý definuje formát: 0 pre bottom-right, 1 pre top-right, 2 pre bottom-left a 3 pre top-left. Verejná enumerácia TXLSPanePosition je deklarovaná v poradí čítania, xlspTopLeft, xlspTopRight, xlspBottomLeft, xlspBottomRight, takže Ord(xlspTopLeft) je 0, čo je v súbore bottom-right pane. Priamy prenos enumu do pane bajtu by zapísal každý top-left výber na bottom-right panel bez akéhokoľvek chybového hlásenia. Každý pane-aware vstupný bod HotXLS konvertuje enum namiesto toho cez explicitný case statement, takže volajúci sa s číselnými kódami nikdy nestretne. Kontroluje sa aj existencia panelu: top-right panel existuje len pri vertikálnom split, bottom-left len pri horizontálnom a bottom-right len pri oboch. Pre panel, ktorý aktuálna geometria split alebo freeze nemá, vráti SelectAreas False a GetSelectedAreas prázdne pole s ActiveAreaIndex nastaveným na -1, bez vytvorenia panelu, selection objektu alebo bunky v workbooku

Ako mapuje HotXLS TXLSPanePosition na pane bajt BIFF8 Selection: enum je deklarovaný v poradí čítania, takže Ord(xlspTopLeft) je 0, kým súbor definuje 0 pre bottom-right, 1 pre top-right, 2 pre bottom-left a 3 pre top-left, takže každý pane-aware vstupný bod konvertuje cez explicitný case statement
Priamy prenos enumu do pane bajtu by zapísal každý top-left výber na bottom-right panel, takže HotXLS pred zápisom kontroluje aj existenciu panelu proti aktuálnej geometrii split alebo freeze

Kde žije scroll pozícia každého panelu?

Scroll pozícia každého panelu je rozdelená do dvoch recordov, pretože štyri panely zdieľajú len dve riadkové a dve stĺpcové pozície. V klasickom workbooku je prvý viditeľný riadok horných panelov a prvý viditeľný stĺpec ľavých panelov Window2.rwTop a Window2.colLeft, zatiaľ čo riadok spodných panelov a stĺpec pravých panelov sú Pane.rwTop a Pane.colLeft. ScrollWindow(xlspTopRight, R, C) preto zapisuje Window2.rwTop a Pane.colLeft a nastavenie stĺpca top-right panelu posunie aj bottom-right panel, rovnako ako obe v Exceli zdieľajú jeden horizontálny scrollbar. Verejné metódy používajú čísla riadkov a stĺpcov od 1. Chýbajúci panel vráti False a oba výstupy dopytu nastaví na nulu a súradnica mimo rozsahu sa odmietne skôr, než sa ktorákoľvek os zmení. Nič z toho nezávisí od toho, ako viewer maľuje mriežku. Rendering control si drží vlastné TopRow a LeftCol, ako popisuje článok o renderovaní workbookov vo vlastnom VCL gride, a to je runtime stav, nie to, čo sa ukladá

Kde žije každá scroll os panelov HotXLS: štyri panely zdieľajú dve riadkové a dve stĺpcové pozície, takže horný riadok a ľavý stĺpec sú Window2.rwTop a Window2.colLeft, kým spodný riadok a pravý stĺpec sú Pane.rwTop a Pane.colLeft a ScrollWindow(xlspTopRight, 1, 6) zapisuje jedno pole Window2 plus jedno pole Pane, takže bottom-right nasleduje
XLSX rozhadzuje tie isté dáta cez atribúty topLeftCell sheetView a pane a zliatie týchto dvoch vrstiev do jednej je presne spôsob, akým horná alebo ľavá scroll pozícia poticho zmizne pri načítaní

XLSX rozhadzuje tie isté dáta do dvoch elementov: sheetView/@topLeftCell (ECMA-376 Part 1, §18.3.1.87) pre okno ako celok a dieťa pane/@topLeftCell (§18.3.1.66) pre pravú spodnú stranu splitu. Oba atribúty môžu byť prítomné naraz. HotXLS číta vonkajší atribút najprv do polí na úrovni okna, nechá dieťa pane prepísať len polia na úrovni panelu a zapisuje obe späť oddelene. Zliatie týchto dvoch vrstiev do jednej je presne spôsob, akým horná alebo ľavá scroll pozícia poticho zmizne pri načítaní. Kópie hárkov nesú obe vrstvy v oboch engineoch. Staršie vstupné body si držia pôvodné správanie: klasické vlastnosti ScrollRow a ScrollColumn a zero-based XLSX SetPaneScroll a GetPaneScroll. Samotnú geometriu freeze a split nastavíte cez nastavenia na úrovni hárku pokryté v ochrane hárku, page setup a tlači

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

  // Bottom-right: spodná riadková os (Pane.rwTop) a pravá stĺpcová os (Pane.colLeft)
  Sheet.ScrollWindow(xlspBottomRight, 500, 3);

  // Top-right zdieľa pravú stĺpcovú os, takže toto posunie aj bottom-right na stĺpec 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čína na riadku 500, stĺpec 6
end;

Čo sa stane, keď je Selection record corrupt?

Keď je Selection record corrupt, HotXLS si ho nechá ako opaque bajty, nahlási diagnostický kód 1304 (xlsDiagnosticSelectionRecordInvalid) a pri ukladaní zapíše pôvodné telo späť bajt za bajtom. Skôr než sa record pridá do skupiny svojho panelu, reader ho skontroluje v poradí. Pane bajt musí byť 3 alebo menej. Recordy jedného panelu musia byť v streame súvislé. Pevných 9 bajtov musí byť prítomných. cref musí byť medzi 1 a 1369 a telo musí mať presne 9 + cref × 6 bajtov. Každý chunk v skupine sa musí zhodnúť v aktívnej bunke a irefAct, irefAct nesmie mať nastavený sign bit, aktívny stĺpec musí byť na mriežke a žiadna oblasť nesmie mať prevrátené hranice. Problémy v jednom fyzickom recorde sa hlásia raz na record. Rozpory, ktoré sa ukážu až po agregácii, ako irefAct ukazujúci za celkový počet oblastí alebo aktívna bunka mimo indexovanej oblasti, sa hlásia raz na skupinu na EOF. Neplatná skupina zostáva pre typované API neviditeľná: GetSelectedAreas vráti prázdne pole s indexom -1 pre ten panel, zatiaľ čo každý ostatný panel funguje ďalej

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;

Ako prežijú výbery vloženia riadkov a stĺpcov?

Výbery prežijú štrukturálne úpravy preto, lebo vloženie alebo zmazanie celých riadkov alebo stĺpcov presunie každú zastúpenú pane skupinu v klasickom aj XLSX engine cez jeden zdieľaný remapper. Preživšie oblasti si držia poradie a aktívna oblasť si drží identitu. Ak sa aktívna oblasť zmaže, aktívnou sa stane prvý preživší nasledovník, prípadne posledný preživší predchodca, ak nič nenasleduje. Ak sa zmažú všetky oblasti, skupina sa zrúti na jednu bunku na hranici zmazania a aktívna bunka, ktorá už nespadá do vybranej oblasti, sa presunie do jej ľavého horného rohu, takže index a súradnica si nikdy protirečia. Limity sú zámerné. Neplatné klasické skupiny remapper preskočí namiesto toho, aby ich prepísal do vymysleného výberu, takže ich pôvodné bajty aj naďalej prechádzajú round-tripom. Úprava jedného panelu nahradí len recordy toho panelu a ostatné nechá byte-identické. ODS nedostáva pane selection stav vôbec, pretože ODF nemá ekvivalentnú štruktúru worksheet view, ktorá by ho niesla

Ak vaša aplikácia zapisuje súbory .xls, ktoré užívatelia otvárajú a potrebujú sa v nich pohybovať, či už kvôli prehľadu označených buniek, pokračovaniu tam, kde skončili, alebo zdieľaniu zamrznutého dashboardu, pane-aware selection a scroll API je súčasťou HotXLS Delphi spreadsheet component a funguje rovnako pre XLS aj XLSX