Техническая статья

Редактирование диаграмм Excel без потери ChartML в HotXLS

HotXLS сохраняет исходный ChartML импортированной диаграммы Excel и побайтовно воспроизводит его, когда в типизированной модели ничего не изменилось, поэтому открытие и пересохранение книги не молча обрезает те части диаграммы, которые объектная модель не представляет. Измените заголовок, ряд или ось, и HotXLS структурно вмержит новые типизированные узлы в исходное дерево, а не перегенерирует диаграмму с нуля

Именно это различие — воспроизведение для нетронутого и слияние для изменённого — и делает Delphi-приложение безопасным, чтобы натравить его на диаграммы, собранные аналитиком в Excel. Перегенерация диаграммы из типизированной модели всегда теряет то, чего модель не знает, а диаграммы Excel полны тем, чего библиотека не знает: нестандартные стили, списки расширений, ветви альтернативного содержимого, форматирование с квалификатором пространства имён из надстроек

Почему пересохранение книги уплощает её диаграммы?

Потому что обычная реализация читает то, что понимает, и пишет то, что понимает. Всё, что между ними — extLst из более нового сборки Excel, ветвь mc:AlternateContent, эффект фигуры, для которого в модели нет свойства, — существует в файле, не имеет дома в объектной модели и исчезает при сохранении. Пользователь видит диаграмму, потерявшую градиент, пользовательские подписи данных или 3D-эффект, и книга не может объяснить почему

С PreserveUnsupportedParts, включённым на книге, HotXLS хранит исходные UTF-8 байты диаграммы при импорте вместе с длиной и 64-битным отпечатком смоделированного содержимого. Если к моменту сохранения типизированная модель не изменилась, он записывает исходные байты обратно. Без переупорядочивания XML, без дрейфа пробелов, без резидентной UTF-16 копии диаграммы, висящей в памяти для диаграммы, которую никто не правил

var
  Workbook: TXLSXWorkbook;
  Chart: TXLSXChart;
begin
  Workbook := TXLSXWorkbook.Create(nil);
  try
    Workbook.PreserveUnsupportedParts := True;   // set before Open
    Workbook.Open('quarterly-review.xlsx');
    Chart := Workbook.Sheets[1].Charts[0];   // Sheets[] is 1-based
    if Chart.HasPreservedXml then
      Log('original ChartML retained; untouched charts replay exactly');
    Chart.Title := 'Revenue by region, Q3 2026';  // now the merge path runs
    Workbook.SaveAs('quarterly-review-out.xlsx');
  finally
    Workbook.Free;
  end;
end;

Что слияние сохраняет и чем владеет модель

Правило — владение. Публичные узлы модели — заголовок, ряды, кэши, группы графиков, оси — решаются заново построенным деревом. Не моделированные поддеревья переносятся из оригинала. Эта асимметрия намерена и требует момента на осмысление: удаление ряда не должно воскрешать старый ряд из сохранённого XML, поэтому всё, чем владеет модель, берётся из модели, даже когда модель говорит «исчезло»

Не принадлежащее содержимое повторно вставляется рядом с matched-узлами, рядом с которыми оно стояло, а extLst остаётся последним внутри своего родителя, потому что именно там требует его схема. Очистка известного свойства — снятие флага «жирный», удаление цвета — трактуется как очистка, а не как пробел, который надо заполнить из исходного дерева. Неизвестные атрибуты в той же окрестности сохраняют своё место

Содержимое markup-compatibility получает особую обработку. При разборе HotXLS пропускает mc:Choice и читает mc:Fallback; если эта ветвь fallback оборачивает группу графиков, известную модели, слияние пишет новое типизированное содержимое обратно в поддерево fallback. Без этого шага сохранение выпустило бы новую группу графиков и оставило старую внутри fallback, и диаграмма несла бы две

Отключение одной диаграммы

ClearPreservedXml сбрасывает исходный XML и отпечаток для одной диаграммы, поэтому следующее сохранение перестраивает эту диаграмму чисто из типизированной модели. Это решение на уровне диаграммы; книжный PreserveUnsupportedParts по-прежнему определяет, строится ли состояние сохранения при импорте

Обращайтесь к этому, когда вам нужна чистая диаграмма, а не верная — например, когда диаграмма-шаблон несёт форматирование из старой корпоративной темы и смысл операции в том, чтобы его отбросить. Не обращайтесь к этому как к рефлексу отладки: диаграмма, ошибочно рендерящаяся после правки, чаще вопрос моделирования, а не сохранения, а очистка сохранённого XML разрушает улику

Комбинированные диаграммы и вторая ось

AddPlotGroup строит упорядоченные комбинированные диаграммы, где каждая группа графиков сохраняет своё семейство диаграммы и своё назначение первичной или вторичной оси. Это то, что нужно вездесущей деловой диаграмме: выручка столбцами по левой оси, маржа в процентах линией по правой

Диаграммы предоставляют первичную и вторичную оси категорий и значений плюс ось рядов с типизированными настройками категории, значения, даты и оси рядов, покрывающими позиции, идентификаторы, пересечения, единицы дат, интервалы меток и видимость. Прежний плоский API — ChartType, AddSeries, плоская коллекция Series — по-прежнему адресует первичную группу графиков, поэтому существующий код продолжает работать, тогда как модель под ним держит каждую упорядоченную группу

var
  Chart: TXLSXChart;
  Line: TXLSXChartPlotGroup;
begin
  // Column chart anchored over rows 1..15, columns 5..12
  Chart := Sheet.Charts.Add(xlsxChartColumn, 'Revenue and margin',
    1, 5, 15, 12);
  Chart.AddSeries('Revenue', 'Data!$A$2:$A$13', 'Data!$B$2:$B$13');
  // Second plot group: a line, read against the secondary value axis
  Line := Chart.AddPlotGroup(xlsxChartLine, xlsxAxisSecondary);
  Line.AddSeries('Margin %', 'Data!$A$2:$A$13', 'Data!$C$2:$C$13');
end;

До появления этой модели открытие и пересохранение комбинированной диаграммы схлопывало её в одно семейство и теряло вторичную, дату и ось рядов. Ссылки в формулах диаграмм и кэшированные значения рядов теперь также переживают копии книг и циклы открыть/сохранить, а привязки диаграмм сопоставляются с их частями по идентификатору связи, поэтому приложение, переупорядочивающее записи связей, больше не меняет содержимое диаграмм между позициями листа

Ловушка ручной разметки

Если вы позиционируете область построения, заголовок, подпись оси или легенду вручную, записывайте все четыре координаты. Office требует x, y, w и h как полную группу, когда присутствует хоть одна из них, иначе игнорирует весь manualLayout. HotXLS поэтому всегда выпускает полный прямоугольник, а SetPositionAndSize задаёт и включает его одним вызовом

Ещё две детали следуют Excel, а не полной свободе спецификации. Позиция записывается с xMode и yMode в edge, тогда как размер использует factor для wMode и hMode, потому что именно так пишет Excel и во что он конвертирует прочие комбинации. А layoutTarget выпускается только для области построения, никогда для заголовков или легенд, где заметки реализации Microsoft это запрещают

Как это встаёт в цикл книги

Сохранение диаграмм — один из случаев общей политики: библиотека, редактирующая документы, авторизованные в другом месте, должна менять то, о чём её попросили, и ничего более. Та же логика движет HotXLS на темах, цепочках вычислений и расширениях листа, описанная в заметках о циклах без потерь для темы, extLst и calcChain и о проектах VBA и внешних ссылках — в сохранении VBA и внешних ссылок. Саму авторскую поверхность диаграмм — создание диаграмм, размещение изображений и графики из Delphi — см. в разборе диаграмм, изображений и графики в HotXLS

HotXLS читает, редактирует и пишет диаграммы XLSX из нативного кода Delphi и C++Builder без установленного на машине Excel, что и делает корректность проблемой библиотеки, а не автоматизации — на странице компонента HotXLS есть список возможностей диаграмм и пробная загрузка