HotXLS speichert Worksheet-Auswahlen und Scroll-Positionen pro Pane über eine einzige pane-bewusste API, und zwar auf TXLSWorksheet wie auf TXLSXWorksheet: SelectAreas, GetSelectedAreas, ScrollWindow und TryGetWindowScroll. Für klassische .xls-Dateien schreibt HotXLS BIFF8-Selection-Records (0x001D) mit jeweils höchstens 1369 Bereichen, übersetzt logische Pane-Namen in die vom Format definierten Pane-Bytes und legt jede Scroll-Achse auf den Window2- oder Pane-Record, wo Excel sie erwartet
Das Problem taucht meist in einem Abgleich- oder Audit-Tool auf. Das Tool öffnet einen Ledger-Export, findet jede Zelle, die vom Quellsystem abweicht, und speichert die Arbeitsmappe mit diesen Zellen als Auswahl unter einer eingefrorenen Kopfzeile, damit der Prüfer direkt bei den Abweichungen landet, statt danach zu scrollen. Bei vierzig Abweichungen klappt das bestens. Die Monatsabschluss-Datei hat 3,000, und ein einzelner Selection-Record mit 3,000 Bereichen kann nicht existieren: Sein Body bräuchte 18,009 Bytes, mehr als das Doppelte dessen, was ein BIFF8-Record tragen kann. Die Scroll-Position hat eine ähnliche Falle. Auf einem Sheet mit eingefrorenen Fenstern ist "wo der Nutzer hingesehen hat" nicht eine Koordinate, sondern vier Panes, die sich zwei Zeilen- und zwei Spaltenpositionen teilen
Warum braucht eine große Auswahl mehr als einen Selection-Record?
Eine große Auswahl braucht mehrere Records, weil ein BIFF8-Record-Body auf 8224 Bytes begrenzt ist und jeder ausgewählte Bereich feste sechs Bytes kostet. [MS-XLS] §2.4.248 legt den Selection-Record als 9-Byte-Fixed-Teil an (das Pane-Byte, rwAct und colAct für die aktive Zelle, irefAct für den aktiven Bereich und cref für die Bereichsanzahl), gefolgt von cref RefU-Strukturen, die jeweils zwei 16-Bit-Zeilen und zwei 8-Bit-Spalten halten. Die größte Anzahl, die hineinpasst, ist (8224 − 9) / 6 abgerundet, also 1369, und das ergibt einen 8223-Byte-Body, ein Byte unter dem Limit. TXLSWorksheet.StoreSelectionGroup benutzt diese Konstante als MaxAreasPerRecord und schreibt eine größere Gruppe als aufeinanderfolgende Selection-Records für denselben Pane, je 1369 Bereiche
Das Detail, das beißt, ist irefAct. Jeder Chunk wiederholt dieselbe aktive Zeile, aktive Spalte und denselben aktiven Bereichsindex, und irefAct indiziert die aggregierte Folge aller Chunks, nicht die Bereiche innerhalb des Records, der ihn trägt. Eine Auswahl, die das Limit um einen Bereich überschreitet, macht das konkret: 1370 Bereiche mit dem letzten aktiven werden zu zwei Records, der erste mit cref 1369, der zweite mit cref 1, und beide tragen irefAct 1369. Dieser Wert ist größer als die eigene Bereichsanzahl des zweiten Records. Ein Reader, der irefAct in jedem Record gegen cref prüft, verwirft eine gültige Datei, und ein Reader, der seinen State bei jedem Record ersetzt, verliert die ersten 1369 Bereiche. Der HotXLS-Reader hängt aufeinanderfolgende Same-Pane-Records an eine Gruppe an, verlangt von jedem Chunk Übereinstimmung bei aktiver Zelle und Index und führt die Bereichsprüfung erst am EOF-Record des Worksheets aus, wenn die vollständige Folge bekannt ist. Die pane-first-Überladung von SelectAreas hat daher keine 1369-Bereiche-Grenze. Sie validiert jede A1-Referenz und den aktiven Index, bevor sie sich den Worksheet-Write-Lock nimmt, und gibt False zurück, ohne die bisherige Auswahl zu verändern, wenn etwas missgestaltet ist
var
Book: TXLSWorkbook;
Sheet: TXLSWorksheet;
Diffs: TXLSSelectedAreas;
I: Integer;
begin
Book := TXLSWorkbook.Create;
try
Sheet := Book.Sheets.Add;
Sheet.FreezePanes(1, 1); // Kopfzeile und Spalte A bleiben fixiert
SetLength(Diffs, 3000);
for I := 0 to High(Diffs) do
Diffs[I] := Format('C%d', [I + 2]);
// Einfrieren setzt die gespeicherte Auswahl zurück, also erst danach auswählen.
// 3000 Bereiche werden zu drei Selection-Records: 1369 + 1369 + 262
if not Sheet.SelectAreas(xlspBottomRight, Diffs, 0) then
raise Exception.Create('Selection rejected');
Book.SaveAs('reconciliation.xls');
finally
Book.Free;
end;
end;
Welches Pane-Byte benutzt ein Selection-Record?
Ein Selection-Record identifiziert seinen Pane über den numerischen Code, den das Format definiert: 0 für unten rechts, 1 für oben rechts, 2 für unten links und 3 für oben links. Die öffentliche Enumeration TXLSPanePosition ist in Lesereihenfolge deklariert, xlspTopLeft, xlspTopRight, xlspBottomLeft, xlspBottomRight, Ord(xlspTopLeft) ist also 0 – was in der Datei der untere rechte Pane ist. Das Enum direkt in das Pane-Byte zu casten, würde jede Oben-links-Auswahl fehlerfrei auf den unteren rechten Pane schreiben. Jeder pane-bewusste HotXLS-Einstiegspunkt konvertiert das Enum stattdessen über ein explizites case-Statement, sodass Aufrufer mit den numerischen Codes nie in Berührung kommen. Geprüft wird auch die Pane-Existenz: Der obere rechte Pane existiert nur mit vertikalem Split, der untere linke nur mit horizontalem und der untere rechte nur mit beidem. Für einen Pane, den die aktuelle Split- oder Freeze-Geometrie nicht hat, gibt SelectAreas False zurück, und GetSelectedAreas liefert ein leeres Array mit ActiveAreaIndex -1 – ohne einen Pane, ein Selection-Objekt oder eine Zelle in der Arbeitsmappe anzulegen
Wo wohnt die Scroll-Position jedes Panes?
Die Scroll-Position jedes Panes verteilt sich auf zwei Records, denn vier Panes teilen sich nur zwei Zeilen- und zwei Spaltenpositionen. In einer klassischen Arbeitsmappe sind die erste sichtbare Zeile der oberen Panes und die erste sichtbare Spalte der linken Panes Window2.rwTop und Window2.colLeft, während die Zeile der unteren Panes und die Spalte der rechten Panes in Pane.rwTop und Pane.colLeft liegen. ScrollWindow(xlspTopRight, R, C) schreibt daher Window2.rwTop und Pane.colLeft, und die Spalte des oberen rechten Panes zu setzen verschiebt auch den unteren rechten – genau wie die beiden sich in Excel eine horizontale Scrollbar teilen. Die öffentlichen Methoden arbeiten mit 1-basierten Zeilen- und Spaltennummern. Ein fehlender Pane gibt False zurück und setzt beide Abfrage-Ausgaben auf null, und eine Koordinate außerhalb des Bereichs wird abgelehnt, bevor sich eine Achse ändert. Nichts davon hängt davon ab, wie ein Viewer das Raster zeichnet. Ein Rendering-Control hält sein eigenes TopRow und LeftCol, wie der Artikel zum Rendern von Arbeitsmappen in einem eigenen VCL-Grid beschreibt, und das ist Runtime-State, nicht das, was gespeichert wird
XLSX verteilt dieselben Daten auf zwei Elemente: sheetView/@topLeftCell (ECMA-376 Part 1, §18.3.1.87) für das Fenster als Ganzes und das Kind pane/@topLeftCell (§18.3.1.66) für die untere rechte Seite eines Splits. Beide Attribute können gleichzeitig vorhanden sein. HotXLS liest das äußere Attribut zuerst in die Felder auf Fensterebene, lässt das pane-Kind nur die Felder auf Pane-Ebene überschreiben und schreibt beide getrennt zurück. Die beiden Ebenen in eine zusammenzufassen ist genau die Art, wie eine obere oder linke Scroll-Position beim Laden stillschweigend verschwindet. Worksheet-Kopien tragen beide Ebenen, in beiden Engines. Die älteren Einstiegspunkte behalten ihr ursprüngliches Verhalten: die klassischen Properties ScrollRow und ScrollColumn sowie die nullbasierten XLSX-Varianten SetPaneScroll und GetPaneScroll. Die Freeze- und Split-Geometrie selbst konfigurieren Sie über die Einstellungen auf Sheet-Ebene, die der Artikel zu Sheet-Schutz, Seiteneinrichtung und Drucken abdeckt
var
Row, Col: Integer;
begin
Sheet.FreezePanes(1, 1);
// Unten rechts: untere Zeilenachse (Pane.rwTop) und rechte Spaltenachse (Pane.colLeft)
Sheet.ScrollWindow(xlspBottomRight, 500, 3);
// Oben rechts teilt die rechte Spaltenachse, daher schiebt dies auch unten rechts auf Spalte 6
Sheet.ScrollWindow(xlspTopRight, 1, 6);
if Sheet.TryGetWindowScroll(xlspBottomRight, Row, Col) then
Memo1.Lines.Add(Format('Bottom-right starts at row %d, column %d', [Row, Col]));
// Unten rechts beginnt bei Zeile 500, Spalte 6
end;
Was passiert, wenn ein Selection-Record korrupt ist?
Ist ein Selection-Record korrupt, behält HotXLS ihn als opake Bytes, meldet den Diagnose-Code 1304 (xlsDiagnosticSelectionRecordInvalid) und schreibt den ursprünglichen Body beim Speichern Byte für Byte zurück. Bevor ein Record der Gruppe seines Panes beitritt, prüft der Reader ihn der Reihe nach. Das Pane-Byte muss 3 oder kleiner sein. Records für einen Pane müssen im Stream zusammenhängend liegen. Die 9 festen Bytes müssen vorhanden sein. cref muss zwischen 1 und 1369 liegen, und der Body muss exakt 9 + cref × 6 Bytes lang sein. Jeder Chunk in einer Gruppe muss bei aktiver Zelle und irefAct übereinstimmen, irefAct darf kein Vorzeichenbit gesetzt haben, die aktive Spalte muss auf dem Raster liegen, und kein Bereich darf vertauschte Grenzen haben. Probleme in einem einzelnen physischen Record werden einmal pro Record gemeldet. Widersprüche, die erst nach der Aggregation auftauchen – etwa ein irefAct, der hinter die Gesamtzahl der Bereiche zeigt, oder eine aktive Zelle außerhalb des indizierten Bereichs – werden einmal pro Gruppe am EOF gemeldet. Eine ungültige Gruppe bleibt für die typisierte API unsichtbar: GetSelectedAreas liefert für diesen Pane ein leeres Array mit Index -1, während alle anderen Panes weiterarbeiten
var
I: Integer;
D: TXLSDiagnostic;
begin
if Book.Open('supplier-upload.xls') <> 1 then
Exit;
for I := 0 to Book.Diagnostics.Count - 1 do
begin
D := Book.Diagnostics[I];
if D.Code = xlsDiagnosticSelectionRecordInvalid then
Log.Add(Format('%s: record $%.4x kept opaque (%s)',
[D.SheetName, D.RecordId, D.Message]));
end;
end;
Wie überleben Auswahlen Zeilen- und Spalteneinfügungen?
Auswahlen überleben strukturelle Änderungen, weil das Einfügen oder Löschen ganzer Zeilen oder Spalten jede dargestellte Pane-Gruppe in der Classic- wie in der XLSX-Engine über einen gemeinsamen Remapper neu abbildet. Überlebende Bereiche behalten ihre Reihenfolge, und der aktive Bereich behält seine Identität. Wird der aktive Bereich gelöscht, wird der erste überlebende Nachfolger aktiv, sonst der letzte überlebende Vorgänger, wenn nichts mehr folgt. Werden alle Bereiche gelöscht, kollabiert die Gruppe auf eine einzelne Zelle an der Löschgrenze, und eine aktive Zelle, die nicht mehr im gewählten Bereich liegt, wandert in dessen obere linke Ecke – Index und Koordinate widersprechen sich also nie. Die Grenzen sind bewusst so gezogen. Ungültige Classic-Gruppen überspringt der Remapper, statt sie in eine erfundene Auswahl umzuschreiben, sodass ihre Original-Bytes weiterhin unverändert round-trippen. Das Bearbeiten eines Panes ersetzt nur die Records dieses Panes und lässt die anderen byteidentisch. ODS bekommt gar keinen Pane-Selection-State, weil ODF keine äquivalente Worksheet-View-Struktur hat, die ihn tragen könnte
Wenn Ihre Anwendung .xls-Dateien schreibt, die Nutzer öffnen und darin navigieren müssen – ob um markierte Zellen zu prüfen, dort weiterzuarbeiten, wo sie aufgehört haben, oder ein eingefrorenes Dashboard zu teilen –, dann ist die pane-bewusste Selection- und Scroll-API Teil der HotXLS-Delphi-Tabellenkomponente, und sie funktioniert für XLS wie für XLSX gleich