HotXLS schreibt BIFF8-Sekundärachsgruppen, indem es einen zweiten AxisParent-Block ausgibt und nicht eine zweite Chart-Gruppe hinter den Achsen anhängt. Im klassischen XLS-Chart-Substream liegt jede Chart-Gruppe – ChartFormat, der Charttyp-Record und CrtLink – innerhalb ihres eigenen Achsgruppenblocks, und jede Serie bindet sich über SerToCrt an eine solche Gruppe. Diese Verschachtelung zu vertauschen, lässt eine Serie ohne zweite Chart-Gruppe, an die sie gebunden werden könnte, unabhängig davon, wie viele Achsrecords ausgegeben werden
Warum bindet eine zweite Chart-Gruppe hinter den Achsen an nichts?
Die Grammatik ist die ganze Antwort, und sie steht in einer Zeile ABNF. Die Regel CHARTFOMATS aus [MS-XLS] 2.1.7.20.1 lautet AxesUsed 1*2AXISPARENT und definiert danach AXISPARENT = AxisParent Begin Pos [AXES] 1*4CRT End. Liest man beide Produktionen zusammen, ergibt sich die Form: Chart-Gruppen sind Kinder einer Achsgruppe und keine Geschwister. Ein Chart-Substream mit zwei Achspaaren und einer nachgestellten Chart-Gruppe ist kein Dual-Axis-Chart mit einer Layoutbesonderheit, sondern ein Chart mit einer Chart-Gruppe und verwaisten Achsrecords. Das ist wichtig, weil SerToCrt ($1045) innerhalb des SERIESFORMAT-Blocks einen nullbasierten Index der Chart-Gruppe und keinen Achsindex trägt. Wird crt = 1 geschrieben, obwohl nur ein CRT-Block existiert, zeigt die Serie auf eine nie ausgegebene Chart-Gruppe. Die Intuition, die hier viele in die Irre führt, steckt im Recordnamen: AXESUSED klingt, als zähle er Achsen, daher scheint es naheliegend, mehr Achsen auszugeben. Er zählt Achsgruppen, und jede Achsgruppe bringt eine vollständige Plotfläche und Chart-Gruppe mit
Eine Serie für die sekundäre Achsgruppe markieren
Auf der HotXLS-Seite reduziert sich das auf ein Boolean. TXLSChartSeriesInfo trägt ein Feld SecondaryAxis; wird es für eine Serie im Array gesetzt, das Sie an TXLSWorksheets.AddChartSheet übergeben, schaltet der gesamte Builder in den Zwei-Gruppen-Modus. Es gibt keinen separaten Aufruf zum Aktivieren der Sekundärachse und keinen Achsanzahlparameter, denn die Anzahl lässt sich ableiten: Will irgendeine Serie die sekundäre Gruppe, benötigt das Chart zwei
var
Wb: TXLSWorkbook;
Series: array [0..1] of TXLSChartSeriesInfo;
begin
Wb := TXLSWorkbook.Create;
try
Wb.Sheets.Add.Name := 'Data';
// ... A1:C12 mit Kategorien, Umsatz und Marge füllen ...
Series[0] := Default(TXLSChartSeriesInfo); // diesen Record nie mit FillChar leeren
Series[0].Name := 'Revenue';
Series[0].Categories := 'Data!$A$1:$A$12';
Series[0].Values := 'Data!$B$1:$B$12';
Series[1] := Default(TXLSChartSeriesInfo);
Series[1].Name := 'Margin';
Series[1].Categories := 'Data!$A$1:$A$12';
Series[1].Values := 'Data!$C$1:$C$12';
Series[1].SecondaryAxis := True; // AXESUSED wird 2
Wb.Sheets.AddChartSheet('Dual Axis', xlsChartTypeLine,
'Revenue vs Margin', '', '', Series);
Wb.SaveAs('dual-axis.xls', xlExcel97);
finally
Wb.Free;
end;
end;
Die Zeile Default(TXLSChartSeriesInfo) ist keine Dekoration. TXLSChartSeriesInfo mischt verwaltete Felder – die WideString-Namen, dynamische Trendline- und Error-Bar-Arrays – mit einfachen Boolean-Membern, und Delphi garantiert nur, dass die verwalteten Felder für Sie gelöscht werden. Lässt man SecondaryAxis uninitialisiert, enthält es, was zufällig auf dem Stack lag. In der Praxis kann derselbe Binärcode aus einem Konsolenhost ein Einachsen-Chart und unter dem Test Runner ein Dual-Axis-Chart erzeugen. Die Kategorien- und Wertebereiche werden dagegen über die EXTERNSHEET-Tabelle der Arbeitsmappe aufgelöst, bevor der Builder sie sieht – dieselbe Indexmechanik wie bei der Klassifizierung externer BIFF-SupBook- und XTI-Links durch HotXLS. Ein Bereich mit unbekanntem Sheet wird dabei zu einem leeren Platzhalter BRAI, statt den Build scheitern zu lassen
Was HotXLS ausgibt, wenn eine Serie sekundär ist
Der Emitter verändert seine Form und nicht nur einen Wert. Ohne sekundäre Serie enthält AXESUSED den Wert 1 – oder 0 bei Pie und 3D-Pie, die überhaupt keine Achsgruppen besitzen –, und es folgt ein AxisParent-Block. Mit einer sekundären Serie enthält AXESUSED den Wert 2, und der Builder durchläuft den Block zweimal. iax, das erste Wort des 18 Byte langen Payloads von AxisParent ($1041), wird dabei zuerst auf 0 und dann auf 1 gesetzt. Jeder Durchlauf gibt Pos, die Kategorieachse und die Wertachse (Axis, $101D), den PlotArea-Marker ($1035), einen Default-Frame, anschließend ChartFormat ($1014), den Charttyp-Record, CrtLink ($1022) und zwei End-Marker zum Schließen von Chart- und Achsgruppe aus. Die sekundäre Serie bindet sich danach mit SerToCrt crt = 1, die primäre behält crt = 0. Ein Record wird absichtlich nicht dupliziert: Die Legende wird nur in der ersten Gruppe ausgegeben, weil Excel einem Chart unabhängig von der Anzahl seiner Achsgruppen genau eine Legende gibt. Zwei weitere Eigenschaften sollte man ausdrücklich nennen. Die Umstrukturierung des Emitters, bei der die Chart-Gruppe in den Axis-Parent-Block verschoben wurde, ändert die Ausgabe gewöhnlicher Charts nicht. Ohne sekundäre Serie ist der Substream byteidentisch mit der vorherigen Version, denn AddAxisParent mit iax = 0 zu parametrisieren, ist genau der alte Codepfad. Außerdem gibt der Builder je Gruppe ein vollständiges Achspaar aus; eine sekundäre Gruppe erhält daher immer ihre eigene Kategorieachse, selbst wenn nur ihre Werteskala interessiert
Wie rekonstruiert die Chartinspektion die Bindung an eine Achsgruppe?
Das Lesen erfolgt in zwei Durchläufen über die Recordliste, und es muss so sein, weil AXESUSED vor den von ihm beschriebenen Blöcken kommt. Der erste Durchlauf sucht nur nach $1046 und liest dessen erstes Wort als Anzahl der Achsgruppen. Der Wert beginnt bei 1 und wird nur erhöht, nie verringert: HotXLS nimmt das Maximum aus aktuellem Wert und deklarierter Zahl, sodass ein fehlerhafter oder doppelter AXESUSED-Record ein Chart, bei dem bereits zwei Gruppen erkannt wurden, nicht wieder zurückstufen kann. Der zweite Durchlauf verfolgt die aktuelle Achsgruppe, aktualisiert sie bei jedem AxisParent und stempelt diesen Index auf jeden Axis-Record, bis der nächste AxisParent erscheint
var
Model: TXLSChartModel;
i: Integer;
begin
// Sheets[1] ist das Daten-Worksheet, Sheets[2] das Chart-Sheet
Model := Wb.Sheets[2]._Chart.GetChartModel;
try
if Model.AxisGroupCount = 2 then
Writeln('AXESUSED declares a secondary axis group');
for i := 0 to Model.AxisCount - 1 do
Writeln('axis ', i, ' group ', Model.GetAxis(i).AxisGroup);
for i := 0 to Model.SeriesCount - 1 do
Writeln('series ', i, ' chart group ', Model.GetSeries(i).ChartGroup);
finally
Model.Free;
end;
end;
Zwei Grenzen sollte man benennen. TXLSChartModel.AxisGroupCount meldet, was die Datei deklariert, nicht wie viele AxisParent-Blöcke tatsächlich gefunden wurden. Eine Datei, die 2 sagt und nur einen Block liefert, meldet 2; bei AxisCount fällt der Unterschied auf. Und TXLSChartAxis.AxisGroup ist ein Positionsstempel: Er vermerkt, in welchem gelesenen Block eine Achse lag, und genau das ist alles, was das Format mitteilt. Auf der Serverseite wird die Dekodierung von SerToCrt daran gebunden, dass man sich in einem Series-Block befindet, weil dieselbe Record-ID in Kontexten vorkommt, in denen sie keine Serienbindung ist. Ein ungebundener Decoder würde damit bereitwillig die falsche Serie überschreiben
Sekundärachsen ohne echte Excel-Datei prüfen
Die Prüfung benötigte keine Excel-Datei mit einer Sekundärachse, und genau das ist der nützliche Teil. Strukturelle Dekodierung ist eine Eigenschaft der Recordfolge; eine synthetisierte Folge beweist sie genauso genau wie eine aufgezeichnete. Die Regression baut AXESUSED mit einem Payload von 2, danach zwei AxisParent-Blöcke, die jeweils Kategorie- und Wertachse umschließen, und prüft, dass das Modell mit AxisGroupCount = 2, vier Achsen mit den Stempeln 0, 0, 1, 1 und den erwarteten Achsentypen am zweiten Paar zurückkommt
// Strukturelle Prüfung ganz ohne Excel-Datei
Chart := TXLSCustomChart.Create(nil, $0600);
try
AddWordRecord($1046, [2]); // AXESUSED: zwei Achsgruppen
AddAxisParentGroup(0); // AxisParent iax=0 + Begin + 2 Axis + End
AddAxisParentGroup(1); // AxisParent iax=1 + Begin + 2 Axis + End
Model := Chart.GetChartModel;
Assert.AreEqual(2, Model.AxisGroupCount);
Assert.AreEqual(4, Model.AxisCount);
Assert.AreEqual(0, Model.GetAxis(1).AxisGroup);
Assert.AreEqual(1, Model.GetAxis(2).AxisGroup);
finally
Model.Free;
Chart.Free;
end;
Zwei praktische Hinweise für eigene synthetische Records. TXLSCustomChart.AddData(RecID, Len, nil) dereferenziert den Payload, wenn Len ungleich null ist. Die Marker Begin ($1033) und End ($1034) müssen daher mit Länge null hinzugefügt werden, nicht mit einem Nil-Blob und veralteter Länge. Außerdem beweist eine synthetische Folge den Decoder, niemals die Akzeptanz Ihrer Ausgabe durch Excel. Die Schreibseite wurde aus der ABNF strukturiert und anschließend per Roundtrip über GetChartModel geprüft, mit Achsstempeln 0/0/1/1 und Serien-Chart-Gruppen 0/1; der byteidentische Pfad ohne Sekundärachse dient als Sicherheitsnetz. Das entspricht der konservativen Haltung der übrigen HotXLS-Unterstützung für Charts, Bilder und Zeichnungen in Delphi: Dekodiert wird, was die Records sagen, und ein nicht gelesener binärer Aufbau wird nicht erraten. Die vollständige Delphi-Suite lief nach der Implementierung des Schreibpfads auf Win32 und Win64 mit 1650 von 1650 Tests durch
Chart3d-Szenenparameter und die fAuto-Falle
Zwei benachbarte Details treffen jeden, der über das Standardchart hinausgeht. Das erste ist Chart3d ($103A, [MS-XLS] 2.4.46), ein flacher 14-Byte-Payload, der bei 3D-Varianten innerhalb der Chart-Gruppe ausgegeben wird: anRot (Rotation, 0 bis 360), anElev (Elevation, vorzeichenbehaftet, -90 bis 90), pcDist (Perspektivabstand, 0 bis 100, ignoriert, solange fPerspective nicht gesetzt ist), pcHeight und pcDepth (Prozent der Chartbreite, 5 bis 500), pcGap (0 bis 500) und ein grbit mit den Bits fPerspective $0001, fCluster $0002, fAutoscale $0004, f3DScaling $0010 und f2DWalls $0020. Die Spezifikation fügt Bedingungen hinzu, die das Recordlayout nicht für Sie erzwingt: Bei einem transponierten Balkendiagramm dürfen anRot und anElev 44 nicht überschreiten, und bei einem Pie-Chart darf anElev nicht negativ sein
Das zweite Detail ist das Bit fAuto, und es erzeugt den Fehlerbericht "my colors were ignored". LineFormat ($1007), AreaFormat ($100A) und MarkerFormat ($1009) tragen fAuto alle in Bit 0 ihres grbit. Ist das Bit gesetzt, verwendet Excel seinen automatischen Stil und behandelt die danebenliegenden expliziten RGB-Werte, den Linienstil sowie Form und Größe des Markers als Dekoration. Jeder Emitter, der einen eigenen Serienstil schreibt, muss Bit 0 löschen; die Standard-Emitter lassen es absichtlich gesetzt, damit Excel die Palette auswählt. Bearbeiten Sie statt des Aufbaus eine vorhandene Arbeitsmappe, gelten andere Erhaltungsregeln, die in der Bearbeitung von Excel-Charts ohne Verlust von ChartML beschrieben sind
Sekundärachsgruppen, die SerToCrt-Bindung und das hier gezeigte typisierte Chartmodell gehören zur HotXLS Delphi spreadsheet component für Delphi und C++Builder, die BIFF8-Charts ohne installierte Excel-Anwendung liest und schreibt; die Produktseite enthält die vollständige Referenz der Chart-Records und die Überladungen von AddChartSheet