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

Вградени chart обекти върху worksheet в HotXLS (BIFF8)

HotXLS може да постави chart директно върху worksheet, анкериран към диапазон от клетки, вместо да го кача на отделен chart sheet. На BIFF8 език това значи писане на drawing shape с OBJ запис от тип 5 и паркиране на chart substream в края на sheet record stream – точно подредбата, която Excel произвежда, и точно мястото, където читателят очаква да го намери

Разграничението има значение за всеки, генериращ оперативни доклади. Chart sheet е добър дом за една-единствена headline визуализация. Месечно регионално разпределение иска chart-а до числата, които обобщава, на същия sheet, размерен към блока клетки, на който принадлежи, така че читателят скролва веднъж, вместо да сменя табове и да губи контекст

Четенето вече имаше, писането – не

Асиметрията заслужава да бъде назована, защото оформя работата. HotXLS вече можеше да чете вградени chart-ове: когато worksheet record stream съдържа BOF, маркиран като chart substream, парсерът сменя контекста, събира chart записите, а на затварящия EOF ги връща на drawing shape-а, въведен от OBJ записа. Този път е упражнен от всяка workbook, писана от Excel, която библиотеката някога е отваряла

Липсваше авторската страна, а полезната последица е, че новият writer имаше прецизна спецификация, по която да цели: произведи байтовата подредба, която съществуващият читател вече прикача обратно. Няма по-добър acceptance критерий за възможност на двоичен формат от независимо написан reader, който не можеш да пипаш

От какво е съставен вграден chart

Три парчета трябва да се разбират. Drawing слойът внася host-control shape, object слоят внася OBJ запис, чиито common object данни декларират object тип 5, а record stream-ът внася самия chart substream. Option флаговете върху OBJ записа са тези, които Excel пише за chart рамка: positioned, locked, automatic line и automatic fill, което кара вградения chart да се държи като нативен, когато потребител го кликне

HotXLS анкерира BIFF8 chart substream към Delphi worksheet чрез три съгласуващи се парчета: host-control shape-а на drawing слоя, OBJ записа, чиито common object данни декларират object тип 5, и веригата chart записи, паркирана в края на sheet record stream, където chart BOF сменя контекста на парсера, а затварящият EOF прикача записите обратно
Три слоя носят един вграден chart: drawing shape-ът го анкерира, OBJ записът го типизира като chart host, а chart substream-ът в края на sheet stream доставя записите, които читателят прикача обратно

Анкерът заслужава бележка, защото е чест източник на off-by-one бъгове. HotXLS API приема едно-базирани номера на редове и колони, съобразно останалата част от библиотеката, а client anchor-ът, записан във файла, е нула-базиран. Конверсията става в AddChartObject, така че викащите остават в координатната система, която ползват навсякъде другаде, но всеки, сравняващ hex dump срещу собственото си извикване, трябва да помни от коя страна на границата чете

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 на този sheet, едно-базирано
    Sheet.AddChartObject(xlsChartTypeColumn, 'Regional sales',
      'Month', 'Amount', Series, 2, 5, 20, 13);

    Book.SaveToFile('regional-sales-charted.xls');
  finally
    Book.Free;
  end;
end;

FillChar-ът върху series масива не е украса. TXLSChartSeriesInfo носи няколко незадължителни под-записа – data labels, стил по серия, trendlines и error bars – всеки gated от булева, а частично инициализиран запис на стека ще подаде на emitter-а флагове, които никой не е задал. Нулирайте масива, после задайте полетата, които имате предвид

Кои series референции приема вграденият път?

Обикновени A1-стил диапазони в същата workbook, и това ограничение е нарочно, а не недоглеждане. Всяка референция се разрешава срещу sheet списъка на workbook-а и се превръща в external reference индекса, от който chart записите се нуждаят. Named range или референция към външна workbook пада обратно на placeholder с parsed expression с нулева дължина, така че chart-ът се записва чисто, но конкретната серия няма източник на данни, докато не я насочите към диапазон

Приемане на series референции в HotXLS на вградения BIFF8 chart път: A1-стил диапазони като Data!$B$2:$B$13 в същата workbook се разрешават срещу sheet списъка в external reference индекса, от който chart записите се нуждаят, докато named range-ове и външни workbook референции падат обратно на placeholder с parsed expression с нулева дължина, като и двете са покрити от AddChartSheet
Само обикновени A1-стил диапазони в същата workbook се компилират в chart series референции; всичко друго се записва чисто като placeholder до пренасочване, а пълният път живее в AddChartSheet

Причината е директен инженерен компромис. Пълният път за компилация на референции съществува на chart-sheet маршрута, опакован в worksheet-collection слоя, а чистото му изваждане би значило дублиране на сто реда resolution логика за случай, необичаен на практика. Вграден chart почти винаги чертае клетки от собствения си sheet или от съседен data sheet. Named и външни референции са покрити на chart-sheet пътя чрез AddChartSheet, така че нищо не е недостъпно, просто стига се от друг вход

Всичко останало в series модела работи еднакво и на двата маршрута. Свързване на secondary axis, стилове на линия, fill и marker по серия, trendlines, error bars и data labels са част от TXLSChartSeriesInfo и се излъчват по един и същ начин, така че chart дефиниция може да мени между вграден обект и chart sheet като се мени само извикването. Axis-group механиката зад secondary axis флага е обхваната в secondary axis групи при BIFF запис

Защо chart заглавието се прочете като два символа?

Защото брояч на символи беше подаден там, където се очакваше брояч на байтове, а BIFF Unicode низовете правят тази грешка лесна за писане и трудна за виждане. Кратък BIFF Unicode низ започва с брой символи и flags байт, а flags байтът носи high-byte бита, казващ дали payload-ът е по един или по два байта на символ. Прочетете ли 16-битов payload с броя символи сякаш е байтова дължина, получавате точно половината низ: серия на име Sales се връща като Sa, а chart заглавие се отрязва по същия начин, защото заглавията и series етикетите споделят декодиращия път

Това, което прави дефекта забележителен, е, че се повтори три пъти в същото семейство записи – веднъж в trendline имена, веднъж в pivot chart имена, веднъж в chart заглавия. Всяко явление изглеждаше като свеж бъг в нова възможност. И трите бяха едно и също липсващо умножение. Правилото, най-после затворило случая, е механично и трябва да се прилага без отсъждане: когато четете някой от тези низове, консултирайте първо high-byte флага и умножете броя символи по ширината на payload-а, преди да пипнете буфера. Записните детайли са в декодиране на XLUnicodeString броя символи и high-byte флага

// Вграденият chart споделя drawing слоя с изображения и фигури,
// така че съществуващо drawing върху sheet-а се запазва. 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-ове срещу алтернативите

Три маршрута съществуват и отговарят на различни въпроси. Вграден chart обект принадлежи до данните си върху worksheet и е това, което повечето доклади искат. Chart sheet пасва на една presentation визуализация и ви дава пълния път за компилация на референции. Запазване на съществуващ chart от зареден файл, недокоснат, е правилният отговор, когато workbook-ът идва от Excel с форматиране, което никой не иска библиотеката да преинтерпретира; това pass-through поведение е описано в запазени ChartML и комбинации от chart-ове

Тъй като вграденият chart язди drawing слоя, той съжителства с изображения и фигури на същия sheet, вместо да ги заменя, а общият модел за този слой е обхванат в chart-ове, изображения и drawings в HotXLS. И трите маршрута ship-ват с HotXLS Delphi spreadsheet компонента, така че изборът е за това как докладът трябва да изглежда, а не за това какво библиотеката може да изрази

Методологичната точка е тази, която си струва да запазите. Когато binary формат възможност има съществуващ читател, стройте writer-а срещу читателя, а не срещу вашето четене на спецификацията. Читателят кодира години контакт с файлове, реално произведени от истински приложения, включително частите, които спецификацията излага разхлабено, и writer, който го удовлетворява, е далеч по-вероятно да удовлетвори и Excel