Teknisk artikel

HotXLS BIFF8 Selection-records og pane-scroll i Delphi

HotXLS gemmer regnearksmarkeringer og scroll-positioner pr. pane gennem én pane-aware API på både TXLSWorksheet og TXLSXWorksheet: SelectAreas, GetSelectedAreas, ScrollWindow og TryGetWindowScroll. For klassiske .xls-filer skriver HotXLS BIFF8 Selection-records (0x001D) med højst 1369 områder hver, konverterer logiske pane-navne til de pane-bytes, formatet definerer, og holder hver scroll-akse på den Window2- eller Pane-record, hvor Excel forventer den

Problemet viser sig som regel i et afstemnings- eller revisionsværktøj. Værktøjet åbner en hovedbogseksport, finder hver celle, der er i uoverensstemmelse med kildesystemet, og gemmer arbejdsbogen med de celler allerede markeret under en frossen headerrække, så revisoren lander på forskellene i stedet for at scrolle efter dem. Med fyrre forskelle fungerer det fint. Månedslutningsfilen har 3.000, og en enkelt Selection-record med 3.000 områder kan ikke eksistere: dens krop ville kræve 18.009 bytes, mere end det dobbelte af, hvad én BIFF8-record kan bære. Scroll-position har en lignende fælde. På et ark med frosne ruder er "hvor brugeren kiggede" fire panes, der deler to rækkepositioner og to kolonnepositioner, ikke én koordinat

Hvorfor kræver en stor markering mere end én Selection-record?

En stor markering kræver flere records, fordi en BIFF8-recordkrop er begrænset til 8224 bytes, og hvert markeret område koster seks faste bytes. [MS-XLS] §2.4.248 beskriver Selection-recorden som en 9-byte fast del (pane-byten, rwAct og colAct til den aktive celle, irefAct til det aktive område og cref til områdetallet) efterfulgt af cref RefU-strukturer, der hver bærer to 16-bit rækker og to 8-bit kolonner. Det største tal, der kan være der, er (8224 − 9) / 6 afrundet ned, altså 1369, og det giver en krop på 8223 bytes, én byte under grænsen. TXLSWorksheet.StoreSelectionGroup bruger den konstant som MaxAreasPerRecord og skriver en større gruppe som på hinanden følgende Selection-records for samme pane, 1369 områder ad gangen

Detaljen, der bider, er irefAct. Hver klump gentager samme aktive række, aktive kolonne og aktive områdeindeks, og irefAct indekserer den aggregerede sekvens af alle klumper, ikke områderne inde i den record, der bærer den. En markering ét område over grænsen gør det konkret: 1370 områder med det sidste aktivt bliver til to records, den første med cref 1369 og den anden med cref 1, og begge bærer irefAct 1369. Den værdi er større end den anden records eget områdetal. En læser, der tjekker irefAct mod cref i hver record, afviser en gyldig fil, og en læser, der erstatter sin tilstand ved hver record, mister de første 1369 områder. HotXLS-læseren føjer på hinanden følgende same-pane-records til én gruppe, kræver, at hver klump er enig om den aktive celle og indekset, og kører range-tjekket først ved regnearkets EOF-record, når hele sekvensen er kendt. Pane-first SelectAreas-overloaden har derfor intet loft på 1369 områder. Den validerer hver A1-reference og det aktive indeks, før den tager regnearkets skrivelås, og den returnerer False med den forrige markering uændret, hvis noget er misdannet

Hvorfor HotXLS skriver en stor regnearksmarkering som flere BIFF8 Selection-records: kropegrænsen på 8.224 bytes giver plads til 9 faste bytes plus 1369 RefU-områder på seks bytes, så 3.000 områder bliver til tre same-pane-records på 1369, 1369 og 262, og irefAct indekserer den aggregerede sekvens, så 1370 områder med den sidste aktiv giver begge records irefAct 1369
Hver klump gentager samme aktive celle og indeks, HotXLS-læseren føjer på hinanden følgende same-pane-records til én gruppe, og range-tjekket kører først ved EOF-recorden, når hele sekvensen er kendt
var
  Book: TXLSWorkbook;
  Sheet: TXLSWorksheet;
  Diffs: TXLSSelectedAreas;
  I: Integer;
begin
  Book := TXLSWorkbook.Create;
  try
    Sheet := Book.Sheets.Add;
    Sheet.FreezePanes(1, 1);           // headerrækken og kolonne A bliver på plads

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

    // Frysning nulstiller den gemte markering, så markér efter frysning.
    // 3000 områder gemmes 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 bruger en Selection-record?

En Selection-record identificerer sin pane med den numeriske kode, formatet definerer: 0 for nederst til højre, 1 for øverst til højre, 2 for nederst til venstre og 3 for øverst til venstre. Den offentlige enum TXLSPanePosition er erklæret i læserækkefølge, xlspTopLeft, xlspTopRight, xlspBottomLeft, xlspBottomRight, så Ord(xlspTopLeft) er 0, hvilket i filen er pane nederst til højre. Castet direkte ind i pane-byten ville enummen skrive hver markering øverst til venstre på pane nederst til højre uden nogen fejl. Ethvert pane-aware HotXLS-indgangspunkt konverterer enummen gennem en eksplicit case-sætning i stedet, så kaldere aldrig arbejder med de numeriske koder. Der tjekkes også, om pane overhovedet findes: pane øverst til højre findes kun med en lodret deling, nederst til venstre kun med en vandret, og nederst til højre kun med begge. For en pane, som den aktuelle delings eller frysgeometri ikke har, returnerer SelectAreas False, og GetSelectedAreas returnerer et tomt array med ActiveAreaIndex sat til -1, uden at oprette en pane, et selection-objekt eller en celle i arbejdsbogen

Hvordan HotXLS mapper TXLSPanePosition på BIFF8 Selection pane-byten: enummen er erklæret i læserækkefølge, så Ord(xlspTopLeft) er 0, mens filen definerer 0 for nederst til højre, 1 for øverst til højre, 2 for nederst til venstre og 3 for øverst til venstre, så ethvert pane-aware indgangspunkt konverterer gennem en eksplicit case-sætning
Et direkte cast af enummen ind i pane-byten ville skrive hver markering øverst til venstre på pane nederst til højre, så HotXLS tjekker også pane-eksistens mod den aktuelle delings eller frysgeometri, før der skrives

Hvor bor hver panes scroll-position?

Scroll-positionen for hver pane er delt over to records, fordi fire panes kun deler to rækkepositioner og to kolonnepositioner. I en klassisk arbejdsbog er de øverste panes første synlige række og de venstre panes første synlige kolonne Window2.rwTop og Window2.colLeft, mens de nederste panes række og de højre panes kolonne er Pane.rwTop og Pane.colLeft. ScrollWindow(xlspTopRight, R, C) skriver derfor Window2.rwTop og Pane.colLeft, og sætter man kolonnen for pane øverst til højre, flytter pane nederst til højre sig med, ligesom de to deler én vandret scrollbar i Excel. De offentlige metoder bruger 1-baserede række- og kolonnenumre. En manglende pane returnerer False og sætter begge query-output til nul, og en koordinat uden for området afvises, før nogen akse ændres. Intet her afhænger af, hvordan en viewer tegner gitteret. En rendering-kontrol holder sit eget TopRow og LeftCol, som artiklen om rendering af arbejdsbøger i et custom VCL-grid beskriver, og det er runtime-tilstand, ikke det, der gemmes

Hvor hver HotXLS pane-scroll-akse bor: fire panes deler to række- og to kolonnepositioner, så den øverste række og venstre kolonne er Window2.rwTop og Window2.colLeft, mens den nederste række og højre kolonne er Pane.rwTop og Pane.colLeft, og ScrollWindow(xlspTopRight, 1, 6) skriver ét Window2-felt plus ét Pane-felt, så nederst til højre følger med
XLSX spreder samme data over sheetView- og pane topLeftCell-attributterne, og at smelte de to lag sammen til ét er præcis, hvordan en øvre eller venstre scroll-position forsvinder lydløst ved indlæsning

XLSX spreder samme data over to elementer: sheetView/@topLeftCell (ECMA-376 Part 1, §18.3.1.87) for vinduet som helhed og det underordnede pane/@topLeftCell (§18.3.1.66) for delingens nederste højre side. Begge attributter kan være til stede på samme tid. HotXLS læser den ydre attribut først ind i vinduesniveau-felterne, lader pane-barnet overskrive kun pane-niveau-felterne og skriver begge tilbage hver for sig. At smelte de to lag sammen til ét er præcis, hvordan en øvre eller venstre scroll-position forsvinder lydløst ved indlæsning. Regnearkskopier bærer begge lag i begge motorer. De ældre indgangspunkter beholder deres oprindelige adfærd: de klassiske ScrollRow- og ScrollColumn-egenskaber og de nulbaserede XLSX SetPaneScroll og GetPaneScroll. Frys- og delingsgeometrien i sig selv konfigureres med indstillingerne på arkniveau, der er dækket i arkbeskyttelse, sideopsætning og udskrivning

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

  // Nederst til højre: nederste rækkeakse (Pane.rwTop) og højre kolonneakse (Pane.colLeft)
  Sheet.ScrollWindow(xlspBottomRight, 500, 3);

  // Øverst til højre deler højre kolonneakse, så dette flytter også nederst til højre 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øjre starter på række 500, kolonne 6
end;

Hvad sker der, når en Selection-record er korrupt?

Når en Selection-record er korrupt, beholder HotXLS den som opake bytes, rapporterer diagnostikkode 1304 (xlsDiagnosticSelectionRecordInvalid) og skriver den originale krop byte for byte tilbage ved gemning. Inden en record joiner sin panes gruppe, tjekker læseren den i rækkefølge. Pane-byten skal være 3 eller mindre. Records for én pane skal være sammenhængende i streamen. De 9 faste bytes skal være der. cref skal være mellem 1 og 1369, og kroppen skal være præcis 9 + cref × 6 bytes lang. Hver klump i en gruppe skal være enig om den aktive celle og irefAct, irefAct må ikke have sin fortegnsbit sat, den aktive kolonne skal være på gitteret, og intet område må have byttede grænser. Problemer i en enkelt fysisk record rapporteres én gang pr. record. Modsigelser, der først viser sig efter aggregering, som irefAct peger forbi det samlede områdetal eller en aktiv celle uden for det indekserede område, rapporteres én gang pr. gruppe ved EOF. En ugyldig gruppe forbliver usynlig for det typede API: GetSelectedAreas returnerer et tomt array med indeks -1 for den pane, mens alle andre panes fortsætter med at 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 markeringer række- og kolonneindsættelser?

Markeringer overlever strukturelle redigeringer, fordi indsættelse eller sletning af hele rækker eller kolonner remapper hver repræsenteret pane-gruppe i både Classic- og XLSX-motoren gennem én delt remapper. Overlevende områder beholder deres rækkefølge, og det aktive område beholder sin identitet. Slettes det aktive område, bliver den første overlevende efterfølger aktiv, derefter den sidste overlevende forgænger, hvis intet følger den. Slettes alle områder, kollapser gruppen til én celle ved slettegrænsen, og en aktiv celle, der ikke længere ligger inde i det valgte område, flytter til områdets øverste venstre hjørne, så indeks og koordinat aldrig modsiges hinanden. Grænserne er bevidste. Ugyldige klassiske grupper bliver sprunget over af remapperen i stedet for at blive omskrevet til en opdigtet markering, så deres originale bytes stadig round-tripper. Redigerer man én pane, udskiftes kun den panes records, og de andre forbliver byte-identiske. ODS får slet ingen pane-selection-tilstand, fordi ODF ikke har nogen ækvivalent regnearksvisningsstruktur at bære den

Skriver din applikation .xls-filer, som brugere åbner og skal navigere i, om det så er for at gennemgå markerede celler, fortsætte der, hvor de stoppede, eller dele et frosset dashboard, er pane-aware selection- og scroll-API'en en del af HotXLS Delphi spreadsheet component, og den virker på samme måde for XLS og XLSX