現行のExcelビルドが保存したチャートパートは、ECMA-376のチャートスキーマが定義する要素で止まることはまれです。チャート空間はたいていmc:AlternateContentブロックで幕を開け、新しいリーダーにはc14:styleを、古いリーダーには素のc:styleを提供します。各系列はc16:uniqueIdを持つc:extLstを運び、「セルの値から」のデータラベルはさらに別の拡張に住んでいます。自分がモデル化したものだけをパースし、保存時にパートを再生成するプログラムは、それらを何も書き戻しません。タイトルの1語を変え、Excelで開き直すと、セル範囲から文字を引っ張っていたラベルが消えます
解決は、保存を再構築として扱うのをやめることです。チャートがやってきたときのバイトを保持し、モデル化されたものが何も動いていなければ元のまま書き戻し、動いていれば新しい要素を元のツリーへマージして、見慣れない部分をExcelが置いた場所に留めます
何も変わっていなければ元のChartMLを再生する
HotXLSはTXLSXWorkbook.PreserveUnsupportedPartsがTrueのときにまさにそれをします。新しく作ったワークブックではこれが既定です。インポート時に、各チャートは型付きモデルが表すコンテンツの長さと64ビットフィンガープリントと一緒に、元のUTF-8バイトを保持します。保存時にHotXLSはモデル化パートを再生成し、長さとフィンガープリントを比較し、両方とも一致すれば保持バイトをそのまま書き戻します。要素の並べ替えも、ホワイトスペースのドリフトも、触れていないチャートのUTF-16コピーがメモリに二重に居座ることもありません
比較はコンテンツに対して走るため、既に持っている値の代入は何も変えません。TXLSXChartのセッターは等しい値で早期リターンするため、データベースの行からすべてのチャートタイトルを「更新」するループでも、タイトルが既に正しいチャートはすべて再生されます。誤差範囲バーと近似曲線の線書式は独自の変更フラグで追跡され、これを変えることは常に編集と数えられます
TXLSXChart.HasPreservedXmlは、何かを編集する前に、そのチャートがどちらの経路にいるかを教えます。元のChartMLがチャートに付随しているときTrueを返し、これが再生とマージ両方の前提条件です
var
Workbook: TXLSXWorkbook;
Chart: TXLSXChart;
begin
Workbook := TXLSXWorkbook.Create;
try
// 既定でTrue。リファクタリングしても意図が生き残るよう明示的に設定する。
// Openが状態を組み立てるため、Openの前に設定しておく必要がある
Workbook.PreserveUnsupportedParts := True;
Workbook.Open('quarterly-review.xlsx');
Chart := Workbook.Sheets[1].Charts[0]; // Sheets[]は1始まり、Charts[]は0始まり
if not Chart.HasPreservedXml then
Log('chart 0 has no original ChartML attached'); // Logは独自のルーチン
Chart.Title := Chart.Title; // 等しい値:それでも再生される
Workbook.Sheets[1].Charts[1].Title :=
'Revenue by region, Q3 2026'; // このチャートはマージパスを通る
Workbook.SaveAs('quarterly-review-out.xlsx');
finally
Workbook.Free;
end;
end;
1つのチャートを編集するとき、マージで生き残るものは何か
短い答えは、型付きモデルが所有しないすべてです。タイトル、系列、データキャッシュ、プロットグループ、軸はモデルが所有するため、新しく生成されたツリーが決めます。モデル化されていない属性、名前空間宣言、スタイル、拡張リスト、書式ノードは元のツリーから来て、以前その隣に座っていた一致ノードの脇へ再挿入されます
系列を削除したら、マージは、元のXMLがその位置にまだc:serを含んでいるからといって、保持XMLからそれを蘇らせてはなりません。モデルが所有するものは、モデルの答えが「消えた」であっても、モデルから取られます。同じことは属性レベルでも言えます。既知のテキストスタイルから太字、斜体、下線、打ち消し線、フォントサイズをクリアしたとき、マージは欠けた属性をクリアとして読み、元のdefRPrからbやszを書き戻しません。同じ要素上の未知の属性は場所を保ちます
タイトルも同じ規則に従います。インポート時にHotXLSはc:titleのネイティブなリッチランを、ランごとのフォント、サイズ、言語タグと一緒に保持し、テキストに触れていない限り再生します。新しいTitleを代入すると、そのリッチコンテンツは落ち、タイトルはTitleTextStyleからスタイルされた1つのランとして書き直されます。古いランを新しいテキストへマージするのは、新しい単語がどのランに属するかの推測を意味し、ここの推測は誰も打っていないタイトルを生みます
スキーマ順序は、Officeがうるさい場所では保持されます。extLstはスキーマの要求どおり、親の最後の子であり続けます。マークアップ互換ブロックは独自の扱いを受けます。リーダーはmc:Choiceをスキップしてmc:Fallbackを読み、そのフォールバックがモデルの知るプロットグループを包んでいるとき、マージは新しい型付きコンテンツをフォールバックのサブツリーの中へ書き戻します。素朴なマージなら、編集済みプロットグループを出力して古いコピーをフォールバック分岐の中に残し、保存されたチャートは同じグループを2回抱えることになります
ClearPreservedXmlで1つのチャートをオプトアウトする
TXLSXChart.ClearPreservedXmlは1つのチャートから元のXMLとフィンガープリントを落とし、次の保存はそのチャートを純粋に型付きモデルから再構築します。チャートごとの決定です。PreserveUnsupportedPartsは引き続き、インポート時に保持状態を組み立てるかどうかを支配し、しかもチャートだけではなく他の非対応パッケージパートも覆うため、1つのクリーンなチャートを得るためにこれを切るのは間違ったレバーです
クリアするのは、捨てること自体が操作の目的であるときです。たとえば、引退した企業テーマの書式をまだ纏っているテンプレートチャートなどです。デバッグの反射としては触れないでください。編集後に誤って描画されるチャートは、たいていモデリングの質問であり、保持XMLをクリアすると、Excelが最初に書いたものの唯一のコピーを捨てることになります
まず確認が要るケースが1つあります。誤差範囲バーや近似曲線が、型付きモデルが表現できない線書式を使っている場合、そのLineAppearanceSupportedはFalseで、モデル外の書式を持つ近似曲線ラベルはLabelAppearanceSupportedが同じことを報告します。HotXLSはどちらかを黙ってより素朴な版に置き換えたりしません。チャートにはもう保持XMLという頼みの綱がないため、保存は、表現されていない線やラベルの外観を名指すEArgumentExceptionで止まります。クリアする前に両方を試すにはループ1つで足ります
// HotXLSの一部ではない独自のヘルパー:すべての誤差範囲バーの線、
// 近似曲線の線、近似曲線ラベルが型付きモデルだけから書ける場合にTrue
function ChartRebuildable(Chart: TXLSXChart): Boolean;
var
I, T: Integer;
S: TXLSXChartSeries;
begin
Result := False;
for I := 0 to Chart.SeriesCount - 1 do // すべてのプロットグループを網羅する
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 // 次の保存でこのチャートはモデルから再構築される
else
Log('kept original ChartML for this chart');
AddPlotGroupと第2軸による複合チャート
TXLSXChart.AddPlotGroupは、独自のチャートファミリーと、主軸か第2軸かの割り当てを持つ、順序付きプロットグループを追加します。これが財務チームが必ず頼むチャートを覆います。左軸に縦棒の売上、右軸に折れ線のマージン率です。各チャートはValueAxis、CategoryAxis、SecondaryValueAxis、SecondaryCategoryAxis、SeriesAxisを公開し、位置、軸識別子、交差、日付単位、ラベル間隔、数値書式、可視性の型付き設定を持ちます
単一型チャート用のフラットAPIもそのまま残り、グループへ写像されます。ChartTypeとAddSeriesは最初のプロットグループをアドレスし、SeriesCountとSeries[]はすべてのグループの系列を順に歩くため、上のヘルパーが折れ線系列も見えるのはこのためです。順序が効く場所が1つあります。TXLSXCharts.Addはプロットグループを1つも持たないチャートを生成し、AddSeriesかAddPlotGroupのどちらが先に走るかでグループ1が生まれます。新しいチャートで先にAddPlotGroup(xlsxChartLine, xlsxAxisSecondary)を呼ぶと、折れ線がグループ1になり、チャートタイプは折れ線に切り替わり、次に追加する売上系列はそこへ落ちます。両グループを明示的に作れば曖昧は消えるため、例はそうしています
var
Chart: TXLSXChart;
Columns, Line: TXLSXChartPlotGroup;
begin
// 1..15行、5..12列にアンカー。プロットグループはまだ存在しない
Chart := Sheet.Charts.Add(xlsxChartColumn, 'Revenue and margin',
1, 5, 15, 12);
Columns := Chart.AddPlotGroup(xlsxChartColumn); // グループ1、主軸
Columns.AddSeries('Revenue', 'Data!$A$2:$A$13', 'Data!$B$2:$B$13');
Line := Chart.AddPlotGroup(xlsxChartLine, xlsxAxisSecondary); // グループ2
Line.AddSeries('Margin %', 'Data!$A$2:$A$13', 'Data!$C$2:$C$13');
Line.ZOrder := 1; // 縦棒の前に折れ線を描く
Chart.SecondaryValueAxis.NumberFormat := '0%';
Chart.SecondaryValueAxis.NumberFormatSourceLinked := False;
Chart.SecondaryValueAxis.MinValue := 0;
Chart.SecondaryValueAxis.MaxValue := 0.5;
// プロットエリアをチャートに対する割合で:x、y、width、heightを1回の呼び出しで
Chart.PlotAreaLayout.SetPositionAndSize(0.08, 0.12, 0.80, 0.72);
end;
プロットグループのZOrderはレイヤーの重なりを制御します。チャートライターはc:barChartとc:lineChartブロックを出力する前にグループをこれで並べ替え、小さい値が後ろです。すべての順序付きグループがモデル化されているため、複合チャートを開いて保存し直しても、各チャートファミリーはその第2軸、日付軸、系列軸と一緒に保たれます。チャートの数式参照とキャッシュ系列値はワークブックの複製や開き・保存サイクルを生き延び、チャートアンカーはリレーションシップ識別子でチャートパートと突き合わされるため、別のツールがリレーションシップエントリを並べ替えたパッケージでも、各チャートは自分のワークシート位置に収まります
Officeは手動レイアウトを無視することがあるのはなぜか
座標が不完全なまま届いたからです。Officeはc:manualLayoutの中に4つのうちどれか1つでも存在する場合、x、y、w、hを完全なグループとして要求し、そうでなければ要素全体を無視します。スキーマは4つすべてを任意としているため、xとyだけのファイルは有効であり、それでもExcelでは何も起こりません
使うべき呼び出しはTXLSXChartLayout.SetPositionAndSizeです。4つの値を設定し、4つのモード要素を存在印付きにし、レイアウトを有効化するのが1ステップです。個々のX、Y、Width、Heightプロパティはそれぞれ1つの値を設定し、HotXLSは存在する要素だけを書きます。ファイルを読むときに9つの任意の子の存在を1つずつ保持するのと同じやり方です。これにより触れていないレイアウトはソースに忠実であり続けます。裏を返せば、2つのプロパティ代入で組み立てられたレイアウトは2つの座標で書かれ、Officeはそれを飛ばします。同じTXLSXChartLayoutクラスがPlotAreaLayout、TitleLayout、LegendLayout、そして各軸のTitleLayoutの背後に座っています
2つの既定値は、仕様が許すすべてではなく、Excelの振る舞いに従います。新しいレイアウトはXModeとYModeにedgeを、WidthModeとHeightModeにfactorを使います。標準は他の組合せも許しますが、Excelは位置をedgeとしてサイズをfactorとして保存し、それ以外は保存時に変換します。そしてlayoutTarget(Target、内側か外側か)はプロットエリアに対しては書かれますが、タイトル、軸タイトル、凡例のレイアウトでは省略されます。チャートスキーマに関するMicrosoftの実装ノートが、これらの要素の下ではそれを許していないからです
ブックのラウンドトリップにこれを組み込む
チャート保持は、より広い規則の1つのケースです。よそで作成された文書を編集するライブラリは、頼まれた変更だけを施し、残りは出会ったときのままにすべきです。HotXLSは同じ考えをテーマ、計算チェーン、ワークシート拡張に適用しており、テーマ、extLst、calcChainのロスレスラウンドトリップで、またVBAプロジェクトと外部リンクについてはVBAと外部リンクの保持で扱っています。チャートの作成、画像の配置、図形描画をゼロから行う場合は、HotXLSのチャート、画像、描画を参照してください
HotXLSは、マシンにExcelをインストールしなくても、ネイティブのDelphiおよびC++BuilderコードからXLSXチャートを読み、編集し、書き出します。チャートの忠実度を決めるのはオートメーションサーバーではなくライブラリです。チャートAPIリファレンスとトライアルビルドはHotXLS Delphiスプレッドシートコンポーネントのページにあります