Teknisk artikel

Tabsfri XLSX-rundtur i Delphi: Tema, extLst, calcChain

HotXLS, det indfødte Excel-bibliotek til Delphi og C++Builder, er bygget til tabsfrie XLSX-rundture: Åbn en arbejdsbog, rediger én celle, gem, og kundens brugerdefinerede tema, eksterne extLst-udvidelsesblokke og beregningskæde (calculation chain) overlever alle. Tre mekanismer får det til at fungere — ordret cachelagring af xl/theme/theme1.xml, hændelsesbaseret re-serialisering af ukendte <ext>-blokke, og en ny, specifikationsgyldig xl/calcChain.xml ved hver lagring af en formel-arbejdsbog

Det scenarie, der motiverer alle tre, er deprimerende almindeligt. En faktureringstjeneste indlæser en skabelon, som kunden har designet i Excel — virksomhedens farvetema, sparklines i en KPI-kolonne, en betinget formateringsregel tilføjet af en nyere Excel-version — skriver et enkelt fakturabeløb i celle B3 og gemmer. Kunden åbner resultatet, og brand-farverne er vendt tilbage til standard Office-blå, sparklines er væk, og Excel tilbyder at "reparere" filen. Intet i koden rørte ved nogen af disse funktioner. Det gjorde biblioteket blot ved at gemme

Hvorfor mister Excel-filer formatering efter redigering med et bibliotek?

Excel-filer mister formateringen efter redigering med et bibliotek, fordi de fleste biblioteker ikke redigerer filen — de genopbygger den. En .xlsx-pakke is en ZIP-fil bestående af XML-dele: xl/workbook.xml, én xl/worksheets/sheetN.xml pr. ark, xl/styles.xml, xl/theme/theme1.xml, xl/calcChain.xml osv. Et typisk bibliotek fortolker disse dele til en objektmodel ved åbning og genererer hver enkelt del igen ud fra denne model ved lagring. Enhver funktion, som modellen ikke repræsenterer — et tema den aldrig har fortolket, en udvidelsesblok fra en nyere Excel — har intet sted at opholde sig i hukommelsen, så den regenererede del udelader den stiltiende

ECMA-376 forudså halvdelen af dette problem. SpreadsheetML definerer extLst (ECMA-376 del 1, "Future Feature Data Storage Area", §18.2.10 for elementet på arbejdsbogsniveau) som et udpeget udvidelsespunkt: Nyere producenter placerer funktioner der, hver især pakket ind i et <ext>-element med en uri-attribut, der identificerer funktionen, og ældre forbrugere forventes at bevare det, de ikke forstår. Sparklines, udsnitsværktøjer (slicers) og nyere betingede formateringstyper overføres alle på denne måde. Et bibliotek, der kasserer ukendte <ext>-blokke, er derfor ikke blot behæftet med tab — det overtræder den kontrakt om fremadrettet kompatibilitet, som formatet blev designet omkring. Spørgsmålet til ethvert regnearksbibliotek, du evaluerer, er kontant: Hvis jeg ændrer én celle, hvad ændrer sig så ellers

Hvordan bevarer HotXLS et brugerdefineret tema byte-for-byte?

HotXLS bevarer en arbejdsbogs tema ved at cache de oprindelige bytes fra xl/theme/theme1.xml ved åbningstidspunktet og skrive dem uforandret tilbage ved lagringstidspunktet. Temadelen (ECMA-376 del 1, §14.2.7) er DrawingML, not SpreadsheetML — farveskemaer, skrifttypeskemaer, formatskemaer — og en regnearksmotor har ingen grund til at modellere den i dybden. Tidligere versioner af HotXLS genererede et fast Office-tema ved hver lagring, hvilket er præcis fejlen med at "brand-farverne vendte tilbage til standarden" nævnt ovenfor; siden v2.89.46 gemmes det åbnede pakketema i råt format og udsendes uberørt, og det indbyggede Office-tema genereres kun for arbejdsbøger, der oprettes helt fra bunden. Rå bytes er den stærkest mulige garanti for nøjagtighed: Ingen fortolkning, ingen re-serialisering, ingen risiko for afvigelser

Den ordrette kopi vinder bevidst over programmatisk temadgang. TXLSXWorkbook eksponerer ThemeMajorFont og ThemeMinorFont, så du kan vælge overskrifts- og brødtekstskrifttyper til nye arbejdsbøger, men når et ordret tema blev opsamlet ved åbning, har disse settere ingen effekt på den gemte fil — rundturen har prioritet. Hvis du reelt har brug for at ændre et eksisterende arbejdsbogstema, er det et signal om at redigere skabelonen i selve Excel frem for via et datamålrettet API. Det daglige brugsscenarie kræver slet intet API:

var
  Book: TXLSXWorkbook;
begin
  Book := TXLSXWorkbook.Create;
  try
    Book.Open('branded-invoice.xlsx');
    Book.Sheets[0].Cells[3, 2].Value := 42750.00;  // the one edit
    Book.SaveAs('branded-invoice-out.xlsx');
    // theme1.xml in the output is byte-identical to the input
  finally
    Book.Free;
  end;
end;

Hvad sker der med ukendte extLst-blokke ved lagring?

HotXLS opsamler enhver <ext>-blok på regnearksniveau, som den ikke modellerer indfødt, og afspiller den igen i det gemte regnearks extLst, så funktioner skrevet af nyere Excel-versioner overlever rundturen intakt. Siden v2.131.0 er de opsamlede fragmenter synlige via den skrivebeskyttede egenskab RawWorksheetExts, som er en TStringList på hvert XLSX-regneark, hvilket gør garantien auditerbar fra testkode frem for at være baseret på blind tillid:

var
  Book: TXLSXWorkbook;
  Sheet: TXLSXWorksheet;
  i: Integer;
begin
  Book := TXLSXWorkbook.Create;
  try
    Book.Open('from-newer-excel.xlsx');
    Sheet := Book.Sheets[0];
    WriteLn(Format('%d foreign ext block(s) captured',
      [Sheet.RawWorksheetExts.Count]));
    for i := 0 to Sheet.RawWorksheetExts.Count - 1 do
      WriteLn(Copy(Sheet.RawWorksheetExts[i], 1, 100)); // peek at each uri
  finally
    Book.Free;
  end;
end;

En implementeringsdetalje, der er værd at kende, er, at opsamlingen er en re-serialisering på hændelsesniveau, ikke en rå byte-kopi. HotXLS' streaming XML-læser eksponerer ingen kilde-offsets, så det ukendte undertræ genopbygges ud fra Element-, Text- og EndElement-hændelser, efterhånden som de strømmer forbi. Denne tilgang gemmer på en klassisk fælde: Et selvlukkende element som f.eks. <a/> udløser kun en Element-hændelse markeret som tom og aldrig en EndElement-hændelse, så en dybdetæller, der udelukkende tæller ned ved EndElement, vil aldrig opdage, at undertræet lukker. Håndteres dette, er det genopbyggede fragment semantisk ækvivalent med originalen — attribut-anførselstegn og selvlukkende former normaliseres, så det er ikke byte-identisk, men Excel læser indhold, ikke bytes. To egenskaber ved Excels eget output gør genafspilningen sikker: Excel erklærer de nødvendige xmlns-attributter på <ext>-elementet eller inde i det, så hvert opsamlet fragment er navnerum-selvstændigt (namespace-self-contained), og denne selvstændighed er netop grunden til, at duplikering af et regneark i eller på tværs af arbejdsbøger kan medføre de eksterne blokke ved hjælp af en simpel tildeling af en strengliste

Skrivning af calcChain.xml så Excel stoler på dine formler

HotXLS skriver xl/calcChain.xml (beregningskædedelen, ECMA-376 del 1, §12.3.1), hver gang den gemte arbejdsbog indeholder formler, og den vælger mellem to sorteringsmetoder. Hvis formelafhængighedsgrafen allerede er blevet opbygget og er aktuel — dvs. at du kaldte Recalculate efter din seneste redigering — udsendes kæden i fuld topologisk rækkefølge, afhængigheder før afhængige celler, med eventuelle cirkulære referencer tilføjet til sidst. Ellers listes cellerne i dokumentrækkefølge. Begge dele er korrekte: Microsofts implementeringsnotater til formatet, [MS-XLSX], behandler beregningskæden som et tip, som Excel verificerer og sorterer under indlæsningen, så enhver komplet liste er gyldig, og HotXLS nægter bevidst at fremtvinge en grafopbygning inde i SaveAs — kantopbygning er kvadratisk i forhold til celletallet, hvilket ville være en uacceptabel skjult omkostning ved lagring af en million celler

Book.Open('model.xlsx');
Book.Sheets[0].Cells[10, 4].Formula := '=SUM(D2:D9)';
// Saved now, calcChain.xml lists formula cells in document order.
// After Recalculate the dependency graph exists, so the same save
// emits a full topological order instead:
Book.Recalculate;
Book.SaveAs('model-out.xlsx');

Hvor den tabsfrie rundtur slutter

Ærlighed betyder mere end et kryds i en marketingboks her, så grænserne fortjener lige så meget fokus. HotXLS kopierer ikke hele pakken byte-for-byte: Regnearks-XML, typografier, delte strenge og arbejdsbogdele regenereres ud fra den fortolkede model, så outputtet er semantisk tro mod originalen, men ikke binært identisk — alene de lokale ZIP-headere indeholder nye DOS-tidsstempler. Opsamlede <ext>-fragmenter returneres normaliseret, som beskrevet ovenfor. Programmatiske temaskrifttype-tilsidesættelser ignoreres, når et ordret tema er til stede. Og bevaringsnetværket har en defineret maskevidde: Funktioner, som HotXLS modellerer indfødt (f.eks. sparklines, der fortolkes og omskrives frem for blindt at blive kopieret) plus eksternt extLst-indhold samt de dele, der cachelagres ordret. En del, der hverken er modelleret eller befinder sig i et udvidelsespunkt — f.eks. et eksotisk tilføjelsesprograms (add-in) brugerdefinerede del — falder uden for de tre mekanismer, som denne artikel dækker, så test dine egne skabeloner frem for at antage noget

Nærtliggende bevaringsarbejde fuldender billedet. VBA-projekter og eksterne arbejdsbogsreferencer bevares under lagring ud fra den samme filosofi om at beholde det, man ikke modellerer, hvilket beskrives i ledsagerartiklen om bevaring af VBA og eksterne links, og dokumentegenskaber i docProps har deres eget læse-skrive-API frem for at blive slettet stiltiende. Når du evaluerer et regnearksbibliotek, så kør testen med en enkelt celle: Åbn en funktionsrig produktionsarbejdsbog, ændr en enkelt værdi, gem, og sammenlign de udzippede dele med originalen. Hvad der har ændret sig ud over det ark, du rørte ved, fortæller dig mere om biblioteket end nogen matrix over funktioner

De rundtursmekanismer, der er beskrevet her — ordret temabevaring siden v2.89.46, opsamling af eksternt extLst og udsendelse af calcChain.xml siden v2.131.0 — leveres i den aktuelle HotXLS Delphi Excel Component, hvis produktside dokumenterer det fulde XLSX-læse-skrive-funktionssæt til Delphi og C++Builder