Technischer Artikel

Verbundene Zellen und Berichtsvorlagen in Delphi mit HotXLS

Wer die Zellen einer frisch geöffneten Berichtsvorlage durchläuft, erlebt einen verbundenen Titel wie ein Leck. Man liest A1 und bekommt „Quarterly Statement"; man liest B1 bis F1, die sichtbar unter demselben Banner liegen, und bekommt nichts. Schreibt man einen Wert in C1, um die Kopfzeile zu korrigieren, erscheint er nie auf dem Bildschirm. Das Raster hat die Daten nicht verloren. Es tut genau das, was ein Verbund bedeutet: In XLS wie in XLSX rendert ein verbundenes Rechteck den Inhalt einer Zelle, des Ankers oben links, und behandelt den Rest als überdeckten Raum, der Werte hält, sie aber nie anzeigt. Excel-Nutzer verinnerlichen das durch Versuch und Irrtum. Ein Berichtsgenerator muss es als Regel kodieren, denn in generiertem Code ist das Symptom ein leerer Bereich ohne Exception, auf die man es zurückführen könnte. HotXLS, eine native Object-Pascal-Bibliothek, die beide Excel-Formate aus Delphi und C++Builder liest und schreibt, legt die Verbundtabelle explizit genug offen, dass Sie gegen die Regel programmieren können, statt sie in einem Support-Ticket neu zu entdecken

Ein Wert, ein Anker

Ein Verbund ist eine Anzeigeanweisung, die über ein Raster gelegt wird, das seine Form nicht ändert. Jede überdeckte Zelle existiert in der Datei weiterhin als eigener Platz; der Verbund-Record sagt einem Abnehmer nur, den Inhalt des Ankers über das Rechteck zu malen. Diese Unterscheidung treibt drei Verhaltensweisen, die man verinnerlichen sollte, bevor man Layout-Code schreibt. Das Lesen einer überdeckten Zelle gibt ihren eigenen gespeicherten Wert zurück, der für ein selbst gebautes Banner meist leer ist, sodass jeder Code, der einen verbundenen Titel untersucht, den Anker auflösen und lesen muss. Das Schreiben in eine überdeckte Zelle gelingt auf Dateiebene und zeigt sich nirgends, was die Falle der unsichtbaren Kopfzeile aus der Einleitung ist. Und das Aufheben eines Verbunds legt frei, was die ganze Zeit darunter lag, sodass ein verirrter Wert im überdeckten Raum an dem Tag zum sichtbaren Defekt wird, an dem jemand den Verbund auflöst

Diagramm eines verbundenen HotXLS-Banners, bei dem überdeckte Zellen ihre eigenen Plätze behalten, während Lesezugriffe in Delphi-Tabellen auf den Anker A1 aufgelöst werden
HotXLS behält jede überdeckte Zelle als echten Platz und zeichnet nur den Anker, sodass Lesezugriffe über A1 aufgelöst werden, während Schreibzugriffe in den überdeckten Raum bis zu einer Verbundaufhebung unsichtbar bleiben

Auf der XLSX-Seite ist diese Tabelle ein vollwertiges Objekt. Sheet.MergedCells bietet Add('A1:C1'), FindAt(Row, Col), DeleteAt und Items, und der Aufruf, zu dem man am häufigsten greift, ist FindAt: Man übergibt ihm eine beliebige Koordinate, und es gibt den verbundenen Bereich zurück, der diese Zelle überdeckt, oder nil, wenn die Zelle allein steht. Dieses eine Nachschlagen ist die Grundlage für beide Hälften korrekter Verbundbehandlung, das sichere Lesen und die Schreibsperre, und beide tauchen später auf

Zwei Fassaden, zwei Verbund-Idiome

HotXLS hält die klassische BIFF8-Engine für .xls und die OOXML-Engine für .xlsx als getrennte Objektmodelle, und sie schreiben das Verbinden verschieden, weil sie von verschiedenen Konventionen abstammen. Die XLS-Fassade folgt dem Excel-COM-Idiom: Man nimmt einen Bereich aus einer indizierten Eigenschaft mit zwei Argumenten und ruft Merge mit einem OleVariant auf, dessen Wert die Geometrie bestimmt, die man am Ende erhält

var
  Book: IXLSWorkbook;   // referenzgezählt: kein manuelles Free
  Sh: IXLSWorksheet;
begin
  Book := TXLSWorkbook.Create;
  Sh := Book.Sheets[1];                 // die XLS-Blattsammlung ist 1-basiert
  Sh.Range['A1', 'F1'].Merge(False);    // False = ein verbundener Block
  Sh.Cells.Item[1, 1].Value := 'Quarterly Statement';
  Sh.Range['A3', 'F4'].Merge(True);     // True = zeilenweise verbinden: ein Verbund je Zeile
  Book.SaveAs('layout.xls');
end;

Das Argument von Merge ist der Teil, den man falsch macht. Über einen zweizeiligen Bereich erzeugt Merge(True) zwei unabhängige einzeilige Verbünde, was Excels „Zeilenweise verbinden" entspricht und genau das ist, was man für ein gestapeltes Kopfband will, dessen Zeilen trennbar bleiben sollen. Merge(False) verschmilzt das gesamte Rechteck zu einem einzigen Block. Der Bereich meldet außerdem MergeCells als Zustandsflag, gibt den umgebenden Bereich über MergeArea zurück und löst sich mit Unmerge auf. Die XLSX-Fassade stellt dieselben Operationen unter anderen Namen bereit: Sheet.MergeCells(Row1, Col1, Row2, Col2) nimmt ganzzahlige Grenzen, TXLSXRange.Merge akzeptiert die entsprechende Across-Variante, und die Sammlung MergedCells hält das Ergebnis

Eine Vorlage, die mit ihren Daten wächst

Eine echte Berichtsvorlage ist kein festes Raster. Kopf und Summen sind fest, aber der Detailabschnitt dazwischen dehnt sich auf das aus, was die Abfrage liefert. Das Muster, das sich bewährt, hält eine vollständig gestaltete Detailzeile in der Vorlage, klont sie einmal je Datensatz und öffnet dann vor dem Summenblock eine Lücke, sodass alles darunter Verankerte nach unten rutscht, ohne seine Formatierung zu verlieren

HotXLS-Berichtsvorlage, die in Delphi wächst: die gestaltete Detailzeile wird je Datensatz geklont, und InsertRows öffnet eine Lücke, sodass der Summenblock mit intakten Verbünden nach unten rutscht
Das Klonen der gestalteten Detailzeile trägt ihre Stile und Formeln in jede Kopie, und InsertRows schiebt dann das Summenband mit intakten Verbünden und Formaten nach unten
Sheet.Range['A1:F1'].Merge;
Sheet.Cells[1, 1].Value := 'INVOICE #2026-0611';    // der Wert geht an den Anker, A1
Sheet.RowHeight[1] := 28;
TitleFont := Book.Fonts.Add('Calibri', 16, True, False);
Sheet.Cells[1, 1].FontIndex := TitleFont + 1;        // Pool-Index 0-basiert, Zellseite 1-basiert

// Zeile 5 ist die gestaltete Detail-Vorlagenzeile
for I := 0 to ItemCount - 1 do
  Sheet.CopyRange(5, 1, 5, 6, 6 + I, 1);             // Stile und Formeln reisen mit

// eine Lücke über dem Summenblock öffnen; Inhalt darunter rückt nach unten
Sheet.InsertRows(6 + ItemCount, 1);
Sheet.Range['A1:F1'].SetBorders(xlsxEdgeOutline, xlsxBorderMedium);

Zwei Zeilen lohnen einen zweiten Blick. Die Schriftzuweisung trägt einen Um-eins-Fehler, der stillschweigend zubeißt: Fonts.Add gibt eine 0-basierte Pool-Position zurück, während eine Zelle eine 1-basierte Schriftreferenz speichert, bei der 0 die Standardschrift bedeutet, sodass das Weglassen des + 1 nichts auslöst, sondern den Titel nur in der falschen Schrift gestaltet. Die andere Zeile ist CopyRange, das Formatierung und Formeln zusammen mit den Werten bewegt. Das ist der ganze Grund, eine handgebaute Vorlagenzeile zu klonen, statt ihr Aussehen im Code nachzubauen. Ein Designer besitzt das Erscheinungsbild einmal, in der Vorlage; der Generator gießt nur noch Daten in Kopien davon

Diese Aufteilung skaliert weiter, wenn das wiederverwendbare Layout in einer eigenen Arbeitsmappe lebt, etwa einem Blatt mit Kopf- und Fußbändern, das berichtsübergreifend geteilt wird. CopyRangeTo führt denselben Klon über Arbeitsblattgrenzen hinweg aus, nimmt ein Zielblatt plus Zielkoordinaten, sodass ein Generator ein einziges unberührtes Vorlagenblatt halten und seine Bereiche in so viele Ausgabeblätter stempeln kann, wie ein Job braucht. Die Alternative, die Vorlage an Ort und Stelle zu verändern und hinterher wiederherzustellen, ist die Art von Vorgehen, die funktioniert, bis ein Lauf auf halbem Weg abbricht

Was InsertRows bewegt und was nicht

Das Muster der wachsenden Vorlage funktioniert nur, weil InsertRows bei XLSX eine strukturelle Bearbeitung ist und kein Zellen-Umsortieren. Wenn es eine Lücke öffnet, verlagert es die verbundenen Bereiche, Zeilenhöhen, Hyperlinks, Kommentare, fixierten Fenster, AutoFilter-Bereiche, bedingten Formate, Datenüberprüfungen, Tabellen, definierten Namen, Bildanker und Diagrammanker, die unter dem Einfügepunkt liegen, nicht nur die Zellwerte. Das ist es, was den Summenblock mit intakten Verbünden und Zahlenformaten in seiner neuen Zeile ankommen lässt, statt entkleidet

Seine zwei dokumentierten Grenzen sind die, um die man herum entwerfen sollte. Die Formelanpassung ist auf das bearbeitete Blatt beschränkt: Referenzen innerhalb dieses Blatts werden umgeschrieben, und eine Formel auf einem anderen Blatt, die in den verschobenen Bereich zeigt, wird ebenfalls umgeschrieben, aber die Anpassung folgt nur Referenzen, die auf das bearbeitete Blatt zielen, sodass jedes arbeitsmappenübergreifende Referenzschema ein eigenes Audit verdient statt blindes Vertrauen. Die zweite Grenze ist schärfer, und sie liegt auf der XLS-Seite. PivotTables überstehen Öffnen-Speichern-Zyklen als rohe erhaltene Records, nicht als modellierte Objekte, die HotXLS bewegen kann, sodass das Einfügen von Zeilen den Fußabdruck einer Pivot nicht verlagert. Jede Vorlage, die Sie für das .xls-Format bauen, sollte ihre Pivot-Bereiche weit entfernt von jedem Band parken, das wächst

Sich weigern, Daten in Layout-Raum zu schreiben

Der Fehler mit verbundenen Zellen, der es tatsächlich in die Produktion schafft, ist nicht der kosmetische. Er ist strukturell: Eine Detailzeile driftet in ein verbundenes Layout-Band, ihre Werte landen in überdeckten Zellen und werden unsichtbar, und die Spaltensummen stimmen stillschweigend nicht mehr mit dem überein, was jemand beim Lesen des Blatts sehen kann. Weil FindAt die Frage nach dem überdeckenden Bereich für jede Koordinate beantwortet, kann der Generator diesen Schreibvorgang in dem Moment verweigern, in dem er stattfinden würde, statt einen Bericht auszuliefern, der stillschweigend zu wenig zählt

// das Schreiben von Detaildaten in einen verbundenen Layout-Bereich verweigern
if Sheet.MergedCells.FindAt(Row, 1) <> nil then
  raise Exception.CreateFmt('row %d overlaps a merged layout region', [Row]);
Sheet.Cells[Row, 1].Value := Detail.Description;

Dieselbe Grenzprüfung gehört überall dorthin, wo ein Nutzer die Ausgabe später sortieren oder filtern wird. Ein Bereich mit Verbünden darin lässt sich nicht sauber sortieren, weil das Sortieren Zeilen unabhängig bewegt und ein zeilenübergreifender Verbund keine einzelne Zeile hat, mit der er reisen könnte; Excel antwortet mit einem Fehler oder einem durcheinandergewürfelten Layout. Die Disziplin, die Berichte korrekt hält, ist geografisch. Verbünde auf Titelbänder, Abschnittstrenner und Unterschriftsblöcke beschränken und die tabellarische Mitte des Blatts flach halten. Der Artikel zur vorlagenbasierten Berichtserzeugung entwickelt diese Trennung von Layout und Daten zu einem vollständigen platzhaltergetriebenen Workflow, und der Artikel zu bedingter Formatierung und Rich Text behandelt die Gestaltung dieses flachen Datenbands

Wie Verbünde auf dem Weg nach draußen degradieren

Ein Verbund ist ein Arbeitsmappen-Konzept, und jedes textorientierte Exportformat beachtet ihn in unterschiedlichem Maß. Die drei Verhaltensweisen vorab zu kennen spart einen QA-Zyklus. Der HTML-Export reproduziert Verbünde getreu und gibt colspan und rowspan in einer einzelnen Tabelle aus, sodass ein browsergebundener Bericht sein Bandlayout behält. Der RTF-Export überspannt überhaupt keine Spalten: Der Ankertext landet in seiner eigenen Zelle, und die restliche Breite des Verbunds kommt als leere Zellen heraus, was einen breiten Titel in einer Textverarbeitung optisch nach links gedrängt lässt. CSV kennt kein Verbundkonzept, sodass der Ankerwert ein Feld belegt und jede überdeckte Zelle als leeres Feld ausgegeben wird. Die Lehre für eine Arbeitsmappe, die auch Exporte mit Trennzeichen speist, lautet, alles Tragende aus verbundener Geometrie herauszuhalten; der Artikel zum CSV-, TSV- und HTML-Export geht jedes Format im Detail durch

Verbundener HotXLS-Titel, aus Delphi nach HTML mit colspan und rowspan, nach RTF ohne Spans und nach CSV als abgeflachte Felder exportiert
Derselbe verbundene Titel übersteht den HTML-Export über colspan und rowspan, degradiert in RTF zu einer einzelnen linksgebundenen Zelle und flacht in CSV zu einem Wert plus leeren Feldern ab

Eine Beruhigung für alle, die das gegen die Dateigröße abwägen: Verbünde kosten im Berichtsmaßstab fast nichts. Die Verbundtabelle ist winzig neben den Zelldaten, und das Lesen einer überdeckten Zelle geht weiterhin über FindAt statt über einen Scan. Der Leistungsdruck bei großen Arbeitsmappen kommt von anderswo, vor allem vom Wachstum des Stil-Pools und dem Speicher, den der Speicherpfad hält, was der Artikel zur Leistung großer Arbeitsmappen direkt aufgreift. Beide Verbund-APIs, die strukturellen Bearbeitungsoperationen und die Vorlagen-Demos werden mit der HotXLS Delphi Component ausgeliefert