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>
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)
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
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