Teknisk artikkel

HotXLS BIFF8 Selection-records og pane-scroll i Delphi

HotXLS lagrer regnearkutvalg og scrollposisjoner per pane gjennom ett felles pane-bevisst API på både TXLSWorksheet og TXLSXWorksheet: SelectAreas, GetSelectedAreas, ScrollWindow og TryGetWindowScroll. For klassiske .xls-filer skriver HotXLS BIFF8 Selection-records (0x001D) på maksimalt 1369 områder hver, konverterer logiske panenavn til de pane-bytene formatet definerer, og beholder hver scrollakse på Window2- eller Pane-recorden der Excel forventer den

Problemet dukker vanligvis opp i et avstemmings- eller revisjonsverktøy. Verktøyet åpner en hovedbokseksport, finner hver celle som er uenig med kildesystemet, og lagrer arbeidsboken med de cellene allerede valgt under en frossen topptekst-rad, så gjennomleseren lander på avvikene i stedet for å scrolle etter dem. Med førti avvik fungerer det fint. Månedsavslutningsfilen har 3000, og én Selection-record med 3000 områder kan ikke eksistere: kroppen dens ville trenge 18 009 byte, mer enn dobbelt av hva én BIFF8-record kan bære. Scrollposisjonen har en lignende felle. På et ark med frosne paner er «der brukeren så» fire paner som deler to radposisjoner og to kolonneposisjoner, ikke én koordinat

Hvorfor trenger et stort utvalg mer enn én Selection-record?

Et stort utvalg trenger flere records fordi en BIFF8-record-kropp er begrenset til 8224 byte og hvert valgt område koster fast seks byte. [MS-XLS] §2.4.248 legger opp Selection-recorden som en fast del på 9 byte (pane-byten, rwAct og colAct for den aktive cellen, irefAct for det aktive området og cref for områdetallet) etterfulgt av cref RefU-strukturer, hver med to 16-bit rader og to 8-bit kolonner. Største antall som får plass, er (8224 − 9) / 6 avrundet ned, som er 1369, og det gir en kropp på 8223 byte, én byte under grensen. TXLSWorksheet.StoreSelectionGroup bruker den konstanten som MaxAreasPerRecord og skriver en større gruppe som sammenhengende Selection-records for samme pane, 1369 områder om gangen

Detaljen som biter, er irefAct. Hver chunk gjentar samme aktive rad, aktive kolonne og aktive områdeindeks, og irefAct indekserer den aggregerte sekvensen av alle chunks, ikke områdene inne i recorden som bærer den. Et utvalg ett område over grensen gjør dette konkret: 1370 områder med det siste aktive blir to records, den første med cref 1369 og den andre med cref 1, og begge bærer irefAct 1369. Den verdien er større enn den andre recordens eget områdetall. En leser som sjekker irefAct mot cref i hver record, avviser en gyldig fil, og en leser som erstatter tilstanden sin ved hver record, mister de første 1369 områdene. HotXLS-leseren legger sammenhengende same-pane-records inn i én gruppe, krever at hver chunk er enig om aktiv celle og indeks, og kjører rekkeviddesjekken først ved regnearkets EOF-record, når hele sekvensen er kjent. Overloaden av SelectAreas som tar pane først, har derfor ingen grense på 1369 områder. Den validerer hver A1-referanse og aktiv indeks før den tar skrivelåsen på regnearket, og den returnerer False med forrige utvalg urørt hvis noe er feilformet

Hvorfor HotXLS skriver et stort regnearkutvalg som flere BIFF8 Selection-records: kroppstaket på 8224 byte rommer 9 faste byte pluss 1369 RefU-områder på seks byte, så 3000 områder blir tre same-pane-records på 1369, 1369 og 262, og irefAct indekserer den aggregerte sekvensen, slik at 1370 områder med det siste aktive gir begge records irefAct 1369
Hver chunk gjentar samme aktive celle og indeks, HotXLS-leseren legger sammenhengende same-pane-records inn i én gruppe, og rekkeviddesjekken kjører først ved EOF-recorden når hele sekvensen er kjent
var
  Book: TXLSWorkbook;
  Sheet: TXLSWorksheet;
  Diffs: TXLSSelectedAreas;
  I: Integer;
begin
  Book := TXLSWorkbook.Create;
  try
    Sheet := Book.Sheets.Add;
    Sheet.FreezePanes(1, 1);           // topptekst-raden og kolonne A blir værende

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

    // Frysing nullstiller det lagrede utvalget, så velg etter frysing.
    // 3000 områder lagres som tre 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;

Hvilken pane-byte bruker en Selection-record?

En Selection-record identifiserer panen sin med den numeriske koden formatet definerer: 0 for nederst til høyre, 1 for øverst til høyre, 2 for nederst til venstre og 3 for øverst til venstre. Den offentlige enumereringen TXLSPanePosition er deklarert i leseretning, xlspTopLeft, xlspTopRight, xlspBottomLeft, xlspBottomRight, så Ord(xlspTopLeft) er 0, som er panen nederst til høyre i filen. Å caste enumen rett inn i pane-byten ville skrevet hvert utvalg øverst til venstre inn på panen nederst til høyre uten noen feil. Hvert pane-bevisste HotXLS-inngangspunkt konverterer enumen gjennom en eksplisit case-setning i stedet, så koden som kaller opp, trenger aldri å forholde seg til de numeriske kodene. Pane-eksistens sjekkes også: panen øverst til høyre finnes bare med vertikal splitting, den nederst til venstre bare med horisontal, og den nederst til høyre bare med begge. For et pane dagens split- eller frysegeometri ikke har, returnerer SelectAreas False, og GetSelectedAreas returnerer en tom matrise med ActiveAreaIndex satt til -1, uten å lage noe pane, noe utvalgsobjekt eller noen celle i arbeidsboken

Hvordan HotXLS mapper TXLSPanePosition over på BIFF8 Selection pane-byte: enumen er deklarert i leseretning, så Ord(xlspTopLeft) er 0, mens filen definerer 0 for nederst til høyre, 1 for øverst til høyre, 2 for nederst til venstre og 3 for øverst til venstre, så hvert pane-bevisste inngangspunkt konverterer gjennom en eksplisit case-setning
Å caste enumen rett inn i pane-byten ville skrevet hvert utvalg øverst til venstre inn på panen nederst til høyre, så HotXLS sjekker også pane-eksistens mot dagens split- eller frysegeometri før skriving

Hvor ligger scrollposisjonen til hver pane?

Scrollposisjonen til hver pane er delt over to records, fordi fire paner deler bare to radposisjoner og to kolonneposisjoner. I en klassisk arbeidsbok er de øvre panenes første synlige rad og de venstre panenes første synlige kolonne Window2.rwTop og Window2.colLeft, mens de nedre panenes rad og de høyre panenes kolonne er Pane.rwTop og Pane.colLeft. ScrollWindow(xlspTopRight, R, C) skriver derfor Window2.rwTop og Pane.colLeft, og å sette kolonnen til panen øverst til høyre flytter også panen nederst til høyre, akkurat som de to deler én horisontal rullefelt i Excel. De offentlige metodene bruker rad- og kolonnenumre fra 1. Et manglende pane returnerer False og setter begge spørringsutgangene til null, og en koordinat utenfor rekkevidde avvises før noen av aksene endres. Ingenting her avhenger av hvordan en fremviser tegner rutenettet. En renderingskontroll holder sin egen TopRow og LeftCol, som artikkelen om å rendre arbeidsbøker i et egendefinert VCL-rutenett beskriver, og det er runtime-tilstand, ikke det som lagres

Hvor hver HotXLS pane-scrollakse bor: fire paner deler to rad- og to kolonneposisjoner, så øvre rad og venstre kolonne er Window2.rwTop og Window2.colLeft, mens nedre rad og høyre kolonne er Pane.rwTop og Pane.colLeft, og ScrollWindow(xlspTopRight, 1, 6) skriver ett Window2-felt pluss ett Pane-felt, slik at nederst til høyre følger med
XLSX sprer de samme dataene over sheetView- og pane topLeftCell-attributtene, og å slå de to lagene sammen til ett er nøyaktig hvordan en øvre eller venstre scrollposisjon forsvinner stille ved innlesing

XLSX sprer de samme dataene over to elementer: sheetView/@topLeftCell (ECMA-376 Part 1, §18.3.1.87) for vinduet som helhet og barnet pane/@topLeftCell (§18.3.1.66) for den nedre høyre siden av en splitting. Begge attributtene kan være til stede samtidig. HotXLS leser det ytre attributtet først inn i vindusnivåfeltene, lar pane-barnet overstyra bare pane-nivåfeltene, og skriver begge tilbake separat. Å slå de to lagene sammen til ett er nøyaktig hvordan en øvre eller venstre scrollposisjon forsvinner stille ved innlesing. Regnearkkopier bærer begge lagene i begge motorer. De eldre inngangspunktene beholder den opprinnelige oppførselen sin: de klassiske egenskapene ScrollRow og ScrollColumn, og de nullbaserte XLSX-metodene SetPaneScroll og GetPaneScroll. Frys- og splitgeometrien i seg selv konfigureres med arknivåinnstillingene som dekkes i arkbeskyttelse, sideoppsett og utskrift

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

  // Nederst til høyre: nedre radakse (Pane.rwTop) og høyre kolonneakse (Pane.colLeft)
  Sheet.ScrollWindow(xlspBottomRight, 500, 3);

  // Øverst til høyre deler høyre kolonneakse, så dette flytter også nederst til høyre til kolonne 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]));
    // Nederst til høyre starter på rad 500, kolonne 6
end;

Hva skjer når en Selection-record er korrupt?

Når en Selection-record er korrupt, beholder HotXLS den som opake byte, rapporterer diagnostikkoden 1304 (xlsDiagnosticSelectionRecordInvalid) og skriver den opprinnelige kroppen tilbake byte for byte ved lagring. Før en record blir med i panens gruppe, sjekker leseren den i rekkefølge. Pane-byten må være 3 eller mindre. Records for ett pane må være sammenhengende i strømmen. De 9 faste bytene må være til stede. cref må være mellom 1 og 1369, og kroppen må være nøyaktig 9 + cref × 6 byte lang. Hver chunk i en gruppe må være enig om aktiv celle og irefAct, irefAct må ikke ha fortegnsbiten satt, den aktive kolonnen må være på rutenettet, og ingen områder kan ha omvendte grenser. Problemer i én enkelt fysisk record rapporteres én gang per record. Motsigelser som bare dukker opp etter aggregering, som irefAct som peker forbi totalområdetallet eller en aktiv celle utenfor det indekserte området, rapporteres én gang per gruppe ved EOF. En ugyldig gruppe forblir usynlig for det typede API-et: GetSelectedAreas returnerer en tom matrise med indeks -1 for den panen, mens alle andre paner fortsetter å virke

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;

Hvordan overlever utvalg rad- og kolonneinnsettinger?

Utvalg overlever strukturelle redigeringer fordi innsetting eller sletting av hele rader eller kolonner mapper alle representerte panegrupper om i både Classic- og XLSX-motoren gjennom én delt remapper. Overlevende områder beholder rekkefølgen sin, og det aktive området beholder identiteten sin. Blir det aktive området slettet, blir den første overlevende etterfølgeren aktiv, så den siste overlevende forgjengeren hvis ingenting følger etter det. Blir alle områder slettet, kollapser gruppen til én celle ved slettingsgrensen, og en aktiv celle som ikke lenger ligger inne i det valgte området, flytter til det områdets øvre venstre hjørne, så indeksen og koordinaten motsier aldri hverandre. Grensene er bevisste. Ugyldige Classic-grupper hoppes over av remapperen i stedet for å skrives om til et oppdiktet utvalg, så de opprinnelige bytene deres tar fortsatt tur-retur. Å redigere ett pane erstatter bare records for den panen og lar de andre være byte-identiske. ODS får ingen pane-utvalgstilstand i det hele tatt, fordi ODF ikke har noen tilsvarende regnearkvisningsstruktur å bære den

Skriver applikasjonen din .xls-filer som brukere åpner og må navigere i, enten for å gå gjennom markerte celler, fortsette der de slapp eller dele et frossent dashbord, er det pane-bevisste utvalgs- og scroll-API-et en del av HotXLS Delphi spreadsheet-komponenten, og det virker på samme måte for XLS og XLSX