Technisch artikel

Excel-grafieken bewerken zonder ChartML-verlies in HotXLS

Een grafiekonderdeel dat door een huidige Excel-build is opgeslagen stopt zelden bij de elementen die het ECMA-376-chart-schema definieert. De chart space opent meestal met een mc:AlternateContent-blok dat nieuwere lezers c14:style aanbiedt en oudere een kale c:style, elke reeks draagt een c:extLst met een c16:uniqueId, en de datalabels van "Value From Cells" wonen in nog weer een andere extensie. Een programma dat alleen parseert wat het modelleert en het onderdeel bij het opslaan opnieuw genereert schrijft daar geen van alle terug. Verander één woord in de titel, open het bestand opnieuw in Excel, en de labels die hun tekst uit een celbereik haalden zijn weg

De oplossing is om een save niet meer als herbouw te behandelen. Houd de bytes vast waarmee de grafiek binnenkwam, schrijf ze ongewijzigd terug wanneer er niets gemodelleerds is verschoven, en schuif wanneer dat wel zo is de nieuwe elementen in de originele boom, zodat de onbekende delen blijven staan waar Excel ze neerzette

De originele ChartML opnieuw afspelen wanneer niets veranderde

HotXLS doet precies dat wanneer TXLSXWorkbook.PreserveUnsupportedParts True is, wat de default is op een vers aangemaakte werkmap. Bij import houdt elke grafiek zijn originele UTF-8-bytes vast, samen met de lengte en een 64-bits vingerprint van de content die het getypeerde model vertegenwoordigt. Bij het opslaan regenereert HotXLS het gemodelleerde onderdeel, vergelijkt lengte en vingerprint, en schrijft bij een dubbele match de opgeslagen bytes letterlijk terug: geen elementherordening, geen whitespace-drift, en geen tweede UTF-16-kopie van elke onaangeroerde grafiek in het geheugen

Omdat de vergelijking op content draait, verandert het toekennen van een property de waarde die hij al heeft niets. De setters van TXLSXChart stappen vroegtijdig uit bij een gelijke waarde, dus een loop die elke grafiektitel "ververst" vanuit een databaserij speelt nog steeds elke grafiek opnieuw af waarvan de titel al klopte. De lijnopmaak van foutbalken en trendlijnen wordt bijgehouden via een eigen change-vlag, en die aanpassen telt altijd als bewerking

Diagram van HotXLS ChartML-behoud in Delphi waarin onveranderde grafieken de originele bytes herafspelen en bewerkte grafieken getypeerde modelknopen samenvoegen in de behouden XML, terwijl een herbouw uit het model alleen extLst- en AlternateContent-takken verliest
HotXLS bewaart de originele ChartML-bytes bij import en speelt ze onaangeroerd af, of voegt getypeerde bewerkingen samen in de bewaarde boom wanneer het model veranderde

TXLSXChart.HasPreservedXml vertelt voor welke route een grafiek staat voordat je iets bewerkt. Hij meldt True wanneer er originele ChartML aan de grafiek hangt, wat de voorwaarde is voor zowel de replay als de merge

var
  Workbook: TXLSXWorkbook;
  Chart: TXLSXChart;
begin
  Workbook := TXLSXWorkbook.Create;
  try
    // Standaard True; zet hem expliciet zodat de intentie refactoring overleeft.
    // Hij moet vóór Open staan, want Open bouwt de toestand op
    Workbook.PreserveUnsupportedParts := True;
    Workbook.Open('quarterly-review.xlsx');

    Chart := Workbook.Sheets[1].Charts[0];   // Sheets[] is 1-based, Charts[] 0-based
    if not Chart.HasPreservedXml then
      Log('chart 0 has no original ChartML attached');   // Log is je eigen routine

    Chart.Title := Chart.Title;                  // gelijke waarde: speelt nog steeds opnieuw af
    Workbook.Sheets[1].Charts[1].Title :=
      'Revenue by region, Q3 2026';              // deze grafiek neemt de merge-route
    Workbook.SaveAs('quarterly-review-out.xlsx');
  finally
    Workbook.Free;
  end;
end;

Wat overleeft een merge als je één grafiek bewerkt?

Het korte antwoord: alles wat het getypeerde model niet bezit. Titel, reeksen, datacaches, plotgroepen en assen zijn eigendom van het model, dus de vers gegenereerde boom bepaalt ze. Niet-gemodelleerde attributen, namespace-declaraties, stijlen, extensielijsten en opmaakknopen komen uit de originele boom en worden opnieuw ingevoegd naast de gematchte knopen waar ze oorspronkelijk naast zaten

Verwijder een reeks, dan mag de merge hem niet uit de behouden XML laten herrijzen alleen omdat het origineel nog een c:ser op die positie bevat. Alles wat het model bezit komt uit het model, ook wanneer het antwoord van het model "weg" is. Hetzelfde geldt op attribuutniveau: wanneer je vet, cursief, onderstrepen, doorhalen of de fontgrootte van een bekende tekststijl wist, leest de merge het ontbrekende attribuut als gewist en kopieert b of sz niet terug uit de originele defRPr. Onbekende attributen op hetzelfde element behouden hun plek

Titels volgen dezelfde regel. Bij import houdt HotXLS de native rich-runs van c:title vast, met hun fonts, groottes en taaltags per run, en speelt ze af zolang de tekst onaangeroerd is. Wijs een nieuwe Title toe en die rich-content valt weg; de titel wordt herschreven als één run met opmaak uit TitleTextStyle. Oude runs in nieuwe tekst samenvoegen zou betekenen dat je gokt bij welke run de nieuwe woorden horen, en zo'n gok levert titels op die niemand heeft getypt

De schema-volgorde blijft behouden waar Office kieskeurig over is. Een extLst blijft het laatste kind van zijn ouder, zoals het schema vereist. Markup-compatibiliteitsblokken krijgen een eigen behandeling: de lezer slaat mc:Choice over en leest mc:Fallback, en wanneer die fallback een plotgroep omsluit die het model kent, schrijft de merge de nieuwe getypeerde content terug in de fallback-subboom. Een naïeve merge zou de bewerkte plotgroep emitteeren en de oude kopie in de fallback-tak achterlaten, dus de opgeslagen grafiek zou dezelfde groep twee keer dragen

Eén grafiek uitschakelen met ClearPreservedXml

TXLSXChart.ClearPreservedXml gooit de originele XML en het vingerprint van één grafiek weg, en de volgende save herbouwt die grafiek puur uit het getypeerde model. Het is een beslissing per grafiek. PreserveUnsupportedParts bepaalt nog steeds of er bij import behoudtoestand wordt opgebouwd, en het dekt ook andere niet-ondersteunde pakketonderdelen, niet alleen grafieken, wat de reden is dat het uitzetten om één schone grafiek te krijgen de verkeerde hendel is

Wis een grafiek wanneer weggooien het doel van de operatie is, bijvoorbeeld een sjabloongrafiek die nog opmaak van een afgeschreven huisstijl draagt. Laat hem verder met rust als debugreflex. Een grafiek die na een bewerking verkeerd rendert is meestal een modelleervraag, en het wissen van de behouden XML gooit het enige exemplaar weg van wat Excel oorspronkelijk schreef

Eén geval vraagt eerst een check. Gebruikt een foutbalk of trendlijn lijnopmaak die het getypeerde model niet kan vertegenwoordigen, dan is zijn LineAppearanceSupported-property False; een trendlijnlabel met opmaak buiten het model meldt hetzelfde via LabelAppearanceSupported. HotXLS vervangt geen van beide stilletjes door een simpelere versie: zodra de grafiek geen behouden XML meer als vangnet heeft, stopt de save met een EArgumentException die de niet-vertegenwoordigde lijn- of labelweergave noemt. Op beide testen vóór het wissen kost één loop

// Eigen hulpfunctie, geen onderdeel van HotXLS: True wanneer elke foutbalklijn,
// trendlijn en trendlijnlabel alleen uit het getypeerde model geschreven kan worden
function ChartRebuildable(Chart: TXLSXChart): Boolean;
var
  I, T: Integer;
  S: TXLSXChartSeries;
begin
  Result := False;
  for I := 0 to Chart.SeriesCount - 1 do    // loopt over elke plotgroep
  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      // de volgende save herbouwt deze grafiek uit het model
else
  Log('kept original ChartML for this chart');

Combinatiegrafieken met AddPlotGroup en een secundaire as

TXLSXChart.AddPlotGroup voegt een geordende plotgroep toe met zijn eigen chartfamilie en zijn eigen primaire of secundaireastoewijzing. Dat dekt de grafiek die elk financieteam vraagt: omzet als kolommen op de linker as, margepercentage als lijn op de rechter. Elke grafiek stelt ValueAxis, CategoryAxis, SecondaryValueAxis, SecondaryCategoryAxis en SeriesAxis bloot, met getypeerde instellingen voor positie, as-identificatoren, kruising, datum-eenheden, labelintervallen, getalnotatie en zichtbaarheid

De vlakke API van single-type-grafieken bestaat nog steeds en mapt op de groepen. ChartType en AddSeries richten zich op de eerste plotgroep; SeriesCount en Series[] lopen de reeksen van elke groep op volgorde af, wat de reden is dat de hulpfunctie hierboven ook de lijnreeks ziet. De volgorde weegt op één plek. TXLSXCharts.Add maakt een grafiek zonder enige plotgroep aan, en welke van AddSeries of AddPlotGroup het eerst draait maakt groep één. Roep AddPlotGroup(xlsxChartLine, xlsxAxisSecondary) eerst aan op een verse grafiek en de lijn wordt groep één, het grafiektype wisselt naar lijn, en de omzetreeks die je daarna toevoegt belandt erin. Beide groepen expliciet aanmaken neemt de ambiguïteit weg, dus het voorbeeld doet dat

var
  Chart: TXLSXChart;
  Columns, Line: TXLSXChartPlotGroup;
begin
  // Verankerd over rijen 1..15, kolommen 5..12; er bestaat nog geen plotgroep
  Chart := Sheet.Charts.Add(xlsxChartColumn, 'Revenue and margin',
    1, 5, 15, 12);
  Columns := Chart.AddPlotGroup(xlsxChartColumn);              // groep 1, primair
  Columns.AddSeries('Revenue', 'Data!$A$2:$A$13', 'Data!$B$2:$B$13');

  Line := Chart.AddPlotGroup(xlsxChartLine, xlsxAxisSecondary); // groep 2
  Line.AddSeries('Margin %', 'Data!$A$2:$A$13', 'Data!$C$2:$C$13');
  Line.ZOrder := 1;                     // de lijn vóór de kolommen tekenen

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

  // Plotgebied als fracties van de grafiek: x, y, breedte, hoogte in één aanroep
  Chart.PlotAreaLayout.SetPositionAndSize(0.08, 0.12, 0.80, 0.72);
end;
Diagram van een HotXLS-combinatiegrafiek in Delphi met omzetkolommen via AddSeries op de primaire waarde-as en een margelijn via AddPlotGroup op de secundaire waarde-as, terwijl ChartType en AddSeries alleen groep één bereiken
Elke AddPlotGroup-aanroep voegt een geordende groep toe met eigen chartfamilie en primaire of secundaireastoewijzing, terwijl ChartType en AddSeries nog steeds groep één bedienen

ZOrder op een plotgroep stuurt de gelaagdheid: de chart-writer sorteert groepen ernaar voordat hij de c:barChart- en c:lineChart-blokken emitteert, lagere waarden achteraan. Omdat elke geordende groep gemodelleerd is, houdt het openen en opnieuw opslaan van een combinatiegrafiek elke chartfamilie bij elkaar met zijn secundaire, datum- en reeksen-assen. Chart-formuleverwijzingen en gecachte reekswaarden overleven werkmapkopieën en open/save-rondes, en grafiekankers worden via relationship-identificator aan hun grafiekonderdelen gematcht, dus een pakket waarvan de relationship-entries door een andere tool zijn herordend zet elke grafiek nog op zijn eigen werkbladpositie

Waarom negeert Office een handmatige layout?

Omdat de coördinaten incompleet binnenkwamen. Office vereist x, y, w en h als complete groep zodra één ervan in een c:manualLayout staat, en negeert anders het hele element. Het schema markeert alle vier als optioneel, dus een bestand met alleen x en y is geldig en doet desondanks niets in Excel

TXLSXChartLayout.SetPositionAndSize is de aanroep om te gebruiken. Hij zet alle vier de waarden, markeert de vier mode-elementen als aanwezig en zet de layout in één stap aan. De aparte properties X, Y, Width en Height zetten elk één waarde, en HotXLS schrijft exact de elementen die aanwezig zijn, op dezelfde manier als hij bij het lezen van een bestand de aanwezigheid van elk van de negen optionele kinderen bijhoudt. Dat houdt een onaangeroerde layout trouw aan zijn bron. Het betekent ook dat een layout gebouwd uit twee property-toekeningen met twee coördinaten wordt geschreven, en Office slaat hem dan over. Dezelfde klasse TXLSXChartLayout zit achter PlotAreaLayout, TitleLayout, LegendLayout en de TitleLayout van elke as

Diagram van de HotXLS-regel voor handmatige layout in Delphi waarbij x, y, w en h als complete groep moeten binnenkomen of Office het hele manualLayout-element negeert, met edge- en factor-modi en layoutTarget beperkt tot het plotgebied
Office past een handmatige layout alleen toe wanneer x, y, w en h als complete groep binnenkomen, en daarom schrijft SetPositionAndSize alle vier in één aanroep

Twee defaults volgen wat Excel doet in plaats van alles wat de specificatie toestaat. Een nieuwe layout gebruikt edge voor XMode en YMode en factor voor WidthMode en HeightMode; de standaard staat de andere combinaties toe, maar Excel slaat posities op als edges en groottes als factors en zet al het andere om wanneer hij opslaat. En layoutTarget (Target, inner of outer) wordt voor het plotgebied geschreven, terwijl titel-, as-titel- en legenda-layouts hem weglaten, want Microsofts implementatienotities bij het chart-schema staan hem onder die elementen niet toe

Dit passen in een werkmap-round-trip

Grafiekbehoud is één geval van een bredere regel: een library die documenten bewerkt die elders zijn gemaakt zou veranderen wat er gevraagd werd en de rest laten zoals hij hem aantrof. HotXLS past dezelfde redenering toe op thema's, berekeningsketens en werkbladextensies, behandeld in verliesvrije round-trips voor theme, extLst en calcChain, en op VBA-projecten en externe koppelingen in VBA en externe koppelingen behouden. Voor het maken van grafieken, het plaatsen van afbeeldingen en het tekenen van shapes vanuit het niets, zie grafieken, afbeeldingen en drawings in HotXLS

HotXLS leest, bewerkt en schrijft XLSX-grafieken vanuit native Delphi- en C++Builder-code zonder Excel op de machine, dus grafiekgetrouwheid wordt door de library bepaald en niet door een automation-server. De chart-API-referentie en een proefbuild staan op de HotXLS Delphi spreadsheet component-pagina