Technický článek

Vkládání grafových objektů na listy sešitu pomocí HotXLS

HotXLS umí umístit graf přímo na list, ukotvený do rozsahu buněk, místo aby ho odložil na samostatný grafový list. V pojmech BIFF8 to znamená zapsat kreslicí tvar s OBJ záznamem typu 5 a zaparkovat grafový substream na konci proudu záznamů listu, což je přesně to rozložení, které produkuje Excel, a přesně místo, kde ho parser očekává

Ten rozdíl zajímá každého, kdo generuje provozní reporty. Grafový list je slušné bydliště pro jediný vizuál do záhlaví. Měsíční přehled po regionech chce mít graf vedle čísel, která shrnuje, na stejném listu a ve velikosti bloku buněk, do nějž patří, takže si čtenář projede scroll jednou místo přepínání záložek a ztráty kontextu

Čtení už tam bylo, zápis ne

Ta asymetrie stojí za pojmenování, protože určuje podobu práce. HotXLS už embedded grafy číst uměl: když proud záznamů listu obsahuje BOF označený jako grafový substream, parser přepne kontext, posbírá grafové záznamy a při zavírajícím EOF je vrátí kreslicímu tvaru, kterého OBJ záznam představil. Tou cestou prošel každý sešit od Excelu, který knihovna kdy otevřela

Chyběla strana zápisu a užitečný důsledek je, že nový writer měl přesnou specifikaci, kterou má trefit: vyprodukovat bajtové rozložení, které stávající reader už umí znovu připojit. Lepší akceptační kritérium pro featuru binárního formátu, než je nezávisle napsaný reader, který jste nemohli měnit, neexistuje

Z čeho se embedded graf skládá

Tři kusy musí souhlasit. Vrstva kreslení přispěje tvarem host controlu, objektová vrstva OBJ záznamem, jehož common object data deklaruje objektový typ 5, a proud záznamů přispěje samotným grafovým substreamem. Option flags na OBJ záznamu jsou ty, které Excel píše pro rámec grafu: positioned, locked, automatic line a automatic fill, a právě to dělá, že se embedded graf po kliknutí chová jako nativní

HotXLS ukotvuje BIFF8 grafový substream k listu v Delphi přes tři souhlasící kusy: tvar host controlu ve vrstvě kreslení, OBJ záznam, jehož common object data deklaruje objektový typ 5, a řetěz grafových záznamů zaparkovaný na konci proudu záznamů listu, kde grafový BOF přepne kontext parseru a zavírající EOF záznamy znovu připojí
Tři vrstvy nesou jeden embedded graf: kreslicí tvar ho ukotví, OBJ záznam ho typuje jako chart host a grafový substream na konci proudu listu dodá záznamy, které reader znovu připojí

Anchor si zaslouží poznámku, protože je běžným zdrojem off-by-one bugů. API HotXLS bere čísla řádků a sloupců od jedničky, v souladu se zbytkem knihovny, a client anchor zapsaný do souboru je od nuly. Konverze proběhne uvnitř AddChartObject, takže volající zůstávají v souřadném systému, který používají všude jinde, ale kdo srovnává hex dump se svým voláním, musí si pamatovat, kterou stranu té hranice zrovna čte

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;

    // Ukotveno do E2:M20 na tomto listu, číslování od jedničky
    Sheet.AddChartObject(xlsChartTypeColumn, 'Regional sales',
      'Month', 'Amount', Series, 2, 5, 20, 13);

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

FillChar nad polem series není dekorace. TXLSChartSeriesInfo nese několik volitelných sub-záznamů, data labels, per-series styl, trendlines a error bars, každý střežený booleanem, a částečně inicializovaný záznam na stacku podá emitteru flagy, které nikdo nenastavil. Pole nejprve vynulujte a pak nastavte jen ty členy, které skutečně chcete

Které reference na série embedded cesta přijímá?

Prosté rozsahy ve stylu A1 uvnitř téhož sešitu, a toto omezení je úmysl, ne přehlédnutí. Každá reference se vyřeší proti seznamu listů sešitu a převede na external reference index, který grafové záznamy potřebují. Pojmenovaný rozsah nebo reference do externího sešitu spadne na placeholder s parsed expression nulové délky, takže se graf zapíše čistě, ale ta konkrétní série nemá datový zdroj, dokud ji nasměrujete na rozsah

Přijímání referencí na série v HotXLS na embedded cestě BIFF8 grafu: rozsahy ve stylu A1 jako Data!$B$2:$B$13 uvnitř téhož sešitu se vyřeší proti seznamu listů na external reference index, který grafové záznamy potřebují, zatímco pojmenované rozsahy a reference do externích sešitů spadnou na placeholder s parsed expression nulové délky, obojí pokryté přes AddChartSheet
Do referencí na série grafu se zkompilují jen prosté rozsahy ve stylu A1 uvnitř téhož sešitu; všechno ostatní se zapíše čistě jako placeholder, dokud se nepřesměruje, a plná cesta bydlí na AddChartSheet

Důvodem je prostý inženýrský trade-off. Plná cesta kompilace referencí existuje na route grafového listu, zabalená ve vrstvě kolekce worksheetů, a vyndat ji čistě ven by znamenalo duplikovat sto řádků resolution logiky pro případ, který je v praxi neobvyklý. Embedded graf skoro vždy plotuje buňky na vlastním listu nebo na sousedním datovém listu. Pojmenované a externí reference pokrývá cesta grafového listu přes AddChartSheet, takže nic není nedostupné, jen se to dosáhne z jiného vstupního bodu

Všechno ostatní v modelu series funguje na obou routách identicky. Vázání na sekundární osu, per-series styl čáry, výplně a markerů, trendlines, error bars a data labels jsou všichni součástí TXLSChartSeriesInfo a emitují se stejným způsobem, takže definice grafu může přejít mezi embedded objektem a grafovým listem se změnou jen volání. Mechaniku axis groups za flagem sekundární osy pokrývá článek sekundární axis groups při BIFF zápisu

Proč se titulek grafu načetl jako dva znaky?

Protože se počet znaků četl tam, kde se čekal počet bajtů, a BIFF Unicode strings tenhle omyl umožňují napsat snadno a vidět těžko. Krátký BIFF Unicode string začíná počtem znaků a flags bytem a flags byte nese high-byte bit říkající, jestli payload je jeden bajt na znak, nebo dva. Přečtete-li 16bitový payload s počtem znaků, jako by to byla délka v bajtech, dostanete přesně polovinu stringu: series jménem Sales se vrátí jako Sa a titulek grafu se usekne stejně, protože titulky a labely serií sdílí dekódovací cestu

Tenhle defekt je pozoruhodný tím, že se třikrát opakoval ve stejné rodině záznamů, jednou ve jménech trendline, jednou ve jménech pivot chart a jednou v titulcích grafů. Každý výskyt vypadal jako čerstvý bug v nové featuře. Všechny tři byly tentýž chybějící násobek. Pravidlo, které to nakonec uzavřelo, je mechanické a má se aplikovat bez úsudku: kdykoli čtete jeden z těchhle stringů, nejdřív se podívejte na high-byte flag a vynásobte počet znaků šířkou payloadu, než sáhnete na buffer. Detaily na úrovni záznamů pokrývá článek dekódování počtů znaků XLUnicodeString a high-byte flagu

// Embedded graf sdílí vrstvu kreslení s obrázky a tvary, takže
// existující kresba na listu zůstane zachována. AddChartObject
// vrací index vytvořeného objektu
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;

Jak se embedded grafy staví proti alternativám

Routy jsou tři a odpovídají na různé otázky. Embedded chart object patří vedle svých dat na worksheet a to je, co většina reportů chce. Grafový list sedí na jediný vizuál pro prezentaci a dává vám plnou cestu kompilace referencí. Zachovat existující graf z načteného souboru nedotčený je správná odpověď, když sešit přišel z Excelu s formátováním, které nikdo nechce, aby knihovna přeinterpretovala; tohle pass-through chování popisuje článek zachované ChartML a combination charts

Protože embedded graf jede po vrstvě kreslení, koexistuje s obrázky a tvary na témže listu místo aby je nahrazoval, a obecný model té vrstvy pokrývá článek grafy, obrázky a kresby v HotXLS. Všechny tři routy se dodávají v HotXLS Delphi spreadsheet komponentě, takže volba je o tom, jak má report vypadat, ne o tom, co knihovna dokáže vyjádřit

Metodický point je ten, který stojí za zapamatování. Když má featura binárního formátu existující reader, stavějte writer proti readeru, ne proti svému čtení specifikace. Reader kóduje roky kontaktu se soubory, které reálné aplikace skutečně produkovaly, včetně částí, které specifikace popisuje volně, a writer, který reader uspokojí, má mnohem větší šanci uspokojit i Excel