Article technique

Éditer des graphiques Excel HotXLS sans perdre le ChartML

Une partie de graphique enregistrée par une version d'Excel récente s'arrête rarement aux éléments que le schéma de graphique ECMA-376 définit. L'espace du graphique s'ouvre en général sur un bloc mc:AlternateContent qui offre c14:style aux lecteurs récents et un c:style simple aux anciens, chaque série porte un c:extLst avec un c16:uniqueId, et les étiquettes de données « Value From Cells » vivent dans encore une autre extension. Un programme qui n'analyse que ce qu'il modélise et régénère la partie au moment de la sauvegarde ne réécrit rien de tout cela. Changez un mot dans le titre, rouvrez le fichier dans Excel, et les étiquettes qui tiraient leur texte d'une plage de cellules ont disparu

Le correctif consiste à cesser de traiter une sauvegarde comme une reconstruction. Gardez les octets avec lesquels le graphique est arrivé, réécrivez-les inchangés quand rien de modélisé n'a bougé, et quand quelque chose a bougé, fusionnez les nouveaux éléments dans l'arbre original pour que les parties inconnues restent là où Excel les a mises

Rejouer le ChartML original quand rien n'a changé

HotXLS fait exactement cela quand TXLSXWorkbook.PreserveUnsupportedParts vaut True, ce qui est le défaut sur un classeur fraîchement créé. À l'import, chaque graphique garde ses octets UTF-8 originaux avec la longueur et une empreinte 64 bits du contenu que le modèle typé représente. À la sauvegarde, HotXLS régénère la partie modélisée, compare longueur et empreinte, et si les deux correspondent il réécrit les octets stockés verbatim : pas de réordonnancement d'éléments, pas de dérive d'espacement, et pas de seconde copie UTF-16 de chaque graphique intact en mémoire

Comme la comparaison porte sur le contenu, affecter à une propriété la valeur qu'elle détient déjà ne change rien. Les setters de TXLSXChart sortent tôt sur une valeur égale, donc une boucle qui « rafraîchit » chaque titre de graphique depuis une ligne de base de données rejoue quand même chaque graphique dont le titre était déjà correct. La mise en forme de ligne des barres d'erreur et des courbes de tendance est suivie par son propre drapeau de changement, et la modifier compte toujours comme une édition

Diagramme de la préservation ChartML de HotXLS en Delphi où les graphiques inchangés rejouent les octets d'origine et les graphiques modifiés fusionnent des nœuds de modèle typés dans le XML préservé, tandis qu'une reconstruction depuis le modèle seul perd les branches extLst et AlternateContent
HotXLS stocke les octets ChartML d'origine à l'import et les rejoue intacts, ou fusionne des modifications typées dans l'arbre préservé quand le modèle a changé

TXLSXChart.HasPreservedXml vous dit sur quel chemin se trouve un graphique avant que vous éditiez quoi que ce soit. Il rapporte True quand un ChartML original est attaché au graphique, ce qui est la précondition du rejeu comme de la fusion

var
  Workbook: TXLSXWorkbook;
  Chart: TXLSXChart;
begin
  Workbook := TXLSXWorkbook.Create;
  try
    // True par défaut ; positionnez-le explicitement pour que l'intention survive au refactoring.
    // Il doit être en place avant Open, parce qu'Open construit l'état
    Workbook.PreserveUnsupportedParts := True;
    Workbook.Open('quarterly-review.xlsx');

    Chart := Workbook.Sheets[1].Charts[0];   // Sheets[] est en base 1, Charts[] en base 0
    if not Chart.HasPreservedXml then
      Log('chart 0 has no original ChartML attached');   // Log est votre propre routine

    Chart.Title := Chart.Title;                  // valeur égale : rejoue quand même
    Workbook.Sheets[1].Charts[1].Title :=
      'Revenue by region, Q3 2026';              // ce graphique prend le chemin de fusion
    Workbook.SaveAs('quarterly-review-out.xlsx');
  finally
    Workbook.Free;
  end;
end;

Que survit à une fusion quand vous éditez un graphique ?

La réponse courte : tout ce que le modèle typé ne possède pas. Titre, séries, caches de données, groupes de traçage et axes appartiennent au modèle, donc le nouvel arbre généré les décide. Les attributs non modélisés, les déclarations d'espaces de noms, les styles, les listes d'extensions et les nœuds de mise en forme viennent de l'arbre original et sont réinsérés à côté des nœuds correspondants auprès desquels ils se trouvaient

Supprimez une série et la fusion ne doit pas la ressusciter depuis le XML préservé juste parce que l'original contient encore un c:ser à cette position. Tout ce que le modèle possède est pris depuis le modèle, même quand la réponse du modèle est « disparu ». Cela vaut aussi au niveau des attributs : quand vous effacez gras, italique, souligné, barré ou la taille de police d'un style de texte connu, la fusion lit l'attribut manquant comme un effacement et ne recopie pas b ou sz depuis le defRPr original. Les attributs inconnus sur le même élément gardent leur place

Les titres suivent la même règle. À l'import, HotXLS garde les runs riches natifs de c:title, avec leurs polices, tailles et balises de langue par run, et les rejoue tant que le texte est intact. Affectez un nouveau Title et ce contenu riche est abandonné ; le titre est réécrit comme un seul run stylé depuis TitleTextStyle. Fusionner d'anciens runs dans un nouveau texte reviendrait à deviner à quel run appartiennent les nouveaux mots, et une devinette ici produit des titres que personne n'a tapés

L'ordre du schéma est préservé là où Office est pointilleux. Un extLst reste le dernier enfant de son parent, comme le schéma l'exige. Les blocs de compatibilité de balisage reçoivent leur propre traitement : le lecteur saute mc:Choice et lit mc:Fallback, et quand ce fallback enveloppe un groupe de traçage que le modèle connaît, la fusion réécrit le nouveau contenu typé dans le sous-arbre de fallback. Une fusion naïve émettrait le groupe de traçage édité et laisserait l'ancienne copie dans la branche de fallback, si bien que le graphique enregistré porterait le même groupe deux fois

Sortir un graphique du dispositif avec ClearPreservedXml

TXLSXChart.ClearPreservedXml abandonne le XML original et l'empreinte d'un graphique, et la prochaine sauvegarde reconstruit ce graphique purement depuis le modèle typé. C'est une décision par graphique. PreserveUnsupportedParts gouverne toujours si l'état de préservation est construit à l'import, et il couvre aussi d'autres parties de package non prises en charge, pas seulement les graphiques, ce qui fait de son extinction pour obtenir un graphique propre le mauvais levier

Effacez un graphique quand l'abandon est le propos de l'opération, par exemple un graphique de modèle qui porte encore la mise en forme d'un thème d'entreprise retiré. Laissez-le tranquille comme réflexe de débogage. Un graphique qui se rend mal après une édition est en général une question de modélisation, et effacer le XML préservé jette la seule copie de ce qu'Excel a écrit à l'origine

Un cas demande une vérification d'abord. Si une barre d'erreur ou une courbe de tendance utilise une mise en forme de ligne que le modèle typé ne peut pas représenter, sa propriété LineAppearanceSupported vaut False ; une étiquette de courbe de tendance avec une mise en forme hors modèle le rapporte pareil via LabelAppearanceSupported. HotXLS ne remplacera silencieusement ni l'une ni l'autre par une version plus simple : dès que le graphique n'a plus de XML préservé en secours, la sauvegarde s'arrête avec une EArgumentException nommant l'apparence de ligne ou d'étiquette non représentée. Tester les deux avant d'effacer prend une boucle

// Votre propre helper, pas une partie de HotXLS : True quand chaque ligne de barre d'erreur,
// de courbe de tendance et d'étiquette de courbe de tendance peut être écrit depuis le modèle typé seul
function ChartRebuildable(Chart: TXLSXChart): Boolean;
var
  I, T: Integer;
  S: TXLSXChartSeries;
begin
  Result := False;
  for I := 0 to Chart.SeriesCount - 1 do    // couvre tous les groupes de traçage
  begin
    S := Chart.Series[I];
    if (S.ErrBarX <> nil) and not S.ErrBarX.LineAppearanceSupported then Exit;
    if (S.ErrBarY <> nil) and not S.ErrBarY.LineAppearanceSupported then Exit;
    for T := 0 to S.TrendlineCount - 1 do
    begin
      if not S.Trendlines[T].LineAppearanceSupported then Exit;
      if not S.Trendlines[T].LabelAppearanceSupported then Exit;
    end;
  end;
  Result := True;
end;

// ...
Chart := Workbook.Sheets[1].Charts.FindByName('Old theme chart');
if (Chart <> nil) and ChartRebuildable(Chart) then
  Chart.ClearPreservedXml      // la prochaine sauvegarde reconstruit ce graphique depuis le modèle
else
  Log('kept original ChartML for this chart');

Graphiques combinés avec AddPlotGroup et un axe secondaire

TXLSXChart.AddPlotGroup ajoute un groupe de traçage ordonné avec sa propre famille de graphique et sa propre assignation d'axe primaire ou secondaire. Cela couvre le graphique que chaque équipe finance demande : le chiffre d'affaires en colonnes sur l'axe de gauche, le pourcentage de marge en ligne sur celui de droite. Chaque graphique expose ValueAxis, CategoryAxis, SecondaryValueAxis, SecondaryCategoryAxis et SeriesAxis, avec des réglages typés pour position, identifiants d'axe, croisement, unités de date, intervalles d'étiquettes, format de nombre et visibilité

L'API plate des graphiques à type unique est toujours là et se projette sur les groupes. ChartType et AddSeries adressent le premier groupe de traçage ; SeriesCount et Series[] parcourent les séries de chaque groupe dans l'ordre, et c'est pourquoi le helper ci-dessus voit aussi la série en ligne. L'ordre compte à un endroit. TXLSXCharts.Add crée un graphique sans aucun groupe de traçage, et celui de AddSeries ou AddPlotGroup qui s'exécute en premier crée le groupe un. Appelez AddPlotGroup(xlsxChartLine, xlsxAxisSecondary) d'abord sur un graphique frais et la ligne devient le groupe un, le type du graphique bascule en ligne, et la série de chiffre d'affaires que vous ajoutez ensuite atterrit dedans. Créer les deux groupes explicitement lève l'ambiguïté, et c'est ce que fait l'exemple

var
  Chart: TXLSXChart;
  Columns, Line: TXLSXChartPlotGroup;
begin
  // Ancré sur les lignes 1..15, colonnes 5..12 ; aucun groupe de traçage n'existe encore
  Chart := Sheet.Charts.Add(xlsxChartColumn, 'Revenue and margin',
    1, 5, 15, 12);
  Columns := Chart.AddPlotGroup(xlsxChartColumn);              // groupe 1, primaire
  Columns.AddSeries('Revenue', 'Data!$A$2:$A$13', 'Data!$B$2:$B$13');

  Line := Chart.AddPlotGroup(xlsxChartLine, xlsxAxisSecondary); // groupe 2
  Line.AddSeries('Margin %', 'Data!$A$2:$A$13', 'Data!$C$2:$C$13');
  Line.ZOrder := 1;                     // dessine la ligne devant les colonnes

  Chart.SecondaryValueAxis.NumberFormat := '0%';
  Chart.SecondaryValueAxis.NumberFormatSourceLinked := False;
  Chart.SecondaryValueAxis.MinValue := 0;
  Chart.SecondaryValueAxis.MaxValue := 0.5;

  // Zone de traçage en fractions du graphique : x, y, largeur, hauteur en un appel
  Chart.PlotAreaLayout.SetPositionAndSize(0.08, 0.12, 0.80, 0.72);
end;
Diagramme d'un graphique combiné HotXLS en Delphi avec des colonnes de revenus de AddSeries sur l'axe de valeurs principal et une ligne de marge de AddPlotGroup sur l'axe de valeurs secondaire, tandis que l'API plate héritée n'atteint que le groupe un
Chaque appel AddPlotGroup ajoute un groupe ordonné avec sa propre famille de graphiques et son affectation d'axe principal ou secondaire tandis que l'API plate adresse toujours le groupe un

ZOrder sur un groupe de traçage contrôle la superposition : l'écrivain de graphiques trie les groupes selon lui avant d'émettre les blocs c:barChart et c:lineChart, valeurs basses derrière. Comme chaque groupe ordonné est modélisé, ouvrir et réenregistrer un graphique combiné garde chaque famille de graphique avec ses axes secondaire, de date et de séries. Les références de formule de graphique et les valeurs de séries en cache survivent aux copies de classeur et aux cycles ouvrir/enregistrer, et les ancres de graphiques sont appariées à leurs parties de graphique par identifiant de relation, si bien qu'un package dont les entrées de relation ont été réordonnées par un autre outil replace quand même chaque graphique à sa position de feuille de calcul

Pourquoi Office ignore-t-il une mise en page manuelle ?

Parce que les coordonnées sont arrivées incomplètes. Office exige x, y, w et h en groupe complet dès que l'un d'eux est présent dans un c:manualLayout, et ignore l'élément entier sinon. Le schéma marque les quatre comme facultatifs, donc un fichier avec seulement x et y est valide et ne fait pourtant rien dans Excel

TXLSXChartLayout.SetPositionAndSize est l'appel à utiliser. Il positionne les quatre valeurs, marque les quatre éléments de mode comme présents et active la mise en page en une étape. Les propriétés individuelles X, Y, Width et Height positionnent chacune une valeur, et HotXLS écrit exactement les éléments présents, de la même façon qu'il garde la présence de chacun des neuf enfants facultatifs quand il lit un fichier. Cela garde une mise en page intacte fidèle à sa source. Cela signifie aussi qu'une mise en page construite par deux affectations de propriétés est écrite avec deux coordonnées, et Office la sautera. La même classe TXLSXChartLayout se trouve derrière PlotAreaLayout, TitleLayout, LegendLayout et le TitleLayout de chaque axe

Diagramme de la règle de mise en page manuelle HotXLS en Delphi où x, y, w et h doivent arriver en groupe complet sinon Office ignore tout l'élément manualLayout, avec les modes edge et factor et layoutTarget limité à la zone de traçage
Office n'applique une disposition manuelle que lorsque x, y, w et h arrivent en groupe complet, c'est pourquoi HotXLS émet le rectangle entier

Deux défauts suivent ce que fait Excel plutôt que tout ce que la spécification permet. Une nouvelle mise en page utilise edge pour XMode et YMode et factor pour WidthMode et HeightMode ; la norme permet les autres combinaisons, mais Excel enregistre les positions en edges et les tailles en factors et convertit tout le reste au moment de sauver. Et layoutTarget (Target, interne ou externe) est écrit pour la zone de traçage, tandis que les mises en page de titre, de titre d'axe et de légende l'omettent, parce que les notes d'implémentation de Microsoft pour le schéma de graphique ne l'y autorisent pas

Faire entrer cela dans un aller-retour de classeur

La préservation des graphiques est un cas d'une règle plus large : une bibliothèque qui édite des documents créés ailleurs devrait changer ce qu'on lui a demandé de changer et laisser le reste comme elle l'a trouvé. HotXLS applique le même raisonnement aux thèmes, chaînes de calcul et extensions de feuille de calcul, couverts dans les allers-retours sans perte pour thème, extLst et calcChain, et aux projets VBA et liens externes dans la préservation de VBA et des liens externes. Pour créer des graphiques, placer des images et dessiner des formes depuis zéro, voir les graphiques, images et dessins dans HotXLS

HotXLS lit, édite et écrit les graphiques XLSX depuis du code Delphi et C++Builder natif sans Excel installé sur la machine, si bien que la fidélité des graphiques se décide dans la bibliothèque et non dans un serveur d'automation. La référence de l'API de graphiques et une version d'essai sont sur la page du composant tableur Delphi HotXLS