Technisch artikel

HotXLS BIFF8 Selection-records en pane-scrollen in Delphi

HotXLS bewaart werkbladselecties en scrollposities per pane via één pane-bewuste API op zowel TXLSWorksheet als TXLSXWorksheet: SelectAreas, GetSelectedAreas, ScrollWindow en TryGetWindowScroll. Voor klassieke .xls-bestanden schrijft HotXLS BIFF8 Selection-records (0x001D) van hooguit 1369 areas per stuk, zet logische panenamen om naar de pane-bytes die het formaat definieert, en laat elke scrollas liggen op het Window2- of Pane-record waar Excel hem verwacht

Het probleem duikt meestal op in een reconciliatie- of audittool. De tool opent een export van een grootboek, vindt elke cel die afwijkt van het bronsysteem en slaat het werkboek op met die cellen al geselecteerd onder een bevroren kopregel, zodat de beoordelaar op de verschillen uitkomt in plaats van ernaar te hoeven scrollen. Met veertig verschillen werkt dat prima. Het maandafsluitingsbestand heeft er 3.000, en één Selection-record met 3.000 areas kan niet bestaan: zijn body zou 18.009 bytes nodig hebben, ruim twee keer zo veel als één BIFF8-record kan meedragen. De scrollpositie heeft een vergelijkbare valkuil. Op een werkblad met bevroren panen is "waar de gebruiker keek" vier panen die twee rijposities en twee kolomposities delen, niet één coördinaat

Waarom heeft een grote selectie meer dan één Selection-record nodig?

Een grote selectie heeft meerdere records nodig omdat de body van een BIFF8-record is afgekap op 8224 bytes en elke geselecteerde area een vaste zes bytes kost. [MS-XLS] §2.4.248 beschrijft het Selection-record als een vast deel van 9 bytes (de pane-byte, rwAct en colAct voor de actieve cel, irefAct voor de actieve area en cref voor het area-aantal), gevolgd door cref RefU-structuren, die elk twee 16-bit rijen en twee 8-bit kolommen bevatten. Het grootste aantal dat past is (8224 − 9) / 6 naar beneden afgerond, dus 1369, en dat levert een body van 8223 bytes op, één byte onder de limiet. TXLSWorksheet.StoreSelectionGroup gebruikt die constante als MaxAreasPerRecord en schrijft een grotere groep als opeenvolgende Selection-records voor hetzelfde pane, telkens 1369 areas

Het detail dat bijt, is irefAct. Elk blok herhaalt dezelfde actieve rij, actieve kolom en actieve area-index, en irefAct indexeert de geaggregeerde reeks van alle blokken samen, niet de areas binnen het record dat hem meedraagt. Een selectie van één area voorbij de limiet maakt dit concreet: 1370 areas waarvan de laatste actief is worden twee records, het eerste met cref 1369 en het tweede met cref 1, en beide dragen irefAct 1369. Die waarde is groter dan het eigen area-aantal van het tweede record. Een reader die irefAct per record tegen cref controleert, verwerpt een geldig bestand, en een reader die zijn staat bij elk record vervangt, verliest de eerste 1369 areas. De HotXLS-reader voegt opeenvolgende records van hetzelfde pane samen tot één groep, eist dat elk blok het over de actieve cel en index eens is, en voert de bereikcontrole pas uit bij het EOF-record van het werkblad, zodra de volledige reeks bekend is. De pane-eerst-overload van SelectAreas heeft daarom geen plafond van 1369 areas. Die valideert elke A1-verwijzing en de actieve index voordat hij de schrijfvergrendeling van het werkblad pakt, en geeft False terug met de vorige selectie ongewijzigd als er iets misvormd is

Waarom HotXLS een grote werkbladselectie als meerdere BIFF8 Selection-records schrijft: de body-afkap van 8224 bytes biedt ruimte aan 9 vaste bytes plus 1369 RefU-areas van zes bytes, dus 3.000 areas worden drie records van hetzelfde pane met 1369, 1369 en 262, en irefAct indexeert de geaggregeerde reeks, zodat 1370 areas met de laatste actief beide records irefAct 1369 geven
Elk blok herhaalt dezelfde actieve cel en index, de HotXLS-reader voegt opeenvolgende records van hetzelfde pane samen tot één groep, en de bereikcontrole draait pas bij het EOF-record zodra de volledige reeks bekend is
var
  Book: TXLSWorkbook;
  Sheet: TXLSWorksheet;
  Diffs: TXLSSelectedAreas;
  I: Integer;
begin
  Book := TXLSWorkbook.Create;
  try
    Sheet := Book.Sheets.Add;
    Sheet.FreezePanes(1, 1);           // kopregelrij en kolom A blijven staan

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

    // Bevriezen reset de opgeslagen selectie, dus selecteer na het bevriezen.
    // 3000 areas worden opgeslagen als drie Selection-records: 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;

Welke pane-byte gebruikt een Selection-record?

Een Selection-record identificeert zijn pane met de numerieke code die het formaat definieert: 0 voor rechtsonder, 1 voor rechtsboven, 2 voor linksonder en 3 voor linksboven. De publieke enumeratie TXLSPanePosition is gedeclareerd in leesvolgorde, xlspTopLeft, xlspTopRight, xlspBottomLeft, xlspBottomRight, dus Ord(xlspTopLeft) is 0, wat in het bestand het pane rechtsonder is. De enum rechtstreeks in de pane-byte casten zou elke selectie linksboven op het pane rechtsonder schrijven zonder enige foutmelding. Elk pane-bewust HotXLS-toegangspunt converteert de enum daarom via een expliciete case-statement, zodat callers nooit met de numerieke codes te maken krijgen. Het bestaan van het pane wordt ook gecontroleerd: het pane rechtsboven bestaat alleen bij een verticale split, linksonder alleen bij een horizontale, en rechtsonder alleen bij beide. Voor een pane die de huidige split- of freeze-geometrie niet heeft, geeft SelectAreas False terug en GetSelectedAreas een lege array met ActiveAreaIndex op -1, zonder in het werkboek een pane, een selectieobject of een cel aan te maken

Hoe HotXLS TXLSPanePosition op de BIFF8 Selection-pane-byte afbeeldt: de enum is gedeclareerd in leesvolgorde, zodat Ord(xlspTopLeft) 0 is, terwijl het bestand 0 definieert voor rechtsonder, 1 voor rechtsboven, 2 voor linksonder en 3 voor linksboven, dus elk pane-bewust toegangspunt converteert via een expliciete case-statement
De enum rechtstreeks in de pane-byte casten zou elke selectie linksboven op het pane rechtsonder schrijven, dus HotXLS controleert het bestaan van het pane ook tegen de huidige split- of freeze-geometrie voordat er wordt geschreven

Waar staat de scrollpositie van elk pane?

De scrollpositie van elk pane is verdeeld over twee records, want vier panen delen slechts twee rijposities en twee kolomposities. In een klassiek werkboek zijn de eerste zichtbare rij van de bovenste panen en de eerste zichtbare kolom van de linker panen Window2.rwTop en Window2.colLeft, terwijl de rij van de onderste panen en de kolom van de rechter panen Pane.rwTop en Pane.colLeft zijn. ScrollWindow(xlspTopRight, R, C) schrijft daarom Window2.rwTop en Pane.colLeft, en door de kolom van het pane rechtsboven te zetten verschuift ook het pane rechtsonder, precies zoals de twee in Excel één horizontale scrollbar delen. De publieke methoden gebruiken rij- en kolomnummers vanaf 1. Een ontbrekend pane geeft False terug en zet beide query-uitvoeren op nul, en een coördinaat buiten bereik wordt geweigerd voordat er een van de twee assen verandert. Niets hieraan hangt af van hoe een viewer het raster tekent. Een rendering-control houdt zijn eigen TopRow en LeftCol bij, zoals het artikel over werkboeken renderen in een custom VCL-grid beschrijft, en dat is runtime-staat, niet wat er wordt opgeslagen

Waar elke HotXLS pane-scrollas leeft: vier panen delen twee rij- en twee kolomposities, dus de bovenste rij en linker kolom zijn Window2.rwTop en Window2.colLeft terwijl de onderste rij en rechter kolom Pane.rwTop en Pane.colLeft zijn, en ScrollWindow(xlspTopRight, 1, 6) schrijft één Window2-veld plus één Pane-veld, waardoor rechtsonder volgt
XLSX spreidt dezelfde data over de sheetView- en pane-topLeftCell-attributen, en het samenvouwen van de twee lagen tot één is precies hoe een bovenste of linker scrollpositie stilletjes verdwijnt bij het laden

XLSX spreidt dezelfde data over twee elementen: sheetView/@topLeftCell (ECMA-376 Part 1, §18.3.1.87) voor het venster als geheel en het kind pane/@topLeftCell (§18.3.1.66) voor de rechtsonderzijde van een split. Beide attributen kunnen tegelijk aanwezig zijn. HotXLS leest het buitenste attribuut eerst in de velden op vensterniveau, laat het kind pane alleen de velden op pane-niveau overschrijven en schrijft beide apart terug. De twee lagen samenvouwen tot één is precies hoe een bovenste of linker scrollpositie stilletjes verdwijnt bij het laden. Werkbladkopieën dragen beide lagen in beide engines. De oudere toegangspunten houden hun oorspronkelijke gedrag: de klassieke eigenschappen ScrollRow en ScrollColumn, en de op nul gebaseerde XLSX-methoden SetPaneScroll en GetPaneScroll. De freeze- en split-geometrie zelf wordt ingesteld met de instellingen op werkbladniveau die in werkbladbeveiliging, pagina-instelling en afdrukken worden behandeld

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

  // Rechtsonder: onderste rijas (Pane.rwTop) en rechter kolomas (Pane.colLeft)
  Sheet.ScrollWindow(xlspBottomRight, 500, 3);

  // Rechtsboven deelt de rechter kolomas, dus dit verschuift rechtsonder ook naar kolom 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]));
    // Rechtsonder begint op rij 500, kolom 6
end;

Wat gebeurt er als een Selection-record corrupt is?

Als een Selection-record corrupt is, houdt HotXLS hem als onleesbare bytes bij, meldt diagnostische code 1304 (xlsDiagnosticSelectionRecordInvalid) en schrijft de oorspronkelijke body bij het opslaan byte voor byte terug. Voordat een record aansluit bij de groep van zijn pane, controleert de reader hem in volgorde. De pane-byte moet 3 of lager zijn. Records voor één pane moeten aaneengesloten in de stream staan. De 9 vaste bytes moeten aanwezig zijn. cref moet tussen 1 en 1369 liggen, en de body moet precies 9 + cref × 6 bytes lang zijn. Elk blok in een groep moet het over de actieve cel en irefAct eens zijn, irefAct mag zijn tekenbit niet gezet hebben, de actieve kolom moet op het raster liggen en geen enkele area mag omgekeerde grenzen hebben. Problemen in één fysiek record worden één keer per record gemeld. Tegenstrijdigheden die pas na aggregatie zichtbaar worden, zoals irefAct die voorbij het totale area-aantal wijst of een actieve cel buiten de geïndexeerde area, worden één keer per groep gemeld bij EOF. Een ongeldige groep blijft onzichtbaar voor de getypeerde API: GetSelectedAreas geeft voor dat pane een lege array met index -1 terug, terwijl elk ander pane gewoon blijft werken

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;

Hoe overleven selecties rij- en kolominserties?

Selecties overleven structurele bewerkingen, want het invoegen of verwijderen van hele rijen of kolommen zet elke vertegenwoordigde pane-groep in zowel de classic- als de XLSX-engine om via één gedeelde remapper. Overlevende areas houden hun volgorde en de actieve area houdt zijn identiteit. Als de actieve area wordt verwijderd, wordt de eerste overlevende opvolger actief, en anders de laatste overlevende voorganger als er niets volgt. Als elke area wordt verwijderd, klapt de groep in tot één cel op de verwijdergrens, en een actieve cel die niet meer binnen de gekozen area valt schuift door naar de linkerbovenhoek van die area, zodat index en coördinaat elkaar nooit tegenspreken. De grenzen zijn bewust gekozen. Ongeldige klassieke groepen worden door de remapper overgeslagen in plaats van herschreven tot een verzonnen selectie, zodat hun oorspronkelijke bytes nog steeds een round-trip overleven. Het bewerken van één pane vervangt alleen de records van dat pane en laat de andere byte-identiek. ODS krijgt helemaal geen pane-selectiestaat, omdat ODF geen equivalente werkbladview-structuur heeft om die te dragen

Als uw applicatie .xls-bestanden schrijft die gebruikers openen en moeten doornemen, of nu om gemarkeerde cellen te beoordelen, verder te gaan waar ze stopten of een bevroren dashboard te delen: de pane-bewuste selectie- en scroll-API hoort bij de HotXLS Delphi spreadsheet-component, en werkt hetzelfde voor XLS en XLSX