Technisch artikel

Chartobjecten insluiten op worksheets met HotXLS

HotXLS kan een grafiek rechtstreeks op een worksheet zetten, verankerd aan een celbereik, in plaats van hem op een speciale chart sheet te plaatsen. In BIFF8-termen betekent dat het schrijven van een drawing shape met een OBJ-record van type 5 en het parkeren van de chart substream aan het einde van de sheet record stream, precies de layout die Excel produceert en precies waar de reader hem verwacht

Het onderscheid telt voor iedereen die operationele rapporten genereert. Een chart sheet is een prima thuis voor één opvallende visual. Een maandelijkse regionale uitsplitsing wil de grafiek naast de cijfers die hij samenvat, op hetzelfde blad, op maat van het cellenblok waartoe hij behoort, zodat een lezer één keer scrollt in plaats van van tab te wisselen en context te verliezen

Lezen kon al, schrijven nog niet

De asymmetrie is het benoemen waard omdat zij het werk vormgeeft. HotXLS kon al embedded charts lezen: bevat de worksheet record stream een BOF die als chart substream is gemarkeerd, dan wisselt de parser van context, verzamelt hij de chart records en geeft hij ze bij de afsluitende EOF terug aan de drawing shape die het OBJ-record introduceerde. Dat pad was belopen door elke workbook die de library ooit opende en die door Excel was gemaakt

Wat ontbrak was de auteurskant, en het nuttige gevolg is dat de nieuwe writer een precieze specificatie had om op te mikken: produceer de bytelayout die de bestaande reader al weer aan elkaar plakt. Er is geen beter acceptatiecriterium voor een binaire formaatfeature dan een onafhankelijk geschreven reader die u niet mocht aanpassen

Waaruit een embedded chart bestaat

Drie stukken moeten met elkaar kloppen. De drawing layer levert een host-control shape, de objectlaag levert een OBJ-record waarvan de common object data object type 5 declareert, en de record stream levert de chart substream zelf. De optieflags op het OBJ-record zijn die welke Excel voor een chart frame schrijft: positioned, locked, automatic line en automatic fill, en dat is wat de embedded chart zich als een native laat gedragen zodra een gebruiker erop klikt

HotXLS verankert een BIFF8 chart substream aan een Delphi-worksheet via drie kloppende stukken: de host-control shape uit de drawing layer, het OBJ-record waarvan de common object data object type 5 declareert, en de chart record chain geparkeerd aan het einde van de sheet record stream, waar een chart BOF de parsercontext wisselt en de afsluitende EOF de records weer aan elkaar plakt
Drie lagen dragen één embedded chart: de drawing shape verankert hem, het OBJ-record typt hem als chart host, en de chart substream aan het einde van de sheet stream levert de records die de reader weer vastplakt

De anchor verdient een kanttekening want hij is een gebruikelijke bron van off-by-one-bugs. De HotXLS API neemt rij- en kolomnummers vanaf één, aansluitend bij de rest van de library, en de client anchor die in de file wordt weggeschreven is nulgebaseerd. De conversie gebeurt binnen AddChartObject, dus aanroepers blijven in het coördinatensysteem dat ze overal anders gebruiken, maar wie een hex dump naast zijn eigen aanroep legt, moet onthouden aan welke kant van die grens hij leest

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;

    // Verankerd aan E2:M20 op dit blad, ééngebaseerd
    Sheet.AddChartObject(xlsChartTypeColumn, 'Regional sales',
      'Month', 'Amount', Series, 2, 5, 20, 13);

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

De FillChar op de series-array is geen versiering. TXLSChartSeriesInfo draagt verschillende optionele sub-records, data labels, per-series stijl, trendlines en error bars, elk achter een booleaanse poort, en een half geïnitialiseerd record op de stack geeft de emitter flags die niemand heeft gezet. Zet de array op nul en zet daarna de velden die u bedoelt

Welke series-verwijzingen accepteert het embedded pad?

Gewone A1-stijl bereiken binnen dezelfde workbook, en die beperking is bewust in plaats van een vergissing. Elke verwijzing wordt geresolved tegen de sheetlijst van de workbook en omgezet in de external reference index die de chart records nodig hebben. Een named range of een verwijzing naar een externe workbook valt terug op een placeholder met een nul-lange geparsde expressie, dus de grafiek schrijft netjes weg maar die ene series heeft geen datasource totdat u hem naar een bereik wijst

HotXLS series-verwijzingsacceptatie op het embedded BIFF8 chart-pad: A1-stijl bereiken zoals Data!$B$2:$B$13 binnen dezelfde workbook worden tegen de sheetlijst geresolved tot de external reference index die de chart records nodig hebben, terwijl named ranges en externe workbook-verwijzingen terugvallen op een placeholder met een nul-lange geparsde expressie, met beide gedekt door AddChartSheet
Alleen gewone A1-stijl bereiken binnen dezelfde workbook compileren tot chart series-verwijzingen; al het andere schrijft netjes weg als placeholder totdat het wordt bijgestuurd, en het volledige pad zit op AddChartSheet

De reden is een zuivere engineeringafweging. Het volledige referentiecompilatiepad bestaat op de chart-sheet-route, verpakt in de worksheet-collection-laag, en het er netjes uittillen zou betekenen dat honderd regels resolutielogica worden gedupliceerd voor een geval dat in de praktijk zelden voorkomt. Een embedded chart plot bijna altijd cellen op zijn eigen blad of een zusterblad met data. Named en external references zijn op de chart-sheet-route gedekt via AddChartSheet, dus niets is onbereikbaar, het wordt alleen via een andere entry point bereikt

Alles andere in het series-model werkt identiek op beide routes. Secondary axis-koppeling, per-series lijn-, vul- en markerstijlen, trendlines, error bars en data labels maken allemaal deel uit van TXLSChartSeriesInfo en worden allemaal op dezelfde manier geëmitteerd, dus een grafiekdefinitie kan wisselen tussen een embedded object en een chart sheet met alleen een andere aanroep. De axis-group-mechaniek achter de secondary axis-flag staat in secondary axis groups bij BIFF-schrijven

Waarom las de grafiektitel als twee tekens?

Omdat een tekenaantal werd doorgegeven waar een byteaantal werd verwacht, en BIFF Unicode strings maken die fout makkelijk te schrijven en moeilijk te zien. Een korte BIFF Unicode string begint met een tekenaantal en een flags-byte, en de flags-byte draagt de high-byte-bit die zegt of de payload één byte per teken is of twee. Lees een 16-bit payload met het tekenaantal alsof het een bytelengte is, en u krijgt precies de helft van de string: een series genaamd Sales komt terug als Sa, en een grafiektitel wordt op dezelfde manier afgekapt omdat titels en series-labels het decodepad delen

Wat dit defect bijzonder maakt, is dat het drie keer terugkwam in dezelfde recordfamilie: een keer in trendline-namen, een keer in pivot chart-namen, en een keer in grafiektitels. Elke keer leek het op een nieuwe bug in een nieuwe feature. Alle drie dezelfde vergeten vermenigvuldiging. De regel die hem uiteindelijk sloot, is mechanisch en moet zonder oordeel worden toegepast: raadpleeg bij het lezen van een van deze strings eerst de high-byte-flag en vermenigvuldig het tekenaantal met de payloadbreedte voordat u de buffer aanraakt. De details op recordniveau staan in XLUnicodeString-tekenaantallen en de high-byte-flag decoderen

// De embedded chart deelt de drawing layer met afbeeldingen en shapes,
// dus een bestaande tekening op het blad blijft bewaard. AddChartObject
// geeft de index van het aangemaakte object terug
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;

Waar embedded charts passen naast de alternatieven

Er zijn drie routes en ze beantwoorden verschillende vragen. Een embedded chart-object hoort naast zijn data op een worksheet en is wat de meeste rapporten willen. Een chart sheet past bij één presentatievisual en geeft het volledige referentiecompilatiepad. Een bestaande grafiek uit een geladen file behouden, onaangeroerd, is het juiste antwoord als de workbook uit Excel komt met opmaak die niemand de library opnieuw wil laten interpreteren; dat pass-through-gedrag staat beschreven in bewaarde ChartML en combination charts

Omdat de embedded chart op de drawing layer rijdt, bestaat hij naast afbeeldingen en shapes op hetzelfde blad in plaats van ze te vervangen, en het algemene model voor die laag staat in charts, afbeeldingen en tekeningen in HotXLS. Alle drie de routes worden geleverd in de HotXLS Delphi spreadsheet component, dus de keuze gaat over hoe het rapport eruitziet en niet over wat de library kan uitdrukken

Het methodologische punt is degene die u moet onthouden. Als een binaire formaatfeature al een bestaande reader heeft, bouw de writer dan tegen de reader aan in plaats van tegen uw lezing van de specificatie. De reader codeert jaren contact met files die echte applicaties werkelijk produceerden, inclusief de delen die de specificatie vaag formuleert, en een writer die daaraan voldoet, voldoet een stuk vaker ook aan Excel