Műszaki cikk

HotXLS: BIFF8 Selection rekordok és pane-görgetés Delphiben

A HotXLS a munkalap-kijelöléseket és a görgetési pozíciókat pane-enként tárolja egyetlen, panetudatos API-n keresztül, a TXLSWorksheet és a TXLSXWorksheet oldalán egyaránt: SelectAreas, GetSelectedAreas, ScrollWindow és TryGetWindowScroll. Klasszikus .xls fájloknál a HotXLS legfeljebb 1369 területet hordozó BIFF8 Selection rekordokat (0x001D) ír, a logikai pane-neveket a formátum által definiált pane-bájtokra képezi le, és minden görgetési tengelyt ott tart a Window2 vagy Pane rekordon, ahol az Excel várja

A probléma jellemzően egy egyeztető vagy auditáló eszközben bukik fel. Az eszköz megnyit egy főkönyv-exportot, megkeres minden olyan cellát, amely ellentmond a forrásrendszernek, és a munkafüzetet úgy menti el, hogy ezek a cellák már ki vannak jelölve egy rögzített fejlécsor alatt, így az ellenőr egyből a különbségeken találja magát, nem pedig görgetés utánuk. Negyven különbségnél ez szépen működik. A hóvégi fájlban 3000 van, és egyetlen Selection rekord sem létezhet 3000 területtel: a törzse 18 009 bájt lenne, több mint kétszerese annak, amennyit egy BIFF8 rekord elbír. A görgetési pozíciónál is ott a csapda. Rögzített pane-ekkel rendelkező munkalapon az „ott, ahol a felhasználó nézett" négy pane, amely két sorpozíciót és két oszloppozíciót oszt meg, nem pedig egyetlen koordináta

Miért van egy nagy kijelölésnek több Selection rekordra szüksége?

A nagy kijelölésnek több rekordra van szüksége, mert egy BIFF8 rekord törzse 8224 bájtnál tetőzik, és minden kijelölt terület fix hat bájtot emészt fel. A [MS-XLS] §2.4.248 egy 9 bájtos rögzített résszel írja le a Selection rekordot (a pane-bájt, a rwAct és a colAct az aktív cellához, az irefAct az aktív területhez, a cref pedig a területszámhoz), amelyet cref darab RefU struktúra követ, mindegyik két 16 bites sort és két 8 bites oszlopot hordozva. A beférő legnagyobb darabszám a lefelé kerekített (8224 − 9) / 6, ami 1369, és ez 8223 bájtos törzset ad, egy bájttal a határ alatt. A TXLSWorksheet.StoreSelectionGroup ezt a konstansot használja MaxAreasPerRecord-ként, és egy nagyobb csoportot ugyanannak a pane-nek szóló, egymást követő Selection rekordokként ír ki, egyszerre 1369 területtel

Az a részlet, amely beleharap, az irefAct. Minden darab ugyanazt az aktív sort, aktív oszlopot és aktív területindexet ismétli, az irefAct pedig valamennyi darab összefésült sorozatát indexeli, nem az éppen hordozó rekordjában lévő területeket. A határ fölé egy területtel érkező kijelölés konkréttá teszi ezt: 1370 terület, az utolsót aktívvá téve, két rekordot eredményez, az első cref 1369-tyal, a második cref 1-gyel, és mindkettő irefAct 1369-et hordoz. Ez az érték nagyobb, mint a második rekord saját területdarabszáma. Az az olvasó, amely rekordonként veti össze az irefAct-et a cref-fel, elutasít egy érvényes fájlt, az pedig, amely minden rekordnál lecseréli az állapotát, elejti az első 1369 területet. A HotXLS olvasója az egymást követő, azonos pane-es rekordokat egyetlen csoportba fűzi, megköveteli, hogy minden darab egyetértsen az aktív cellában és az indexben, a tartományellenőrzést pedig csak a munkalap EOF rekordjánál futtatja, ha a teljes sorozat már ismert. A pane-első SelectAreas túlterhelésnek ezért nincs 1369 területes plafonja. Minden A1-hivatkozást és az aktív indexet érvényesíti, még mielőtt megszerezné a munkalap írási zárolását, és ha bármi hibásan formázott, False-szal tér vissza, a korábbi kijelölés változatlanul

Miért írja a HotXLS egy nagy munkalap-kijelölést több BIFF8 Selection rekordként: a 8224 bájtos törzshatár 9 rögzített bájtot meg 1369 hatbájtos RefU területet enged, így 3000 terület három azonos pane-es rekorddá áll össze, 1369, 1369 és 262 darabbal, az irefAct pedig az összefésült sorozatot indexeli, ezért 1370 terület, az utolsót aktívvá téve, mindkét rekordnak irefAct 1369-et ad
Minden darab ugyanazt az aktív cellát és indexet ismétli, a HotXLS olvasója az egymást követő azonos pane-es rekordokat egy csoportba fűzi, a tartományellenőrzés pedig csak az EOF rekordnál fut, ha a teljes sorozat már ismert
var
  Book: TXLSWorkbook;
  Sheet: TXLSWorksheet;
  Diffs: TXLSSelectedAreas;
  I: Integer;
begin
  Book := TXLSWorkbook.Create;
  try
    Sheet := Book.Sheets.Add;
    Sheet.FreezePanes(1, 1);           // a fejlécsor és az A oszlop a helyén marad

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

    // A rögzítés alaphelyzetbe állítja a tárolt kijelölést, ezért utána jelöljön.
    // 3000 terület három Selection rekordként mentődik: 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;

Melyik pane-bájtot használ egy Selection rekord?

A Selection rekord a formátum által definiált numerikus kóddal azonosítja a pane-jét: 0 a jobb alsó, 1 a jobb felső, 2 a bal alsó, 3 pedig a bal felső számára. A nyilvános TXLSPanePosition enumeráció olvasási sorrendben van deklarálva, xlspTopLeft, xlspTopRight, xlspBottomLeft, xlspBottomRight, így az Ord(xlspTopLeft) 0, ami a fájlban a jobb alsó pane. Az enumeráció panebájtba öntése minden bal felső kijelölést hibajelzés nélkül a jobb alsó pane-re írna. Minden panetudatos HotXLS belépési pont ezért explicit case utasításon keresztül konvertálja az enumerációt, így a hívó soha nem találkozik a numerikus kódokkal. A pane létezése is ellenőrzés alá esik: a jobb felső pane csak függőleges osztással létezik, a bal alsó csak vízszintessel, a jobb alsó pedig mindkettővel. Olyan pane-nél, amelyet az aktuális osztási vagy rögzítési geometria nem tartalmaz, a SelectAreas False-szal tér vissza, a GetSelectedAreas pedig üres tömböt ad, az ActiveAreaIndex -1-en, pane, kijelölési objektum vagy cella létrehozása nélkül a munkafüzetben

Hogyan képezi le a HotXLS a TXLSPanePosition-t a BIFF8 Selection pane-bájtjára: az enumeráció olvasási sorrendben van deklarálva, így az Ord(xlspTopLeft) 0, a fájl pedig 0-t definiál a jobb alsó, 1-et a jobb felső, 2-t a bal alsó és 3-at a bal felső számára, ezért minden panetudatos belépési pont explicit case utasításon keresztül konvertál
Az enumeráció panebájtba való egyenes öntése minden bal felső kijelölést a jobb alsó pane-re írna, ezért a HotXLS írás előtt a pane létezését is az aktuális osztási vagy rögzítési geometriához méri

Hol lakik minden pane görgetési pozíciója?

Minden pane görgetési pozíciója két rekordra oszlik, mert négy pane mindössze két sorpozíciót és két oszloppozíciót oszt meg. Klasszikus munkafüzetben a felső pane-ek első látható sora és a bal oldali pane-ek első látható oszlopa a Window2.rwTop és a Window2.colLeft, az alsó pane-ek sora és a jobb oldali pane-ek oszlopa pedig a Pane.rwTop és a Pane.colLeft. A ScrollWindow(xlspTopRight, R, C) ezért a Window2.rwTop-ot és a Pane.colLeft-et írja, és a jobb felső pane oszlopának beállítása a jobb alsó pane-t is magával mozgatja, éppúgy, ahogy a kettő az Excelben közös vízszintes görgetősávot használ. A nyilvános metódusok 1-alapú sorszámot és oszlopszámot használnak. Hiányzó pane False-ot ad, és mindkét lekérdezési kimenetet nullára állítja, a tartományon kívüli koordinátát pedig még bármelyik tengely változása előtt elutasítja a hívás. Semmi sem függ attól, hogyan fest egy megjelenítő a rácsot. Egy renderelő vezérlő a saját TopRow-ját és LeftCol-ját tartja, ahogy az a munkafüzetek egyéni VCL rácsban történő megjelenítéséről szóló cikk is írja, ezek pedig futásidejű állapotok, nem az, ami mentésre kerül

Hol lakik a HotXLS pane-jeinek egyes görgetési tengelye: négy pane két sor- és két oszloppozíciót oszt meg, így a felső sor és a bal oszlop a Window2.rwTop és a Window2.colLeft, az alsó sor és a jobb oszlop pedig a Pane.rwTop és a Pane.colLeft, a ScrollWindow(xlspTopRight, 1, 6) pedig egy Window2 mezőt és egy Pane mezőt ír, így a jobb alsó követi
Az XLSX ugyanezeket az adatokat a sheetView és a pane topLeftCell attribútumai közt szórja szét, és a két réteg egybeolvasztása pontosan az, ahogy egy felső vagy bal görgetési pozíció betöltéskor csendben eltűnik

Az XLSX ugyanezeket az adatokat két elemre osztja szét: sheetView/@topLeftCell (ECMA-376 Part 1, §18.3.1.87) az egész ablakhoz, és a gyerek pane/@topLeftCell (§18.3.1.66) egy osztás jobb alsó oldalához. Mindkét attribútum ott lehet egyszerre. A HotXLS először a külső attribútumot olvassa be az ablakszintű mezőkbe, a pane gyerek csak a pane-szintű mezőket bírálhatja felül, és mindkettőt külön-külön írja vissza. A két réteg egybeolvasztása pontosan az, ahogy egy felső vagy bal görgetési pozíció betöltéskor csendben eltűnik. A munkalapmásolatok mindkét réteget hordozzák mindkét motorban. A régebbi belépési pontok meg tartják az eredeti viselkedésüket: a klasszikus ScrollRow és ScrollColumn tulajdonságok, valamint a nullaalapú XLSX SetPaneScroll és GetPaneScroll. Maga a rögzítési és osztási geometria a munkalapszintű beállításokkal konfigurálható, amelyeket a munkalapvédelemről, lapbeállításról és nyomtatásról szóló cikk fed le

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

  // Jobb alsó: alsó sortengely (Pane.rwTop) és jobb oszloptengely (Pane.colLeft)
  Sheet.ScrollWindow(xlspBottomRight, 500, 3);

  // A jobb felső megosztja a jobb oszloptengelyt, ezért ez a jobb alsót is a 6. oszlopra viszi
  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]));
    // A jobb alsó az 500. sorban és a 6. oszlopban indul
end;

Mi történik, ha egy Selection rekord sérült?

Ha egy Selection rekord sérült, a HotXLS átlátszatlan bájtokként tartja meg, a 1304-es diagnosztikai kódot (xlsDiagnosticSelectionRecordInvalid) jelenti, és mentéskor bájtról bájtra visszaírja az eredeti törzset. Mielőtt egy rekord csatlakozna a pane-je csoportjához, az olvasó sorban ellenőrzi. A pane-bájtnak 3 vagy annál kisebbnek kell lennie. Egy pane rekordjainak folytonosnak kell lenniük a streamben. A 9 rögzített bájtnak jelen kell lennie. A cref-nek 1 és 1369 közé kell esnie, a törzsnek pedig pontosan 9 + cref × 6 bájt hosszúnak kell lennie. Minden darabnak egyet kell érteni csoporton belül az aktív cellában és az irefAct-ben, az irefAct előjelbitje nem állhat, az aktív oszlopnak a rácson kell lennie, és egyetlen terület sem lehet fordított határokkal. Az egyetlen fizikai rekordon belüli problémák rekordonként egyszer jelentenek. Azok az ellentmondások, amelyek csak az összesítés után bukkannak fel, például a teljes területdarabszámot túllépő irefAct vagy az indexelt területen kívüli aktív cella, csoportonként egyszer jelentenek az EOF-nál. Egy érvénytelen csoport láthatatlan marad a típusos API előtt: a GetSelectedAreas üres tömböt ad, -1 indexszel arra a pane-re, miközben minden más pane tovább dolgozik

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;

Hogyan élik túl a kijelölések a sor- és oszlopbeszúrásokat?

A kijelölések túlélik a szerkezeti szerkesztéseket, mert teljes sorok vagy oszlopok beszúrása és törlése mindkét motorban, a klasszikusban és az XLSX-ben is, egyetlen közös áthelyezőn keresztül képezi át minden reprezentált pane-csoportot. A túlélő területek megtartják sorrendjüket, az aktív terület pedig az identitását. Ha az aktív terület törlődik, az első túlélő utód lesz aktív, ha pedig utód nincs, az utolsó túlélő előd. Ha minden terület törlődik, a csoport a törlés határán egyetlen cellára összezsugorodik, és az aktív cella, amely nem esik többé a választott területbe, annak a területnek a bal felső sarkába ugrik, így az index és a koordináta soha nem mondanak ellent egymásnak. A határok szándékosak. Az érvénytelen klasszikus csoportokat az áthelyező kihagyja, nem írja át kitalált kijelöléssé, így az eredeti bájtaik továbbra is körbejárnak. Egy pane szerkesztése csak az adott pane rekordjait cseréli, a többit bájtonként azonosan hagyja. Az ODS pane-kijelölési állapotot egyáltalán nem kap, mert az ODF-nek nincs megfelelő munkalapnézet-struktúrája, amely hordozhatná

Ha az Ön alkalmazása olyan .xls fájlokat ír, amelyeket a felhasználók megnyitnak, és amelyekben navigálniuk kell, legyen szó megjelölt cellák átnézéséről, arról, hogy ott folytassák, ahol abbahagyták, vagy egy rögzített dashboard megosztásáról, a panetudatos kijelölési és görgetési API a HotXLS Delphi táblázatkezelő komponens része, és XLS-nél és XLSX-nél egyaránt ugyanúgy működik