当前版本 Excel 存出的图表部件很少止步于 ECMA-376 图表模式定义的那些元素。图表空间通常以一个 mc:AlternateContent 块开场,向新阅读器提供 c14:style、向旧阅读器提供朴素 c:style;每个系列带着带 c16:uniqueId 的 c:extLst;“Value From Cells”数据标签则活在又一个扩展里。只解析自己建模的内容、保存时重新生成整个部件的程序,这些一样也写不回去。改一下标题里的一个词,在 Excel 里重新打开,从单元格区域取文字的标签就没了
解法是别把保存当重建。图表带什么字节来就留什么字节,模型没动就原样写回;动了,就把新元素合并进原始树,让 Excel 放的那些陌生部件留在原位
什么都没改时原样重放原始 ChartML
TXLSXWorkbook.PreserveUnsupportedParts 为 True 时(新建工作簿的默认值),HotXLS 正是这么做的。导入时,每个图表保存原始 UTF-8 字节,连同类型化模型所表示内容的长度和 64 位指纹。保存时 HotXLS 重新生成建模部分,比较长度与指纹,两者皆同就把存储字节原封不动写回:没有元素重排、没有空白漂移、内存里也不为每个未触碰的图表再留一份 UTF-16 拷贝
因为比较基于内容,把属性赋成它已有的值不构成改变。TXLSXChart 的 setter 遇到相等值提前返回,所以一个从数据库行“刷新”每个图表标题的循环,对标题本来就正确的图表照样走重放。误差线和趋势线的线条格式由独立变更标志跟踪,改它永远算编辑
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;
编辑一个图表时,合并保住了什么?
简短回答:类型化模型不拥有的一切。标题、系列、数据缓存、绘图组和坐标轴归模型所有,由新生成的树决定;未建模的属性、命名空间声明、样式、扩展列表和格式化节点来自原始树,重新插回到它们原本相邻的匹配节点旁边
删掉一个系列,合并绝不能因为保留 XML 里那个位置仍有 c:ser 就让它复活。模型拥有的一切取自模型,哪怕模型的答案是“没了”。属性层面同理:当你清除某个已知文本样式的粗体、斜体、下划线、删除线或字号时,合并把缺失的属性读作清除,不会从原始 defRPr 把 b 或 sz 抄回来。同一元素上的未知属性保持原位
标题遵循同一规则。导入时 HotXLS 保留 c:title 的原生富文本 run,连同每个 run 的字体、字号和语言标记,只要文本未动就重放它们。赋一个新 Title,这些富内容就被丢弃;标题改写为按 TitleTextStyle 设置样式的单个 run。把旧 run 合并进新文本意味着猜测新词属于哪个 run,这种猜测产出的标题没人打得出来
Office 在意的模式顺序会被保留。extLst 像模式要求的那样保持为其父元素的最后一个孩子。标记兼容块有专门处理:读取器跳过 mc:Choice 读 mc:Fallback,当回退包装着模型认识的绘图组时,合并把新的类型化内容写回回退子树。想当然的合并会输出编辑后的绘图组、同时把旧副本留在回退分支里,存出的图表就带着两份同样的组
用 ClearPreservedXml 让单个图表退出
TXLSXChart.ClearPreservedXml 丢弃一个图表的原始 XML 和指纹,下一次保存完全从类型化模型重建该图表。这是逐图表的决定。PreserveUnsupportedParts 管的是导入时是否构建保留状态,而且它还覆盖图表之外的其他不受支持包部件,所以为了得到一个“干净”图表去关它是错误的杠杆
当丢弃本身就是操作目的时清空,比如一张还带着废弃企业主题格式的模板图表。作为调试反射动作时别清。编辑后渲染不对的图表通常是建模问题,清掉保留 XML 会把 Excel 最初写下的唯一一份记录扔掉
有一种情况要先检查。如果误差线或趋势线用了类型化模型无法表示的线条格式,其 LineAppearanceSupported 为 False;带模型外格式的趋势线标签通过 LabelAppearanceSupported 报告同样情况。HotXLS 不会悄悄用更朴素的版本顶替任何一方:一旦图表没有保留 XML 可退,保存就以 EArgumentException 停下,点名无法表示的线条或标签外观。清空之前两个都测,一个循环就够
// 你自己的辅助函数,不属于 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 和副轴构建组合图表
TXLSXChart.AddPlotGroup 追加一个有序绘图组,带自己的图表族和主轴或副轴指派。这正好覆盖每个财务团队都点名要的图表:收入作左轴柱,利润率作右轴线。每个图表暴露 ValueAxis、CategoryAxis、SecondaryValueAxis、SecondaryCategoryAxis 和 SeriesAxis,位置、轴标识、交叉、日期单位、标签间隔、数字格式和可见性都有类型化设置
单类型图表的扁平 API 还在,并映射到组上。ChartType 和 AddSeries 作用于第一个绘图组;SeriesCount 和 Series[] 按顺序遍历每个组的系列,上面的辅助函数因此也看得到线系列。顺序只在一处要紧。TXLSXCharts.Add 创建的图表没有任何绘图组,AddSeries 和 AddPlotGroup 谁先跑谁创建组一。在全新图表上先调 AddPlotGroup(xlsxChartLine, xlsxAxisSecondary),线就成了组一,图表类型切换为线,接着添加的收入系列落进组一。显式创建两个组消除歧义,示例就是这么做的
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、宽、高
Chart.PlotAreaLayout.SetPositionAndSize(0.08, 0.12, 0.80, 0.72);
end;
绘图组上的 ZOrder 控制层叠:图表写入器输出 c:barChart 和 c:lineChart 块前按它排序,值小的在后。因为每个有序组都被建模,打开再保存组合图表时,每个图表族连同其副轴、日期轴和系列轴都保持一体。图表公式引用和缓存的系列值在工作簿复制与开存周期中存活;图表锚点按关系标识符匹配到其图表部件,所以关系条目被别的工具重排过的包仍能把每个图表放到自己的工作表位置
为什么 Office 无视手工布局?
因为坐标到得不齐。只要 c:manualLayout 里出现了 x、y、w、h 中的任何一个,Office 就要求这四个作为完整的一组,否则无视整个元素。模式把四个都标成可选,所以只有 x 和 y 的文件合法,但在 Excel 里什么也不做
该用的调用是 TXLSXChartLayout.SetPositionAndSize。它一步设齐四个值、标记四个 mode 元素在场并启用布局。单个的 X、Y、Width、Height 属性各设一个值,HotXLS 精确写出在场的元素,与读取文件时保留九个可选孩子各自在场状态的做法一致。这让未触碰的布局忠实于源。也意味着靠两次属性赋值搭起来的布局只带两个坐标写出,Office 会跳过它。同一个 TXLSXChartLayout 类支撑着 PlotAreaLayout、TitleLayout、LegendLayout 和每根轴的 TitleLayout
两个默认值跟随 Excel 的做法,而非规范允许的一切。新布局的 XMode、YMode 用 edge,WidthMode、HeightMode 用 factor;标准允许其他组合,但 Excel 保存位置用边、尺寸用系数,保存时把其他形式转换掉。layoutTarget(Target,inner 或 outer)只为绘图区写出;标题、轴标题和图例布局省略它,因为微软对图表模式的实现说明不允许这些元素下出现它
把它放进工作簿往返
图表保留是更广规则的一例:编辑别处产出的文档的库,应当只改被要求改的,其余保持发现时的样子。HotXLS 对主题、计算链和工作表扩展适用同一思路,见 主题、extLst 与 calcChain 的无损往返;对 VBA 工程和外部链接亦然,见 保留 VBA 与外部链接。从零创建图表、放置图片和绘制形状见 HotXLS 中的图表、图片与绘图
HotXLS 从原生 Delphi 与 C++Builder 代码读写 XLSX 图表,机器上无需安装 Excel,图表保真度由库决定而不是由自动化服务器决定。图表 API 参考和试用版在 HotXLS Delphi 电子表格组件页面