HotXLS kann ein Chart direkt auf ein Worksheet setzen, an einen Zellbereich verankert, statt es auf ein dediziertes Chart-Sheet zu legen. In BIFF8-Terminen heißt das: eine Drawing-Shape mit einem OBJ-Record vom Typ 5 schreiben und den Chart-Substream ans Ende des Sheet-Record-Streams parken — exakt das Layout, das Excel produziert, und exakt die Stelle, an der der Reader es erwartet
Der Unterschied zählt für jeden, der operative Reports erzeugt. Ein Chart-Sheet ist ein gutes Zuhause für eine einzelne Schlagzahl-Grafik. Eine monatliche Regionalaufschlüsselung will das Chart neben den Zahlen, die es zusammenfasst, auf demselben Sheet, dimensioniert auf den Zellblock, zu dem es gehört, damit der Leser einmal scrollt statt Tabs zu wechseln und den Kontext zu verlieren
Lesen war schon da, Schreiben nicht
Die Asymmetrie verdient eine Nennung, weil sie die Arbeit prägt. HotXLS konnte eingebettete Charts bereits lesen: Enthält der Worksheet-Record-Stream eine als Chart-Substream markierte BOF, schaltet der Parser den Kontext, sammelt die Chart-Records und übergibt sie beim schließenden EOF an die Drawing-Shape zurück, die der OBJ-Record eingeführt hat. Diesen Pfad hatte jede Excel-erzeugte Arbeitsmappe durchlaufen, die die Bibliothek je geöffnet hat
Es fehlte die Authoring-Seite, und die nützliche Konsequenz: Der neue Writer hatte eine präzise Spezifikation zu treffen — das Byte-Layout erzeugen, das der bestehende Reader bereits wieder anfügt. Es gibt kein besseres Abnahmekriterium für ein Binärformat-Feature als einen unabhängig geschriebenen Reader, den man nicht anfassen darf
Woraus ein eingebettetes Chart besteht
Drei Teile müssen zusammenpassen. Die Drawing-Layer steuert eine Host-Control-Shape bei, die Objekt-Layer einen OBJ-Record, dessen Common Object Data den Objekttyp 5 deklariert, und der Record-Stream den Chart-Substream selbst. Die Options-Flags am OBJ-Record sind die, die Excel für einen Chart-Rahmen schreibt: positioned, locked, automatic line und automatic fill — genau das lässt das eingebettete Chart sich wie ein natives anfühlen, wenn ein Nutzer draufklickt
Der Anker verdient eine Notiz, weil er eine häufige Quelle von Off-by-one-Bugs ist. Die HotXLS-API nimmt einbasierte Zeilen- und Spaltennummern, passend zum Rest der Bibliothek, und der ins File geschriebene Client-Anker ist nullbasiert. Die Konvertierung passiert in AddChartObject, Aufrufer bleiben also im Koordinatensystem, das sie überall sonst benutzen — aber wer einen Hex-Dump gegen den eigenen Aufruf hält, muss sich daran erinnern, auf welcher Seite dieser Grenze er gerade liest
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;
// Verankert auf E2:M20 in diesem Sheet, einbasiert
Sheet.AddChartObject(xlsChartTypeColumn, 'Regional sales',
'Month', 'Amount', Series, 2, 5, 20, 13);
Book.SaveToFile('regional-sales-charted.xls');
finally
Book.Free;
end;
end;
Das FillChar auf dem Series-Array ist kein Schmuck. TXLSChartSeriesInfo trägt mehrere optionale Sub-Records — Datenbeschriftungen, Stil pro Series, Trendlinien und Fehlerbalken, jeweils hinter einem Boolean verschlossen — und ein teilweise initialisierter Record auf dem Stack übergibt dem Emitter Flags, die niemand gesetzt hat. Array nullen, dann die Felder setzen, die man meint
Welche Series-Referenzen akzeptiert der eingebettete Pfad?
Schlichte A1-artige Bereiche innerhalb derselben Arbeitsmappe — und diese Beschränkung ist Absicht, kein Versäumnis. Jede Referenz wird gegen die Sheet-Liste der Arbeitsmappe aufgelöst und in den externen Referenzindex überführt, den die Chart-Records brauchen. Ein benannter Bereich oder eine externe Arbeitsmappen-Referenz fällt auf einen Platzhalter mit einem Ausdruck der Länge null zurück, das Chart schreibt sich also sauber, nur diese eine Series hat keine Datenquelle, bis man sie auf einen Bereich zeigt
Der Grund ist ein schlichter Engineering-Trade-off. Der volle Referenzkompilierungspfad existiert auf der Chart-Sheet-Route, eingewickelt in die Worksheet-Collection-Layer, und ihn sauber herauszulösen hieße, hundert Zeilen Auflösungslogik zu duplizieren für einen Fall, der in der Praxis selten ist. Ein eingebettetes Chart plotted fast immer Zellen auf seinem eigenen Sheet oder einem Geschwister-Datensheet. Benannte und externe Referenzen sind über AddChartSheet auf dem Chart-Sheet-Pfad abgedeckt — nichts fehlt, es wird nur über einen anderen Eingang erreicht
Alles andere im Series-Modell funktioniert auf beiden Routen identisch. Sekundärachsen-Bindung, Linien-, Füll- und Marker-Stile pro Series, Trendlinien, Fehlerbalken und Datenbeschriftungen sind alle Teil von TXLSChartSeriesInfo und werden alle gleich emittiert, sodass eine Chart-Definition zwischen eingebettetem Objekt und Chart-Sheet wandern kann, wobei sich nur der Aufruf ändert. Die Achsengruppen-Mechanik hinter dem Sekundärachsen-Flag beschreibt Sekundärachsengruppen beim BIFF-Write
Warum las sich der Chart-Titel als zwei Zeichen?
Weil eine Zeichenzahl übergeben wurde, wo eine Bytezahl erwartet wurde, und BIFF-Unicode-Strings machen diesen Fehler leicht zu schreiben und schwer zu sehen. Ein kurzer BIFF-Unicode-String beginnt mit einer Zeichenzahl und einem Flags-Byte, und das Flags-Byte trägt das High-Byte-Bit, das sagt, ob die Nutzlast ein oder zwei Byte pro Zeichen hat. Liest man eine 16-Bit-Nutzlast mit der Zeichenzahl, als wäre sie eine Bytelänge, bekommt man exakt die halbe Zeichenkette: Eine Series namens Sales kommt als Sa zurück, und ein Chart-Titel stutzt auf dieselbe Weise, denn Titel und Series-Labels teilen den Dekodierpfad
Was diesen Defekt bemerkenswert macht: Er trat in derselben Record-Familie dreimal auf, einmal bei Trendliniennamen, einmal bei Pivot-Chart-Namen und einmal bei Chart-Titeln. Jeder Vorfall sah wie ein frischer Bug in einem neuen Feature aus. Alle drei waren dieselbe fehlende Multiplikation. Die Regel, die das schließlich beendet hat, ist mechanisch und sollte ohne Ermessen angewandt werden: Wann immer man einen dieser Strings liest, zuerst das High-Byte-Flag konsultieren und die Zeichenzahl mit der Nutzlastbreite multiplizieren, bevor man den Puffer anfasst. Die Record-Details stehen in XLUnicodeString-Zeichenzahlen und das High-Byte-Flag dekodieren
// Das eingebettete Chart teilt die Drawing-Layer mit Bildern und Shapes,
// eine bestehende Zeichnung im Sheet bleibt also erhalten. AddChartObject
// liefert den Index des erzeugten Objekts
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;
Wo eingebettete Charts neben den Alternativen passen
Drei Routen existieren, und sie beantworten verschiedene Fragen. Ein eingebettetes Chart-Objekt gehört neben seine Daten auf ein Worksheet und ist das, was die meisten Reports wollen. Ein Chart-Sheet passt zu einer einzelnen Präsentationsgrafik und gibt den vollen Referenzkompilierungspfad. Ein bestehendes Chart aus einer geladenen Datei unangetastet zu bewahren ist die richtige Antwort, wenn die Arbeitsmappe aus Excel kam, mit Formatierungen, die keine Bibliothek neu interpretieren soll; dieses Pass-Through-Verhalten beschreibt bewahrte ChartML- und Kombinations-Charts
Weil das eingebettete Chart auf der Drawing-Layer reitet, koexistiert es mit Bildern und Shapes auf demselben Sheet, statt sie zu ersetzen, und das allgemeine Modell dieser Layer beschreibt Charts, Bilder und Drawings in HotXLS. Alle drei Routen liegen der HotXLS Delphi spreadsheet component bei, die Wahl dreht sich also darum, wie der Report aussehen soll, nicht darum, was die Bibliothek ausdrücken kann
Der methodologische Punkt ist der, den man behalten sollte. Hat ein Binärformat-Feature einen bestehenden Reader, baut man den Writer gegen den Reader statt gegen die eigene Lesung der Spezifikation. Der Reader kodiert Jahre Kontakt mit Dateien, die echte Anwendungen tatsächlich erzeugt haben, inklusive der Teile, die die Spezifikation vage formuliert, und ein Writer, der ihn befriedigt, befriedigt damit sehr wahrscheinlich auch Excel