HotXLS умеет класть диаграмму прямо на лист, привязанную к диапазону ячеек, вместо выделения ей отдельного chart sheet. На языке BIFF8 это значит записать drawing shape с OBJ-рекордом типа 5 и припарковать субпоток диаграммы в конец потока рекордов листа, — ровно та раскладка, которую производит Excel, и ровно там, где ридер её ожидает
Это различие важно всякому, кто генерирует операционные отчёты. Chart sheet — неплохой дом для одного заголовочного визуала. Месячный региональный срез хочет диаграмму рядом с числами, которые она суммирует, на том же листе, размером в блок ячеек, к которому она относится, чтобы читатель проскроллил один раз, а не переключал табы и терял контекст
Чтение уже было, записи не было
Асимметрию стоит назвать, потому что она формирует работу. HotXLS уже умел читать встроенные диаграммы: когда поток рекордов листа содержит BOF, помеченный как субпоток диаграммы, парсер переключает контекст, собирает рекорды диаграммы и на закрывающем EOF отдаёт их назад drawing shape, который представил OBJ-рекорд. Этот путь упражнялся каждой книгой, написанной Excel, которую библиотека когда-либо открывала
Не хватало стороны авторинга, и полезное следствие: у нового писателя была точная спецификация, в которую надо попасть, — произвести байтовую раскладку, которую существующий ридер уже умеет присоединять. Лучшего критерия приёмки для фичи бинарного формата, чем независимо написанный ридер, который тебе не дали менять, не существует
Из чего состоит встроенная диаграмма
Три части обязаны сойтись. Слой рисования даёт форму host-контрола, объектный слой даёт OBJ-рекорд, чьи common object data объявляют тип объекта 5, а поток рекордов даёт сам субпоток диаграммы. Флаги опций на OBJ-рекорде — те, что Excel пишет для рамки диаграммы: positioned, locked, автоматическая линия и автоматическая заливка, — именно они делают встроенную диаграмму похожей на нативную, когда пользователь кликает её
Якорь заслуживает примечания, потому что это частый источник багов off-by-one. API HotXLS принимает номера строк и колонок с единицы, как и остальная библиотека, а клиентский якорь, записываемый в файл, — с нуля. Конверсия происходит внутри AddChartObject, так что вызывающие остаются в той системе координат, что используют везде, но всякому, кто сравнивает hex-дамп со своим вызовом, надо помнить, по какую сторону этой границы он читает
var
Book: TXLSWorkbook;
Sheet: TXLSWorksheet;
Series: array[0..1] of TXLSChartSeriesInfo;
begin
Book := TXLSWorkbook.Create(nil);
try
Book.LoadFromFile('regional-sales.xls');
Sheet := Book.Sheets[0];
FillChar(Series, SizeOf(Series), 0);
Series[0].Name := 'Actual';
Series[0].Categories := 'Data!$A$2:$A$13';
Series[0].Values := 'Data!$B$2:$B$13';
Series[0].DataLabels.ShowValue := True;
Series[0].HasDataLabels := True;
Series[1].Name := 'Target';
Series[1].Categories := 'Data!$A$2:$A$13';
Series[1].Values := 'Data!$C$2:$C$13';
Series[1].SecondaryAxis := True;
// Привязан к E2:M20 на этом листе, one-based
Sheet.AddChartObject(xlsChartTypeColumn, 'Regional sales',
'Month', 'Amount', Series, 2, 5, 20, 13);
Book.SaveToFile('regional-sales-charted.xls');
finally
Book.Free;
end;
end;
FillChar на массиве серий — не украшение. TXLSChartSeriesInfo несёт несколько опциональных под-рекордов — подписи данных, стиль серии, линии тренда и планки погрешностей, — каждый под булевым флагом, и частично инициализированная запись на стеке вручит эмиттеру флаги, которые никто не ставил. Обнулите массив, затем выставьте поля, которые имеете в виду
Какие ссылки на серии принимает встроенный путь?
Простые диапазоны в стиле A1 внутри той же книги, и это ограничение намеренное, а не недосмотр. Каждая ссылка резолвится по списку листов книги и превращается в индекс внешней ссылки, который нужен рекордам диаграммы. Именованный диапазон или ссылка на внешнюю книгу уходят в плейсхолдер с парсеным выражением нулевой длины: диаграмма записывается чисто, но у этой конкретной серии нет источника данных, пока вы не наведёте её на диапазон
Причина — прямой инженерный трейд-офф. Полный путь компиляции ссылок существует на маршруте chart sheet, обёрнутый в слой коллекции листов, и аккуратно вытащить его наружу значило бы продублировать сотню строк логики резолвинга ради случая, редкого на практике. Встроенная диаграмма почти всегда рисует ячейки собственного листа или соседнего листа данных. Именованные и внешние ссылки покрыты на пути chart sheet через AddChartSheet, так что ничего не недоступно — просто доходит из другой точки входа
Всё остальное в модели серий работает одинаково на обоих маршрутах. Привязка вторичной оси, стиль линии, заливки и маркеров по серии, линии тренда, планки погрешностей и подписи данных — всё часть TXLSChartSeriesInfo и всё эмитится одинаково, поэтому определение диаграммы может переехать между встроенным объектом и chart sheet, меняя только вызов. Механика axis-group за флагом вторичной оси разобрана в группах вторичной оси при BIFF-записи
Почему заголовок диаграммы прочёлся в два символа?
Потому что туда, где ждали число байт, передали число символов, а BIFF Unicode строки делают эту ошибку лёгкой для написания и тяжёлой для замечания. Короткая BIFF Unicode строка начинается с числа символов и байта флагов, и байт флагов несёт бит high-byte, говорящий, однобайтовый символ в полезной нагрузке или двухбайтовый. Прочитайте 16-битную полезную нагрузку с числом символов так, будто это длина в байтах, — получите ровно половину строки: серия с именем Sales возвращается как Sa, и заголовок диаграммы обрезается так же, потому что заголовки и подписи серий делят путь декодирования
Что делает дефект примечательным, так это то, что он повторился трижды в одном семействе рекордов: раз в именах линий тренда, раз в именах pivot-диаграмм и раз в заголовках диаграмм. Каждый случай выглядел свежим багом в новой фиче. Все три были одним и тем же пропущенным умножением. Правило, закрывшее тему, механическое и должно применяться без суждений: читая любую из этих строк, сначала посмотрите флаг high-byte и умножьте число символов на ширину символа, прежде чем трогать буфер. Детали уровня рекордов — в декодировании числа символов XLUnicodeString и флага high-byte
// Встроенная диаграмма делит слой рисования с изображениями и фигурами,
// так что существующий рисунок на листе сохраняется. AddChartObject
// возвращает индекс созданного объекта
var
ObjIndex: Integer;
begin
ObjIndex := Sheet.AddChartObject(xlsChartTypeLine, 'Trend',
'Week', 'Units', Series, 2, 8, 18, 16);
if ObjIndex < 0 then
raise Exception.Create('chart object was not created');
end;
Где место встроенных диаграмм среди альтернатив
Маршрутов три, и они отвечают на разные вопросы. Встроенный объект-диаграмма принадлежит рядом со своими данными на листе, и это то, чего хочет большинство отчётов. Chart sheet подходит одиночному презентационному визуалу и даёт полный путь компиляции ссылок. Сохранение существующей диаграммы из загруженного файла нетронутой — правильный ответ, когда книга пришла из Excel с форматированием, которое никто не хочет, чтобы библиотека переосмысляла; такое pass-through поведение описано в сохранённых ChartML и комбинированных диаграммах
Поскольку встроенная диаграмма едет на слое рисования, она сосуществует с изображениями и фигурами на том же листе, а не заменяет их, и общая модель этого слоя разобрана в диаграммах, изображениях и рисунках в HotXLS. Все три маршрута поставляются в Delphi spreadsheet-компоненте HotXLS, так что выбор — о том, как должен выглядеть отчёт, а не о том, что библиотека способна выразить
Методологический пункт — тот, что стоит запомнить. Когда у фичи бинарного формата есть существующий ридер, стройте писателя против ридера, а не против вашего прочтения спецификации. Ридер кодирует годы контакта с файлами, которые реальные приложения реально производили, включая части, которые спецификация формулирует вяло, и писатель, удовлетворяющий ридеру, куда вероятнее удовлетворит и Excel