Technisch artikel

HotXLS BIFF8 Selection-records en pane-scrollen in Delphi

Een reconciliatietool vindt 3.000 cellen die niet kloppen met het grootboek en wil dat het opgeslagen .xls opent met ze allemaal geselecteerd onder een bevroren koprij. Op papier is dat één Selection-record. In bytes is het 9 + 3.000 × 6 = 18.009, en een BIFF8-recordbody stopt bij 8.224. Hetzelfde werkbook roept een tweede vraag op die triviaal klinkt en dat niet is: op een blad bevroren op B2 is "de scrollpositie" vier panen die twee rijoffsets en twee kolomoffsets delen, en die vier getallen wonen in twee verschillende records

Beide problemen zijn formaatrekenkunde: de selectie wordt geknipt in records die een lezer weer aan elkaar moet naaien, het pane wordt benoemd door een code die de leesvolgorde niet volgt, en elke scrollas belandt in het record waaruit Excel hem leest

Waarom 3.000 geselecteerde areas drie Selection-records nodig hebben

[MS-XLS] §2.4.248 legt het Selection-record (0x001D) op als een vast deel van 9 bytes gevolgd door de areas. Het vaste deel bevat de pane-byte, rwAct en colAct voor de actieve cel, irefAct voor de index van het actieve area en cref voor het areaaantal. Elk area is een RefU-structuur met twee 16-bit rijen en twee 8-bit kolommen. Het grootste aantal dat past is (8.224 − 9) / 6 naar beneden afgerond, wat 1.369 areas en een body van 8.223 bytes oplevert, één byte onder het plafon

HotXLS gebruikt 1.369 als zijn plafon per record. Als TXLSWorksheet een selectiegroep met meer areas dan dat opslaat, schrijft hij opeenvolgende Selection-records voor hetzelfde pane, telkens 1.369 areas, en elk record herhaalt dezelfde pane-byte, actieve cel en actieve index. De reconcilieselectie van 3.000 cellen komt eruit als drie records met 1.369, 1.369 en 262 areas. Aanroepers zien die splitsing nooit; zij overhandigen een array van A1-referenties

var
  Book: TXLSWorkbook;
  Sheet: TXLSWorksheet;
  Diffs: TXLSSelectedAreas;
  I: Integer;
begin
  Book := TXLSWorkbook.Create;
  try
    Sheet := Book.Sheets.Add;
    Sheet.FreezePanes(1, 1);           // koprij en kolom A blijven staan

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

    // FreezePanes wist de bewaarde 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;

De volgorde van aanroepen telt hier. FreezePanes bouwt de pane-indeling opnieuw en wist elke per-pane-selectie die het blad droeg, dus een eerdere selectie zou bij het opslaan al weg zijn. Op een niet-bevroren blad bestaat het pane rechtsonder nog niet, en dezelfde aanroep SelectAreas zou simpelweg False teruggeven. Eerst bevriezen, dan selecteren

Waarnaar wijst irefAct als een selectie over meerdere records loopt?

In een gesplitste selectie indexeert irefAct de hele geaggregeerde reeks areas over alle brokken heen, en elk brok draagt dezelfde waarde. Een selectie van één area voorbij de grens maakt dat concreet: 1.370 areas waarvan de laatste actief is, worden twee records, het eerste met cref 1.369 en het tweede met cref 1, en allebei dragen ze irefAct 1.369. In het tweede record is die index groter dan het eigen areaaantal van het record. Dat oogt 'kapot' als je één record per keer leest, en twee soorten lezers struikelen erover

  • Een lezer die irefAct tegen cref binnen elk record controleert, wijst een volkomen geldig bestand af bij het tweede brok
  • Een lezer die zijn selectietoestand bij elk Selection-record vervangt, houdt alleen het laatste brok over en verliest de eerste 1.369 areas

De HotXLS-lezer plakt opeenvolgende records voor hetzelfde pane aan één groep en eist dat elk brok het over de actieve rij, actieve kolom en irefAct eens is. De bereikcontrole op irefAct, en de controle dat de actieve cel echt binnen het area zit waar hij naar wijst, draaien pas wanneer het EOF-record van het werkblad binnenkomt en de volledige reeks bekend is. Eerder controleren zou simpelere code zijn en voor elke selectie van meer dan 1.369 areas het verkeerde antwoord geven

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

Aan de schrijfkant heeft de pane-eerst-overload SelectAreas geen eigen plafon van 1.369 areas. Hij parst elke referentie voordat hij het blad aanraakt: $-markeringen worden geaccepteerd en gestript, een omgekeerd bereik zoals C9:A2 wordt genormaliseerd naar A2:C9, en een lege of onparbare entry faalt de hele aanroep. De actieve index moet binnen de array vallen en op of onder 32.767 blijven, want irefAct is een signed 16-bit veld. Elke faal geeft False terug en laat de vorige selectie exact zoals hij was

Pane-codes volgen de leesvolgorde niet

Een Selection-record benoemt 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 in leesvolgorde gedeclareerd: xlspTopLeft, xlspTopRight, xlspBottomLeft, xlspBottomRight. Dus Ord(xlspTopLeft) is 0, en 0 in het bestand betekent rechtsonder. Een schrijver die de enum rechtstreeks in de pane-byte cast, zet elke selectie linksboven op het pane rechtsonder, en het bestand blijft structureel geldig want allebei zijn legale pane-codes

Elk pane-bewust instappunt in HotXLS zet de enum om via een expliciete case-statement, dus applicatiecode raakt de numerieke codes nooit aan. Het bestaan van een pane wordt ook gecontroleerd tegen de huidige freeze- of split-geometrie. Het pane linksboven bestaat altijd. Rechtsboven vraagt om een verticale splitsing, linksonder om een horizontale, en rechtsonder om allebei. Voor een pane dat het blad niet heeft, geeft SelectAreas False terug en geeft GetSelectedAreas een lege array terug met ActiveAreaIndex op -1, en geen van beide aanroepen maakt onderweg een pane, een selectie of een cel aan

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

Het werkblad houdt ook de oudere twee-argument-overload SelectAreas(Areas, ActiveAreaIndex) bij, die naar het pane schrijft dat op dit moment actief is en een Integer-status teruggeeft. Op een blad met bevroren panen is dat vaak een gok over een toestand die je zelf niet zette. Gebruik voor alles wat op bevroren of gesplitste bladen draait eerst de overload die een TXLSPanePosition neemt; het pane zit dan in de aanroep zelf, en een uitkomst False vertelt je dat geometrie en verzoek het oneens zijn. Het resultaat teruglezen volgt dezelfde vorm:

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;

Waar staat de scrollpositie van elk pane?

De scrollpositie van elk pane is verspreid over twee records, want vier panen delen maar twee rijposities en twee kolomposities. In een klassiek werkbook is de eerst zichtbare rij van de panen 'boven' en de eerst zichtbare kolom van de panen 'links' te vinden in Window2.rwTop en Window2.colLeft. De rij van de onderste panen en de kolom van de rechterpanen staan in Pane.rwTop en Pane.colLeft. Rechtsboven haalt zijn rij dus uit Window2 en zijn kolom uit Pane, en linksonder doet het omgekeerde

ScrollWindow en TryGetWindowScroll volgen die indeling precies en gebruiken rij- en kolomnummers vanaf één. ScrollWindow(xlspTopRight, R, C) schrijft Window2.rwTop en Pane.colLeft, wat betekent dat het zetten van de kolom rechtsboven ook het pane rechtsonder verschuift. Dat past bij Excel, waar de twee rechterpanen één horizontale scrollbar delen. Een ontbrekend pane maakt dat ScrollWindow False teruggeeft en dat TryGetWindowScroll beide uitvoeren op nul zet. Een rij of kolom buiten bereik wordt geweigerd voordat een van de twee assen verandert, dus een slechte kolom kan geen half toegepaste rij achterlaten

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

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

  // Top-right deelt de rechterkolomas, dus dit verplaatst 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;
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 bewaart dezelfde twee lagen in twee elementen: sheetView/@topLeftCell (ECMA-376 Deel 1, §18.3.1.87) voor het venster als geheel en het kindelement pane/@topLeftCell (§18.3.1.66) voor de onderkant van een splitsing. Allebei de attributen kunnen tegelijk aanwezig zijn. Een lader die er maar één van de twee vasthoudt, verliest ofwel de bovenste rijen ofwel de linker kolommen van elk bevroren blad dat hij aanraakt. TXLSXWorksheet leest het buitenste attribuut eerst in de vensterniveau-velden, laat het pane-kind alleen de pane-niveau-velden overschrijven en schrijft allebei apart terug. ScrollWindow en TryGetWindowScroll hebben op TXLSXWorksheet dezelfde handtekeningen en betekenis, en een werkblad kopieeren draagt beide lagen mee in elk van beide formaten

De smallere eigenschappen zijn er nog steeds voor code die maar één aspaar nodig heeft: ScrollRow en ScrollColumn op het klassieke werkblad, en de nulgebaseerde SetPaneScroll en GetPaneScroll voor het pane rechtsonder op XLSX. Een renderingbesturing houdt zijn eigen TopRow en LeftCol bij, zoals het artikel over werkboeken renderen in een custom VCL grid beschrijft, en die runtimetoestand is los van wat wordt opgeslagen. De freeze- en split-geometrie zelf stel je in met de instellingen op bladniveau die in bladbeveiliging, pagina-instelling en afdrukken worden behandeld

Kapotte Selection-records worden byte voor byte bewaard

Als een Selection-record misvormd is, houdt HotXLS zijn body vast als opaakte bytes, meldt diagnostische code 1304 (xlsDiagnosticSelectionRecordInvalid) als waarschuwing en schrijft bij het opslaan de originele body ongewijzigd terug. Bestanden van onbekende exporters of handgemaakte BIFF-writers kunnen zulke records bevatten. Voordat een record zich bij de groep van zijn pane voegt, controleert de lezer op een pane-byte van 3 of lager, op records die per pane 'aaneengesloten' liggen, op cref tussen 1 en 1.369 met een body van exact 9 + cref × 6 bytes, op brokken die het over de actieve cel eens zijn en een irefAct zonder tekenbit, op een actieve kolom op het raster en op geen enkel omgekeerd area. Problemen binnen één fysiek record worden per record gemeld; tegenstrijdigheden die pas na aggregatie zichtbaar worden, zoals een irefAct die voorbij het totale areaaantal wijst of een actieve cel buiten het gendexde area, worden per groep gemeld bij EOF

Een ongeldige groep blijft buiten de getypte API: GetSelectedAreas geeft voor dat pane een lege array terug met index -1, en de andere panen blijven normaal 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 is je eigen TStrings; HotXLS vult alleen Diagnostics
      Log.Add(Format('%s: record $%.4x kept opaque (%s)',
        [D.SheetName, D.RecordId, D.Message]));
  end;
end;

Het record repareren, door irefAct af te knijpen of het omgekeerde area te laten vallen, was de andere optie. Bewaren is de veiligere standaard: een gerepareerde selectie is er een die niemand koos, en de originele bytes zouden weg zijn. De diagnostische melding benoemt de controle die faalde, zoals "het areaaantal klopt niet met de recordlengte" of "records voor hetzelfde pane liggen niet aaneengesloten", en RecordId is voor deze code altijd $001D

Rij- en kolominvoegingen herzien de selectie van elk pane

Hele rijen of kolommen invoegen of verwijderen herijkt de selectie van elk pane via één gedeelde routine, in zowel de klassieke als de XLSX-engine. Overlevende areas houden hun volgorde en schuiven mee met de cellen die ze bedekken, en het actieve area behoudt zijn 'identiteit'. Is het actieve area verwijderd, dan wordt het eerste overlevende area erna actief; volgt er niets, dan het laatste overlevende area ervoor. Zijn alle areas verwijderd, dan krimpt de groep tot één cel op de verwijdergrens. Een actieve cel die niet meer binnen het gekozen area valt, schuift naar de linkerbovenhoek van dat area, zodat index en coördinaat elkaar in het opgeslagen bestand nooit tegenspreken

Groepen die bij het laden als ongeldig waren aangemerkt, laat de hermapper met rust, zodat hun originele bytes ook na de bewerking 'ongeschonden' meedraaien. De selectie op één pane veranderen herschrijft alleen de records van dat pane en laat de records van de andere panen byte-identiek. ODS-bestanden dragen helemaal geen pane-selectietoestand, want OpenDocument heeft geen equivalente werkweergavestructuur per pane; selecties en pane-scrollposities gelden voor XLS en XLSX

Slaat je applicatie werkboeken op die mensen openen om gemarkeerde cellen te beoordelen, om het werk ergens op te pakken of om een bevroren dashboard te lezen, dan worden de pane-bewuste selectie- en scrollaanroepen geleverd met het HotXLS Delphi spreadsheet component en gedragen ze zich hetzelfde voor .xls- als .xlsx-uitvoer