Technischer Artikel

XLSX-Pivot-Felder schema-gültig mit HotXLS in Delphi

HotXLS schreibt XLSX-PivotTable-Definitionen, deren pivotField- und cacheField-Elemente gegen das ECMA-376 Part 1 §18.10-Schema validieren: Achsen-Attribute nutzen die ST_Axis-Tokens axisRow, axisCol und axisPage, Felder im Wertebereich tragen dataField="1", Item-Listen sind nie leer, und Cache-Felder speichern eine numerische numFmtId. Seit v2.384.33 respektiert der Reader auch die Schema-Defaults, die er früher falsch annahm

Die Bugs hinter dieser Säuberung teilen ein wenig schmeichelhaftes Merkmal: Keiner hat je einen Test scheitern lassen. HotXLS schrieb einen Pivot, HotXLS las ihn zurück, jedes Feld landete auf der richtigen Achse, und die Roundtrip-Suite blieb jahrelang grün. Das Problem war, dass Writer und Reader stillschweigend einen privaten Dialekt vereinbart hatten. Ein aus Delphi gebauter Pivot sah für die Komponente, die ihn erzeugte, bestens aus, während ein Abgleich gegen CT_PivotField und CT_CacheField ungültige Enumerations-Tokens zutage förderte, ein leeres Element, das das Schema verbietet, und Flags, die Excel erwartet, aber nie bekam. Wer Pivots auf einem Server generiert und an Leute ausliefert, die sie in Excel öffnen oder in eigene Parser füttern, für den zählt nur der eine Vertrag: das Schema, nicht das, was der eigene Reader zufällig verzeiht

Warum haben HotXLS-Roundtrips die falschen Achsen-Tokens nie bemerkt?

HotXLS-Roundtrips haben die falschen Achsen-Tokens nie bemerkt, weil der Reader beide Schreibweisen akzeptierte. Das alte XlsxPivotAxisAttr emittierte axis="rowAxis", colAxis und pageAxis – liest sich im Englischen natürlich, existiert aber nicht im Schema; ST_Axis definiert exakt vier Werte, axisRow, axisCol, axisPage und axisValues. Derweil matchte PivotAxisFromToken in lxPivotXml.pas sowohl das Schema-Token als auch das erfundene, also bestand jeder Selbsttest. Der Writer emittiert jetzt nur die Schema-Tokens, und der Reader akzeptiert die alten Schreibweisen weiter, damit von früheren HotXLS-Versionen gespeicherte Dateien mit intaktem Layout weiterladen

<!-- vor v2.384.33: ungültiger ST_Axis-Wert, leeres CT_Items -->
<pivotField axis="rowAxis" defaultSubtotal="1"><items count="0"></items></pivotField>

<!-- seit v2.384.33 -->
<pivotField axis="axisRow" defaultSubtotal="1">
  <items count="4"><item x="0"/><item x="1"/><item x="2" h="1"/><item t="default"/></items>
</pivotField>
HotXLS-pivotField-XML vor und nach v2.384.33, wo der erfundene Achsenwert rowAxis und ein leeres items-Element CT_PivotField verletzen, bis der Writer ST_Axis-Tokens wie axisRow mit echten Item-Einträgen emittiert, einem erhaltenen Hidden-Flag und einem abschließenden Default-Subtotal, die das Schema akzeptiert
Der nachsichtige Reader akzeptierte beide Schreibweisen, also bestand jeder Roundtrip, während die Datei jede strenge Schema-Prüfung zerbrach – schreiben Sie nur die vier ST_Axis-Tokens und lassen Sie CT_Items mindestens ein Item tragen

Was verlangt CT_PivotField, das der alte Writer übersprang?

CT_PivotField verlangt drei Dinge, die das alte BuildPivotTableXml ausließ oder falsch machte. Erstens muss ein im Wertebereich aggregiertes Feld das auf seiner eigenen Definition mit dataField="1" sagen; der Writer setzt dieses Flag jetzt auf jedem Feld, das ein Eintrag in DataFields referenziert, nicht nur in der <dataFields>-Liste. Zweitens braucht CT_Items mindestens ein item, also bekommt ein Feld ohne Items kein leeres <items count="0"> mehr, und das ganze Element bleibt schlicht weg. Drittens behält jedes Item seinen Zustand: h="1" für ein verstecktes Item (TXLSPivotItem.IsHidden) und sd="0" für eingeklappte Details (IsDetailHidden), beides, was der alte Writer bei jedem Speichern fallen ließ

Der subtile Teil sind die abschließenden Subtotal-Items. Hat ein Feld Items, listet Excel nach den Daten-Items je Subtotal-Funktion ein zusätzliches item auf, typisiert mit ST_ItemType: <item t="default"/> für das automatische Subtotal, dann sum, countA, avg, max, min, product, count, stdDev, stdDevP, var und varP für explizite. HotXLS leitet diese Einträge beim Speichern aus TXLSPivotField.Subtotals ab und zählt sie in items count hinein. Felder aus AddPivotTable starten mit einer leeren Subtotals-Menge, was defaultSubtotal="0" und kein abschließendes Item schreibt – fordern Sie Subtotals also explizit an, wenn der Bericht sie braucht. Beachten Sie die Namensfalle: xlpsCount mappt auf countA (alle Einträge) und xlpsCountNums auf count (nur Zahlen)

HotXLS-Anatomie der Pivot-Items-Liste, wo auf die Daten-Item-Einträge abschließende Subtotal-Items folgen, die aus TXLSPivotField.Subtotals abgeleitet werden, etwa t=default und t=avg, und in den items count eingerechnet werden, mit der ausgeschriebenen Namensfalle xlpsCount zu countA und xlpsCountNums zu count
Felder aus AddPivotTable starten mit leerer Subtotals-Menge, was defaultSubtotal=0 und kein abschließendes Item schreibt – fordern Sie die gewünschten Funktionen an, und der Writer leitet je Funktion ein Item in den Count ab
uses
  lxHandleX, lxPivot;

var
  Book  : TXLSXWorkbook;
  Sheet : TXLSXWorksheet;
  Pivot : TXLSPivotTable;
  Region: TXLSPivotField;
begin
  Book := TXLSXWorkbook.Create;
  try
    Book.Open('orders.xlsx');
    Sheet := Book.Sheets[1];                  // 1-basiert, wie die XLS-Engine
    Pivot := Sheet.AddPivotTable('Data!$A$1:$D$800', 3, 6, 'RegionTotals');
    if Pivot = nil then
      raise Exception.Create('Bad source range or anchor');

    Region := Pivot.AddRowField('Region');    // nil, wenn es kein solches Feld gibt
    if Region <> nil then
      Region.Subtotals := [xlpsDefault, xlpsAverage];  // -> t="default", t="avg"
    Pivot.AddColumnField('Quarter');
    Pivot.AddDataFieldByName('Revenue', xlpaSum);      // markiert Revenue mit dataField="1"

    Book.SaveAs('orders-pivot.xlsx');
  finally
    Book.Free;
  end;
end;

Wie liest HotXLS jetzt Subtotal-Items und Schema-Defaults?

Der HotXLS-Reader überspringt jetzt jedes item, dessen t-Attribut vorhanden und nicht data ist, denn Subtotal-, Grand-Total- und Leer-Einträge tragen keinen Cache-Index. Vor v2.384.34 wurden diese Einträge als ordentliche Items mit CacheItemIndex -1 geladen, sodass ein aus Excel stammender Pivot mit Phantomen zurückkam, die ins Nichts zeigten, und jeder Code, der Items abläuft, sie von Hand ausfiltern musste. Da der Writer die abschließenden Einträge aus Subtotals neu aufbaut, ist die Aufgabe des Readers, sie in diese Menge zu übersetzen, nicht, sie als Daten zu behalten

Der zweite Reader-Fix betrifft Attribute, die fehlen. Im Schema defaulten defaultSubtotal auf CT_PivotField und containsString auf CT_SharedItems beide zu true, und Excel lässt sie weg, wenn sie diesen Default halten. HotXLS las ein fehlendes Attribut früher als false, was bedeutete, dass jeder von Excel gespeicherte Pivot beim Laden stillschweigend sein Default-Subtotal verlor und ein schlichtes Text-Cache-Feld als gemischt statt als String klassifiziert wurde. Das ist das Spiegelbild des Achsen-Bugs: Ein Writer, der jedes Attribut immer ausschreibt, läuft nie über den Default-Pfad – nur Dateien eines anderen Producers legen ihn offen

Warum war numFmtId="General" auf Cache-Feldern ungültig?

Der Wert numFmtId="General" war ungültig, weil ST_NumFmtId eine vorzeichenlose Ganzzahl ist, kein Formatname. Der alte Cache-Writer hart-kodierte diesen String auf jedem cacheField, entlehnt aus dem Namen, den Nutzer im Dialog Zellen formatieren sehen. HotXLS schreibt das NumberFormat des Cache-Felds jetzt als Zahl, also 0 (das eingebaute General-Format), sofern nichts anderes gesetzt wurde. Ein strenger Parser, der Attribute nach dem Schema typisiert, weist den alten Wert rundweg zurück – und genau diese Fehlerklasse wird zum Reparatur-Dialog; der Artikel über die OPC- und Markup-Regeln hinter dem Excel-Repair-Prompt beschreibt, wie diese Dialoge ausgelöst werden

Warum wurden Pivot-Tables ab Zeile 65535 abgeschnitten?

XLSX-Pivot-Tables ab Zeile 65536 wurden abgeschnitten, weil das gemeinsame Pivot-Modell FirstRow, LastRow, FirstHeaderRow, FirstDataRow und die Spalten-Pendants als Word speicherte und der Zeilenverschiebe-Code sie mit Min(.., High(Word)) klemmte. Das ist ein Überbleibsel des BIFF8-SxView-Records, wo 16 Bits reichen, aber ein XLSX-Sheet läuft bis Zeile 1.048.576. Seit v2.384.37 sind diese Properties auf TXLSPivotTable Integer, die Klemmen sind weg, und nur der BIFF8-Writer macht die Werte schmaler. TXLSXWorksheet.AddPivotTable und AddPivotTableCopy liefern jetzt nil für einen Anker außerhalb 1..1048576 mal 1..16384 oder für eine Kopie, deren Ausdehnung übers Raster hinausliefe

HotXLS-Pivot-Anker in Zeile 70001 gegen die 16-Bit-Grenze, wo FirstRow und LastRow als Word gespeichert und mit Min gegen High(Word) bei 65535 geklemmt wurden, wodurch Pivots ab der Linie abgeschnitten wurden, bis v2.384.37 das Modell auf Integer-Felder mit nil-Rückgabe außerhalb des Rasters umstellte
Die Word-Felder waren ein BIFF8-SxView-Überbleibsel in einem Format, dessen Sheets bis Zeile 1048576 laufen – ein Anker jenseits von Zeile 65536 schlug früher in den 16-Bit-Bereich um und verlor seinen Pivot beim Speichern
var
  Pivot: TXLSPivotTable;
  Check: TXLSXWorkbook;
begin
  // Zeile 70001 schlug früher in den 16-Bit-Bereich um; jetzt übersteht sie Speichern und Laden
  Pivot := Sheet.AddPivotTable('Data!$A$1:$D$800', 70001, 1, 'LateTotals');
  if Pivot = nil then
    Exit;  // Anker außerhalb des Sheets oder unauffindbarer Quellbereich
  Pivot.AddRowField('Region');
  Pivot.AddDataFieldByName('Revenue', xlpaSum);
  Book.SaveAs('late.xlsx');

  Check := TXLSXWorkbook.Create;
  try
    Check.Open('late.xlsx');
    Pivot := Check.Sheets[1].PivotTables.FindByName('LateTotals');
    Assert((Pivot <> nil) and (Pivot.FirstRow = 70001));
  finally
    Check.Free;
  end;
end;

Die klassische XLS-Engine bekam den zugehörigen Fix in v2.384.38. Ihr Modell speicherte früher die rohen nullbasierten SxView- und DConRef-Werte und reichte AddPivotTable-Anker unverändert durch, während die Dokumentation, die Demos und die XLSX-Engine durchweg 1-basierte Zellen wie Cells[Row, Col] nutzten. Beide Engines halten jetzt 1-basierte Positionen im Modell, der BIFF8-Reader addiert 1 und der Writer subtrahiert 1 an der Record-Grenze, also muss Code, der bei (0, 0) verankerte, auf (1, 1) umziehen, denn das klassische AddPivotTable liefert jetzt nil für einen Anker außerhalb 1..65536 mal 1..256; der neue Aufruf schreibt dieselben Bytes wie der alte. Das Record-Layout selbst ist unverändert und wird in den BIFF8-SX-Records hinter klassischen .xls-Pivot-Tables beschrieben

Am Schema validieren, nicht am eigenen Reader

Die Lehre verallgemeinert sich über Pivots hinaus: Ein nachsichtiger Reader versteckt Writer-Verstöße, also beweist ein Roundtrip durch den eigenen Code Konsistenz, nicht Korrektheit. Jeder Bug hier überlebte, weil die tolerante und die fehlerhafte Seite in derselben Bibliothek wohnten. Die Checks, die diese Fehlerklasse wirklich fangen, sind eine Schema-Validierung der erzeugten Teile, von Excel erzeugte Dateien, die mit an ihren Defaults weggelassenen Attributen durch den eigenen Reader laufen, und Fixtures, die das exakte Token pinnen statt des geparsten Ergebnisses. Pivots, die Sie über die API bauen, einschließlich der in Bau und Aktualisierung von XLSX-Pivot-Tables mit berechneten Feldern gezeigten berechneten Felder, berechneten Items und Prozent-von-Gesamt-Layouts, bekommen das korrigierte XML ohne Codeänderung, während aus Excel-Dateien geladene Pivots ihre ursprünglichen Teile weiter abspielen, bis Sie sie anfassen

Alle diese Fixes stecken im aktuellen HotXLS Delphi spreadsheet component, das XLS, XLSX und Pivot-Tables aus Delphi und C++Builder liest und schreibt, ohne Excel oder COM-Automation auf der Maschine