Technischer Artikel

Excel-Diagramme ohne ChartML-Verlust bearbeiten in HotXLS

Ein Chart-Part, den ein aktueller Excel-Build speichert, endet selten bei den Elementen, die das ECMA-376-Chart-Schema definiert. Der Chart Space öffnet gewöhnlich mit einem mc:AlternateContent-Block, der neueren Lesern c14:style anbietet und älteren ein schlichtes c:style, jede Serie trägt ein c:extLst mit einer c16:uniqueId, und "Value From Cells"-Datenbeschriftungen leben in yet einer weiteren Erweiterung. Ein Programm, das nur parst, was es modelliert, und den Part zur Speicherzeit neu erzeugt, schreibt nichts davon zurück. Ein Wort im Titel geändert, die Datei in Excel wieder geöffnet, und die Beschriftungen, die ihren Text aus einem Zellbereich zogen, sind weg

Die Lösung ist, ein Speichern nicht mehr als Neuaufbau zu behandeln. Die Bytes behalten, mit denen das Diagramm ankam, sie unverändert zurückschreiben, wenn sich nichts Modellierte bewegt hat, und wenn doch, die neuen Elemente in den originalen Baum einführen, sodass die unvertrauten Teile bleiben, wo Excel sie hingelegt hat

Das originale ChartML erneut abspielen, wenn nichts geändert hat

HotXLS tut genau das, wenn TXLSXWorkbook.PreserveUnsupportedParts auf True steht, was bei einer frisch erzeugten Arbeitsmappe die Voreinstellung ist. Beim Import behält jedes Diagramm seine originalen UTF-8-Bytes zusammen mit der Länge und einem 64-Bit-Fingerprint des Inhalts, den das typisierte Modell repräsentiert. Zur Speicherzeit erzeugt HotXLS den modellierten Part neu, vergleicht Länge und Fingerprint, und wenn beide übereinstimmen, schreibt es die gespeicherten Bytes wortwörtlich zurück: keine Element-Umsortierung, kein Whitespace-Drift und keine zweite UTF-16-Kopie jedes unangetasteten Diagramms im Speicher

Weil der Vergleich auf dem Inhalt läuft, ändert das Zuweisen eines Werts, den eine Eigenschaft bereits hält, nichts. TXLSXChart-Setter kehren bei gleichem Wert vorzeitig zurück, also spielt eine Schleife, die jeden Diagrammtitel aus einer Datenbankzeile "auffrischt", weiterhin jedes Diagramm ab, dessen Titel bereits korrekt war. Die Linienformatierung von Fehlerbalken und Trendlinien wird von ihrem eigenen Änderungsflag verfolgt, und sie zu ändern zählt immer als Bearbeitung

Diagramm der HotXLS-ChartML-Erhaltung in Delphi, bei der unveränderte Diagramme die originalen Bytes erneut ausgeben und bearbeitete Diagramme typisierte Modellknoten in das bewahrte XML einführen, während ein Neuaufbau allein aus dem Modell extLst- und AlternateContent-Zweige verliert
HotXLS speichert die originalen ChartML-Bytes beim Import und gibt sie unangetastet wieder aus oder führt typisierte Bearbeitungen in den bewahrten Baum ein, wenn sich das Modell geändert hat

TXLSXChart.HasPreservedXml verrät, welchen Weg ein Diagramm nimmt, bevor man irgendetwas bearbeitet. Es meldet True, wenn dem Diagramm originales ChartML anhaftet, was die Vorbedingung für Replay und Zusammenführung gleichermaßen ist

var
  Workbook: TXLSXWorkbook;
  Chart: TXLSXChart;
begin
  Workbook := TXLSXWorkbook.Create;
  try
    // Per Voreinstellung True; explizit setzen, damit die Absicht Refactorings überlebt.
    // Es muss vor Open stehen, weil Open den Zustand aufbaut
    Workbook.PreserveUnsupportedParts := True;
    Workbook.Open('quarterly-review.xlsx');

    Chart := Workbook.Sheets[1].Charts[0];   // Sheets[] ist 1-basiert, Charts[] 0-basiert
    if not Chart.HasPreservedXml then
      Log('chart 0 has no original ChartML attached');   // Log ist die eigene Routine

    Chart.Title := Chart.Title;                  // gleicher Wert: wird weiterhin abgespielt
    Workbook.Sheets[1].Charts[1].Title :=
      'Revenue by region, Q3 2026';              // dieses Diagramm nimmt den Zusammenführungspfad
    Workbook.SaveAs('quarterly-review-out.xlsx');
  finally
    Workbook.Free;
  end;
end;

Was eine Zusammenführung behält, wenn man ein Diagramm bearbeitet?

Die kurze Antwort: alles, was das typisierte Modell nicht besitzt. Titel, Serien, Daten-Caches, Plot-Gruppen und Achsen gehören dem Modell, also entscheiden sie der neu erzeugte Baum. Nicht modellierte Attribute, Namespace-Deklarationen, Styles, Erweiterungslisten und Formatierungsknoten kommen aus dem originalen Baum und werden neben den passenden Knoten wieder eingefügt, neben denen sie saßen

Löscht man eine Serie, darf die Zusammenführung sie nicht aus dem bewahrten XML auferstehen lassen, nur weil das Original an derselben Position noch ein c:ser enthält. Alles, was dem Modell gehört, kommt aus dem Modell, selbst wenn die Antwort des Modells "weg" lautet. Dasselbe gilt auf Attributebene: Wischen Sie Fett, Kursiv, Unterstreichung, Durchgestrichen oder die Schriftgröße eines bekannten Textstils ab, liest die Zusammenführung das fehlende Attribut als Zurücksetzen und kopiert b oder sz nicht aus dem originalen defRPr zurück. Unbekannte Attribute desselben Elements behalten ihren Platz

Titel folgen derselben Regel. Beim Import behält HotXLS die nativen Rich-Runs von c:title mit ihren Schriftarten, Größen und Sprach-Tags pro Run und gibt sie erneut aus, solange der Text unangetastet ist. Weist man einen neuen Title zu, wird dieser Rich-Inhalt verworfen; der Titel wird als ein Run neu geschrieben, formatiert aus TitleTextStyle. Alte Runs in neuen Text einzuführen hieße raten, welcher Run zu den neuen Worten gehört, und ein Rateergebnis hier produziert Titel, die niemand geschrieben hat

Die Schema-Reihenfolge bleibt dort erhalten, wo Office pingelig ist. Ein extLst bleibt das letzte Kind seines Elternelements, wie das Schema verlangt. Markup-Compatibility-Blöcke bekommen eine eigene Behandlung: Der Reader überspringt mc:Choice und liest mc:Fallback, und wenn dieser Fallback eine Plot-Gruppe umschließt, die das Modell kennt, schreibt die Zusammenführung die neuen typisierten Inhalte in den Fallback-Teilbaum zurück. Eine naive Zusammenführung würde die bearbeitete Plot-Gruppe ausgeben und die alte Kopie im Fallback-Zweig lassen, also trüge das gespeicherte Diagramm dieselbe Gruppe doppelt

Ein einzelnes Diagramm per ClearPreservedXml ausnehmen

TXLSXChart.ClearPreservedXml verwirft das originale XML und den Fingerprint eines einzelnen Diagramms, und der nächste Save baut dieses Diagramm rein aus dem typisierten Modell neu auf. Es ist eine Entscheidung pro Diagramm. PreserveUnsupportedParts steuert weiterhin, ob beim Import überhaupt ein Erhaltungszustand aufgebaut wird, und es umfasst auch andere nicht unterstützte Paketparts, nicht nur Diagramme – deshalb ist das Abschalten, um ein einziges sauberes Diagramm zu bekommen, der falsche Hebel

Räumt man ein Diagramm aus, wenn das Wegwerfen der Sinn der Operation ist, etwa bei einer Vorlage, die noch Formatierung eines ausgedienten Firmen-Themes trägt. Als Debug-Reflex es allein lassen. Ein Diagramm, das nach einer Bearbeitung falsch aussieht, ist meist eine Modellierungsfrage, und das Löschen des bewahrten XML wirft die einzige Kopie davon weg, was Excel ursprünglich geschrieben hat

Ein Fall braucht vorher eine Prüfung. Nutzt ein Fehlerbalken oder eine Trendlinie eine Linienformatierung, die das typisierte Modell nicht darstellen kann, ist ihre Eigenschaft LineAppearanceSupported False; eine Trendlinien-Beschriftung mit Formatierung außerhalb des Modells meldet dasselbe über LabelAppearanceSupported. HotXLS ersetzt keines von beiden stillschweigend durch eine schlichtere Variante: Sobald das Diagramm kein bewahrtes XML als Rückfallebene hat, stoppt der Save mit einer EArgumentException, die die nicht darstellbare Linien- oder Beschriftungserscheinung benennt. Beides vor dem Ausnehmen zu prüfen kostet eine Schleife

// Eigener Helfer, kein Teil von HotXLS: True, wenn jede Fehlerbalken-Linie,
// Trendlinien-Linie und Trendlinien-Beschriftung allein aus dem Modell schreibbar sind
function ChartRebuildable(Chart: TXLSXChart): Boolean;
var
  I, T: Integer;
  S: TXLSXChartSeries;
begin
  Result := False;
  for I := 0 to Chart.SeriesCount - 1 do    // läuft über jede Plot-Gruppe
  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      // der nächste Save baut dieses Diagramm aus dem Modell neu auf
else
  Log('kept original ChartML for this chart');

Kombinationsdiagramme mit AddPlotGroup und einer zweiten Achse

TXLSXChart.AddPlotGroup hängt eine geordnete Plot-Gruppe mit eigener Diagrammfamilie und eigener primärer oder sekundärer Achsenzuordnung an. Das deckt das Diagramm ab, das jedes Finanzteam verlangt: Umsatz als Säulen auf der linken Achse, Marge als Prozentlinie rechts. Jedes Diagramm exponiert ValueAxis, CategoryAxis, SecondaryValueAxis, SecondaryCategoryAxis und SeriesAxis, mit typisierten Einstellungen für Position, Achsen-Identifikatoren, Schnitt, Datumseinheiten, Beschriftungsintervalle, Zahlenformat und Sichtbarkeit

Die flache API aus Eintyp-Diagrammen ist weiterhin da und mappt auf die Gruppen. ChartType und AddSeries adressieren die erste Plot-Gruppe; SeriesCount und Series[] laufen die Serien aller Gruppen der Reihe nach ab, weshalb der Helfer oben auch die Linienserie sieht. Die Reihenfolge zählt an einer Stelle. TXLSXCharts.Add erzeugt ein Diagramm ohne jede Plot-Gruppe, und welches von AddSeries oder AddPlotGroup zuerst läuft, legt Gruppe eins an. Auf einem frischen Diagramm zuerst AddPlotGroup(xlsxChartLine, xlsxAxisSecondary) aufzurufen macht die Linie zu Gruppe eins, der Diagrammtyp wechselt zu Linie, und die Umsatzserie, die man danach hinzufügt, landet darin. Beide Gruppen explizit anzulegen nimmt die Mehrdeutigkeit, also tut das Beispiel genau das

var
  Chart: TXLSXChart;
  Columns, Line: TXLSXChartPlotGroup;
begin
  // Verankert über Zeilen 1..15, Spalten 5..12; noch keine Plot-Gruppe vorhanden
  Chart := Sheet.Charts.Add(xlsxChartColumn, 'Revenue and margin',
    1, 5, 15, 12);
  Columns := Chart.AddPlotGroup(xlsxChartColumn);              // Gruppe 1, primär
  Columns.AddSeries('Revenue', 'Data!$A$2:$A$13', 'Data!$B$2:$B$13');

  Line := Chart.AddPlotGroup(xlsxChartLine, xlsxAxisSecondary); // Gruppe 2
  Line.AddSeries('Margin %', 'Data!$A$2:$A$13', 'Data!$C$2:$C$13');
  Line.ZOrder := 1;                     // die Linie vor den Säulen zeichnen

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

  // Plot-Area als Bruchteile des Diagramms: x, y, Breite, Höhe in einem Aufruf
  Chart.PlotAreaLayout.SetPositionAndSize(0.08, 0.12, 0.80, 0.72);
end;
Diagramm eines HotXLS-Kombinationsdiagramms in Delphi mit Revenue-Säulen aus AddSeries auf der primären Wertachse und einer Margin-Linie aus AddPlotGroup auf der sekundären Wertachse, während ChartType und AddSeries nur Gruppe eins erreichen
Jeder AddPlotGroup-Aufruf hängt eine geordnete Gruppe mit eigener Diagrammfamilie und primärer oder sekundärer Achsenzuordnung an, während ChartType und AddSeries weiterhin Gruppe eins ansprechen

ZOrder einer Plot-Gruppe steuert die Stapelung: Der Chart-Writer sortiert die Gruppen danach, bevor er die c:barChart- und c:lineChart-Blöcke ausgibt, niedrigere Werte hinten. Weil jede geordnete Gruppe modelliert ist, behält das Öffnen und erneute Speichern eines Kombinationsdiagramms jede Diagrammfamilie zusammen mit ihren Sekundär-, Datums- und Serienachsen. Chart-Formelreferenzen und gecachte Serienwerte überleben Arbeitsmappen-Kopien und Open/Save-Zyklen, und Chart-Anker werden über den Relationship-Identifikator ihrem Chart-Part zugeordnet, sodass ein Paket, dessen Relationship-Einträge von einem anderen Tool umsortiert wurden, jedes Diagramm trotzdem an seiner eigenen Arbeitsblattposition platziert

Warum ignoriert Office ein manuelles Layout?

Weil die Koordinaten unvollständig ankamen. Office verlangt x, y, w und h als vollständige Gruppe, sobald auch nur eines davon in einem c:manualLayout auftaucht, und ignoriert andernfalls das gesamte Element. Das Schema markiert alle vier als optional, also ist eine Datei mit nur x und y gültig und macht in Excel trotzdem nichts

TXLSXChartLayout.SetPositionAndSize ist der Aufruf dafür. Er setzt alle vier Werte, markiert die vier Mode-Elemente als vorhanden und schaltet das Layout in einem Schritt frei. Die einzelnen Eigenschaften X, Y, Width und Height setzen jeweils einen Wert, und HotXLS schreibt exakt die Elemente, die vorhanden sind, genauso wie es beim Lesen einer Datei die Anwesenheit jedes der neun optionalen Kinder festhält. So bleibt ein unangetastetes Layout treu zu seiner Quelle. Es bedeutet auch, dass ein Layout aus zwei Eigenschaftszuweisungen mit zwei Koordinaten geschrieben wird und Office es überspringt. Dieselbe Klasse TXLSXChartLayout steckt hinter PlotAreaLayout, TitleLayout, LegendLayout und dem TitleLayout jeder Achse

Diagramm der HotXLS-Manual-Layout-Regel in Delphi, wonach x, y, w und h als vollständige Gruppe ankommen müssen, sonst ignoriert Office das gesamte manualLayout-Element; mit Edge- und Faktor-Modi und layoutTarget beschränkt auf die Plot-Area
Office wendet ein manuelles Layout nur an, wenn x, y, w und h als vollständige Gruppe ankommen, weshalb SetPositionAndSize alle vier in einem Aufruf schreibt

Zwei Voreinstellungen folgen dem, was Excel tut, statt allem, was die Spezifikation erlaubt. Ein neues Layout nutzt edge für XMode und YMode und factor für WidthMode und HeightMode; der Standard erlaubt die anderen Kombinationen, aber Excel speichert Positionen als Kanten und Größen als Faktoren und konvertiert alles andere beim eigenen Speichern. Und layoutTarget (Target, innen oder außen) wird für die Plot-Area geschrieben, während Titel-, Achsentitel- und Legend-Layouts es weglassen, denn die Implementierungsnotizen von Microsoft zum Chart-Schema erlauben es unter diesen Elementen nicht

Das in einen Arbeitsmappen-Roundtrip einpassen

Diagramm-Erhaltung ist ein Fall einer allgemeineren Regel: Eine Bibliothek, die woanders erstellte Dokumente bearbeitet, sollte das ändern, was sie ändern soll, und den Rest so lassen, wie sie ihn vorgefunden hat. Denselben Gedanken wendet HotXLS bei Themes, Berechnungsketten und Arbeitsblatt-Erweiterungen an, beschrieben in verlustfreie Roundtrips für Theme, extLst und calcChain, und bei VBA-Projekten und externen Links in Erhaltung von VBA und externen Links. Für Diagramme, Bilder und Zeichnungen von Grund auf siehe Diagramme, Bilder und Zeichnungen in HotXLS

HotXLS liest, bearbeitet und schreibt XLSX-Diagramme aus nativem Delphi- und C++Builder-Code, ohne dass Excel auf der Maschine installiert ist, also wird die Diagrammtreue von der Bibliothek entschieden und nicht von einem Automatisierungsserver. Die Diagramm-API-Referenz und ein Trial-Build finden sich auf der HotXLS-Delphi-Spreadsheet-Komponentenseite