HotXLS écrit des définitions de tableaux croisés dynamiques XLSX dont les éléments pivotField et cacheField valident contre le schéma ECMA-376 Partie 1 §18.10 : les attributs d'axe utilisent les jetons ST_Axis axisRow, axisCol et axisPage, les champs de la zone de valeurs portent dataField="1", les listes d'items ne sont jamais vides, et les champs de cache stockent un numFmtId numérique. Depuis la v2.384.33, le lecteur honore aussi les défauts du schéma qu'il lisait mal auparavant
Les bogues derrière ce nettoyage partagent un trait peu flatteur : aucun n'a jamais fait échouer un test. HotXLS écrivait un tableau croisé, HotXLS le relisait, chaque champ atterrissait sur le bon axe, et la suite d'allers-retours est restée verte pendant des années. Le problème, c'est que le générateur et le lecteur s'étaient tranquillement mis d'accord sur un dialecte privé. Un tableau croisé construit depuis Delphi avait l'air bon pour le composant qui l'avait fabriqué, tandis qu'une vérification contre CT_PivotField et CT_CacheField révélait des jetons d'énumération invalides, un élément vide que le schéma interdit et des drapeaux qu'Excel attendait mais ne recevait jamais. Si vous générez des tableaux croisés sur un serveur et les expédiez à des gens qui les ouvrent dans Excel ou les donnent à leurs propres analyseurs, le seul contrat qui compte, c'est le schéma, pas ce que votre propre lecteur arrive à pardonner
Pourquoi les allers-retours HotXLS n'ont-ils jamais attrapé les mauvais jetons d'axe ?
Les allers-retours HotXLS n'ont jamais attrapé les mauvais jetons d'axe parce que le lecteur acceptait les deux graphies. L'ancien XlsxPivotAxisAttr émettait axis="rowAxis", colAxis et pageAxis, qui se lisent naturellement en anglais mais n'existent pas dans le schéma ; ST_Axis définit exactement quatre valeurs, axisRow, axisCol, axisPage et axisValues. Pendant ce temps, PivotAxisFromToken dans lxPivotXml.pas reconnaissait à la fois le jeton du schéma et celui inventé, si bien que chaque autotest passait. Le générateur n'émet désormais que les jetons du schéma, et le lecteur continue d'accepter les anciennes graphies pour que les fichiers sauvegardés par des versions antérieures de HotXLS se chargent toujours avec leur mise en page intacte
<!-- avant la v2.384.33 : valeur ST_Axis invalide, CT_Items vide -->
<pivotField axis="rowAxis" defaultSubtotal="1"><items count="0"></items></pivotField>
<!-- depuis la 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>
Qu'exige CT_PivotField que l'ancien générateur sautait ?
CT_PivotField exige trois choses que l'ancien BuildPivotTableXml omettait ou ratait. Un, un champ agrégé dans la zone de valeurs doit le dire sur sa propre définition avec dataField="1" ; le générateur pose désormais ce drapeau sur chaque champ référencé par une entrée de DataFields, et pas seulement dans la liste <dataFields>. Deux, CT_Items a besoin d'au moins un item, si bien qu'un champ sans item ne reçoit plus de <items count="0"> vide et que tout l'élément est simplement omis. Trois, chaque item garde son état : h="1" pour un item caché (TXLSPivotItem.IsHidden) et sd="0" pour les détails repliés (IsDetailHidden), deux choses que l'ancien générateur lâchait à chaque sauvegarde
La partie subtile, ce sont les items de sous-total en fin de liste. Quand un champ a des items, Excel liste un item supplémentaire par fonction de sous-total après les items de données, typés avec ST_ItemType : <item t="default"/> pour le sous-total automatique, puis sum, countA, avg, max, min, product, count, stdDev, stdDevP, var et varP pour les explicites. HotXLS dérive ces entrées de TXLSPivotField.Subtotals à la sauvegarde et les compte dans items count. Les champs créés par AddPivotTable démarrent avec un ensemble Subtotals vide, ce qui écrit defaultSubtotal="0" et aucun item final, donc demandez explicitement les sous-totaux quand le rapport en a besoin. Notez le piège de nommage : xlpsCount correspond à countA (toutes les entrées) et xlpsCountNums à count (nombres seulement)
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]; // en base 1, comme le moteur XLS
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 si aucun champ de ce nom
if Region <> nil then
Region.Subtotals := [xlpsDefault, xlpsAverage]; // -> t="default", t="avg"
Pivot.AddColumnField('Quarter');
Pivot.AddDataFieldByName('Revenue', xlpaSum); // marque Revenue dataField="1"
Book.SaveAs('orders-pivot.xlsx');
finally
Book.Free;
end;
end;
Comment HotXLS lit-il désormais les items de sous-total et les défauts du schéma ?
Le lecteur HotXLS saute désormais tout item dont l'attribut t est présent et différent de data, parce que les entrées de sous-total, de total général et de vide ne portent aucun index de cache. Avant la v2.384.34, ces entrées étaient chargées comme des items ordinaires avec CacheItemIndex à -1, si bien qu'un tableau croisé fabriqué par Excel revenait avec des membres fantômes qui ne pointaient nulle part, et tout code qui parcourait Items devait les filtrer à la main. Comme le générateur reconstruit les entrées finales depuis Subtotals, le travail du lecteur est de les traduire dans cet ensemble, pas de les garder comme données
La seconde correction du lecteur concerne les attributs absents. Dans le schéma, defaultSubtotal sur CT_PivotField et containsString sur CT_SharedItems valent tous deux true par défaut, et Excel les omet quand ils portent cette valeur. HotXLS lisait un attribut manquant comme false, ce qui faisait que chaque tableau croisé sauvegardé par Excel perdait en silence son sous-total par défaut au chargement, et qu'un champ de cache texte pur était classé mixte au lieu de chaîne. C'est l'image en miroir du bogue d'axe : un générateur qui épelle toujours chaque attribut n'exerce jamais le chemin des défauts, donc seuls les fichiers d'un autre producteur l'exposent
Pourquoi numFmtId="General" était-il invalide sur les champs de cache ?
La valeur numFmtId="General" était invalide parce que ST_NumFmtId est un entier non signé, pas un nom de format. L'ancien générateur de cache codait cette chaîne en dur sur chaque cacheField, empruntant le nom que les utilisateurs voient dans la boîte de dialogue Format de cellule. HotXLS écrit désormais le NumberFormat du champ de cache comme un nombre, qui vaut 0 (le format General intégré) sauf si quelque chose l'a fixé. Un analyseur strict qui type les attributs depuis le schéma rejette carrément l'ancienne valeur, et c'est exactement la classe de défaillance qui devient une boîte de réparation ; l'article sur les règles OPC et de balisage derrière l'invitation de réparation d'Excel couvre comment ces boîtes se déclenchent
Pourquoi les tableaux croisés à partir de la ligne 65535 étaient-ils coupés ?
Les tableaux croisés XLSX placés à la ligne 65536 ou au-delà étaient coupés parce que le modèle de pivot partagé stockait FirstRow, LastRow, FirstHeaderRow, FirstDataRow et leurs équivalents en colonnes comme des Word, et que le code de décalage de lignes les bornait avec Min(.., High(Word)). C'est un vestige de l'enregistrement BIFF8 SxView, où 16 bits suffisent, mais une feuille XLSX court jusqu'à 1 048 576 lignes. Depuis la v2.384.37, ces propriétés de TXLSPivotTable sont des Integer, les bornes ont disparu, et seul le générateur BIFF8 resserre les valeurs. TXLSXWorksheet.AddPivotTable et AddPivotTableCopy renvoient désormais nil pour une ancre hors de 1..1048576 par 1..16384, ou pour une copie dont l'étendue sortirait de la grille
var
Pivot: TXLSPivotTable;
Check: TXLSXWorkbook;
begin
// La ligne 70001 bouclait dans la plage 16 bits ; elle survit désormais à la sauvegarde et au chargement
Pivot := Sheet.AddPivotTable('Data!$A$1:$D$800', 70001, 1, 'LateTotals');
if Pivot = nil then
Exit; // ancre hors feuille ou plage source irrésoluble
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;
Le moteur XLS classique a reçu la correction correspondante en v2.384.38. Son modèle stockait les valeurs brutes SxView et DConRef en base 0 et laissait passer les ancres AddPivotTable telles quelles, tandis que la documentation, les démos et le moteur XLSX utilisaient tous des cellules en base 1 comme Cells[Row, Col]. Les deux moteurs gardent désormais des positions en base 1 dans le modèle, le lecteur BIFF8 ajoute 1 et le générateur retranche 1 à la frontière d'enregistrement, si bien que du code qui ancrerait en (0, 0) doit passer en (1, 1), parce que le AddPivotTable classique renvoie désormais nil pour une ancre hors de 1..65536 par 1..256 ; le nouvel appel écrit les mêmes octets que l'ancien. La disposition des enregistrements elle-même est inchangée et est décrite dans les enregistrements SX BIFF8 derrière les tableaux croisés .xls classiques
Validez contre le schéma, pas contre votre propre lecteur
La leçon se généralise au-delà des pivots : un lecteur tolérant cache les violations du générateur, donc un aller-retour par votre propre code prouve la cohérence, pas la correction. Chaque bogue ici a survécu parce que le côté tolérant et le côté fautif vivaient dans la même bibliothèque. Les vérifications qui attrapent vraiment cette classe de défaut sont une validation de schéma des parties générées, des fichiers produits par Excel passés dans votre lecteur avec les attributs omis à leurs défauts, et des fixtures qui épinglent le jeton exact plutôt que le résultat analysé. Les pivots que vous construisez via l'API, y compris les champs calculés, les items calculés et les dispositions pourcentage du total montrés dans construire et rafraîchir des tableaux croisés dynamiques XLSX avec champs calculés, reçoivent le XML corrigé sans changement de code, tandis que les pivots chargés depuis des fichiers Excel continuent de rejouer leurs parties d'origine jusqu'à ce que vous les modifiiez
Toutes ces corrections arrivent dans le composant tableur HotXLS pour Delphi actuel, qui lit et écrit XLS, XLSX et des tableaux croisés dynamiques depuis Delphi et C++Builder sans Excel ni automatisation COM sur la machine