Ein Abstimmungswerkzeug findet 3.000 Zellen, die vom Hauptbuch abweichen, und möchte, dass die gespeicherte .xls sich öffnet, mit allen ausgewählt unter einer fixierten Kopfzeile. Auf dem Papier ist das ein Selection-Record. In Bytes sind es 9 + 3.000 × 6 = 18.009, und ein BIFF8-Record-Body endet bei 8.224. Dieselbe Mappe stellt eine zweite Frage, die trivial klingt und es nicht ist: Auf einem bei B2 fixierten Blatt sind "die Scroll-Position" vier Panes, die sich zwei Zeilen-Offsets und zwei Spalten-Offsets teilen, und diese vier Zahlen leben in zwei verschiedenen Records
Beide Probleme sind Format-Arithmetik: Die Auswahl wird in Records geschnitten, die ein Reader wieder zusammensetzen muss, das Pane wird über einen Code benannt, der nicht der Lesereihenfolge folgt, und jede Scroll-Achse landet in dem Record, aus dem Excel sie liest
Warum 3.000 ausgewählte Bereiche drei Selection-Records brauchen
[MS-XLS] §2.4.248 legt den Selection-Record (0x001D) als einen 9-Byte-Festteil gefolgt von den Bereichen an. Der Festteil hält das Pane-Byte, rwAct und colAct für die aktive Zelle, irefAct für den Index des aktiven Bereichs und cref für die Bereichsanzahl. Jeder Bereich ist eine RefU-Struktur mit zwei 16-Bit-Zeilen und zwei 8-Bit-Spalten. Die größte Anzahl, die hineinpasst, ist (8.224 − 9) / 6 abgerundet, was 1.369 Bereiche und einen 8.223-Byte-Body ergibt, ein Byte unter dem Limit
HotXLS verwendet 1.369 als seine Deckelung pro Record. Wenn TXLSWorksheet eine Auswahlgruppe mit mehr Bereichen als das speichert, schreibt es aufeinanderfolgende Selection-Records für dasselbe Pane, je 1.369 Bereiche, und jeder Record wiederholt dasselbe Pane-Byte, dieselbe aktive Zelle und denselben aktiven Index. Die 3.000-Zellen-Abstimmungsauswahl kommt als drei Records mit 1.369, 1.369 und 262 Bereichen heraus. Aufrufer sehen diese Aufteilung nie; sie reichen ein Array von A1-Referenzen weiter
var
Book: TXLSWorkbook;
Sheet: TXLSWorksheet;
Diffs: TXLSSelectedAreas;
I: Integer;
begin
Book := TXLSWorkbook.Create;
try
Sheet := Book.Sheets.Add;
Sheet.FreezePanes(1, 1); // Headerzeile und Spalte A bleiben fixiert
SetLength(Diffs, 3000);
for I := 0 to High(Diffs) do
Diffs[I] := Format('C%d', [I + 2]);
// FreezePanes löscht die gespeicherte Auswahl, also erst nach dem Fixieren auswählen.
// 3000 Bereiche werden als drei Selection-Records gespeichert: 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;
Die Aufrufreihenfolge zählt hier. FreezePanes baut das Pane-Layout neu auf und löscht jede pane-spezifische Auswahl, die das Blatt trug, also wäre eine frühere Auswahl zur Speicherzeit weg. Auf einem nicht fixierten Blatt existiert das untere rechte Pane noch nicht, und derselbe SelectAreas-Aufruf würde schlicht False zurückgeben. Erst fixieren, dann auswählen
Auf was zeigt irefAct, wenn eine Auswahl mehrere Records umfasst?
In einer geteilten Auswahl indiziert irefAct die gesamte aggregierte Folge von Bereichen über alle Chunks hinweg, und jeder Chunk trägt denselben Wert. Eine Auswahl von einem Bereich über dem Limit macht das konkret: 1.370 Bereiche mit dem letzten aktiven werden zwei Records, der erste mit cref 1.369 und der zweite mit cref 1, und beide tragen irefAct 1.369. Im zweiten Record ist dieser Index größer als die eigene Bereichsanzahl des Records. Liest man die Records einzeln, sieht's kaputt aus, und zwei Arten von Readern stolpern darüber
- Ein Reader, der
irefActinnerhalb jedes Records gegencrefprüft, weist eine vollkommen gültige Datei am zweiten Chunk ab - Ein Reader, der seinen Auswahlzustand bei jedem Selection-Record ersetzt, behält nur den letzten Chunk und verliert die ersten 1.369 Bereiche
Der HotXLS-Reader hängt aufeinanderfolgende Records für dasselbe Pane zu einer Gruppe zusammen und verlangt, dass jeder Chunk bei der aktiven Zeile, der aktiven Spalte und irefAct übereinstimmt. Die Bereichsprüfung auf irefAct und die Prüfung, dass die aktive Zelle wirklich innerhalb des Bereichs sitzt, auf den sie zeigt, laufen erst, wenn der EOF-Record des Worksheets ankommt und die volle Sequenz bekannt ist. Früher zu prüfen hieße einfacheren Code, und's hieße die falsche Antwort für jede Auswahl über 1.369 Bereiche
Auf der Schreibseite hat die Pane-zuerst-Überladung von SelectAreas keine eigene 1.369-Bereiche-Deckelung. Sie parst jede Referenz, bevor sie das Blatt anfasst: $-Marker werden akzeptiert und entfernt, ein umgedrehter Bereich wie C9:A2 wird zu A2:C9 normalisiert, und ein leerer oder unparsbarer Eintrag lässt den ganzen Aufruf scheitern. Der aktive Index muss innerhalb des Arrays liegen und bei oder unter 32.767 bleiben, denn irefAct ist ein vorzeichenbehaftetes 16-Bit-Feld. Jeder Fehlschlag liefert False und lässt die vorherige Auswahl exakt so, wie sie war
Pane-Codes folgen nicht der Lesereihenfolge
Ein Selection-Record benennt sein Pane mit dem 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 Aufzählung TXLSPanePosition ist in Lesereihenfolge deklariert, xlspTopLeft, xlspTopRight, xlspBottomLeft, xlspBottomRight. Also ist Ord(xlspTopLeft) 0, und 0 in der Datei bedeutet unten rechts. Ein Writer, der das Enum direkt in das Pane-Byte castet, legt jede Auswahl oben links auf das Pane unten rechts, und die Datei bleibt strukturell gültig, weil beide legale Pane-Codes sind
Jeder Pane-bewusste Einstiegspunkt in HotXLS konvertiert das Enum über ein explizites case-Statement, also fasst Anwendungscode die numerischen Codes nie an. Die Pane-Existenz wird ebenfalls gegen die aktuelle Fixier- oder Split-Geometrie geprüft. Das Pane oben links existiert immer. Oben rechts braucht einen vertikalen Split, unten links einen horizontalen, und unten rechts beide. Für ein Pane, das das Blatt nicht hat, liefert SelectAreas False und GetSelectedAreas ein leeres Array mit ActiveAreaIndex auf -1, und keiner der beiden Aufrufe erzeugt unterwegs ein Pane, eine Auswahl oder eine Zelle
Das Worksheet hält außerdem den älteren Zwei-Argument-SelectAreas(Areas, ActiveAreaIndex), der in das gerade aktive Pane schreibt und einen Integer-Status zurückgibt. Auf einem Blatt mit fixierten Panes ist das oft eine Vermutung über einen Zustand, den Sie nicht selbst gesetzt haben. Verwenden Sie zuerst die Überladung mit einem TXLSPanePosition für alles, was auf fixierten oder geteilten Blättern läuft; das Pane ist dann Teil des Aufrufs, und ein False-Ergebnis sagt Ihnen, dass Geometrie und Anfrage sich widersprechen. Das Zurücklesen folgt derselben Gestalt:
var
Areas: TXLSSelectedAreas;
Active: Integer;
begin
Areas := Sheet.GetSelectedAreas(xlspBottomRight, Active);
if Active < 0 then
Memo1.Lines.Add('No usable selection on the bottom-right pane')
else
Memo1.Lines.Add(Format('%d areas, active area %s',
[Length(Areas), Areas[Active]]));
end;
Wo wohnt jedes Pane's Scroll-Position?
Die Scroll-Position jedes Panes ist über zwei Records verteilt, denn's sind nur zwei Zeilenpositionen und zwei Spaltenpositionen, die sich vier Panes teilen. In einer klassischen Mappe ist's die erste sichtbare Zeile der oberen Panes und die erste sichtbare Spalte der linken Panes, abgelegt als Window2.rwTop und Window2.colLeft. Die Zeile der unteren Panes und die Spalte der rechten Panes sind Pane.rwTop und Pane.colLeft, und's bleibt dabei: Oben rechts nimmt seine Zeile aus Window2 und seine Spalte aus Pane, macht's unten links umgekehrt
ScrollWindow und TryGetWindowScroll folgen diesem Layout exakt und verwenden 1-basierte Zeilen- und Spaltennummern. ScrollWindow(xlspTopRight, R, C) schreibt Window2.rwTop und Pane.colLeft, was bedeutet, dass das Setzen der Spalte oben rechts auch das Pane unten rechts bewegt. Das entspricht Excel, wo sich die beiden rechten Panes eine horizontale Scrollbar teilen. Ein fehlendes Pane lässt ScrollWindow False zurückgeben und TryGetWindowScroll beide Ausgaben auf null setzen. Eine Zeile oder Spalte außerhalb des Bereichs wird abgelehnt, bevor sich eine Achse ändert, also kann eine schlechte Spalte keine halb angewandte Zeile zurücklassen
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, also verschiebt dies unten rechts ebenfalls 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;
XLSX speichert dieselben beiden Schichten in zwei Elementen: 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. Ein Loader, der nur eines von beiden behält, verliert entweder die oberen Zeilen oder die linken Spalten jedes fixierten Blatts, das er anfasst. TXLSXWorksheet liest das äußere Attribut zuerst in die Fenster-Level-Felder, lässt das pane-Kind nur die Pane-Level-Felder überschreiben und schreibt beide getrennt zurück. ScrollWindow und TryGetWindowScroll haben auf TXLSXWorksheet dieselben Signaturen und dieselbe Bedeutung, und das Kopieren eines Worksheets trägt beide Schichten in beiden Formaten
Die schmaleren Eigenschaften sind weiterhin da für Code, der nur ein Achsenpaar braucht: ScrollRow und ScrollColumn auf dem klassischen Worksheet, und die nullbasierten SetPaneScroll und GetPaneScroll für das Pane unten rechts auf XLSX. Ein Rendering-Control hält sein eigenes TopRow und LeftCol, wie der Artikel über das Rendern von Workbooks in einem custom VCL grid beschreibt, und dieser Laufzeitzustand ist getrennt von dem, was gespeichert wird. Die Fixier- und Split-Geometrie selbst wird mit den Einstellungen auf Blattebene aus Blattschutz, Seiteneinrichtung und Drucken gesetzt
Korrupte Selection-Records werden Byte für Byte behalten
Wenn ein Selection-Record malformed ist, behält HotXLS seinen Body als opake Bytes, meldet den Diagnosecode 1304 (xlsDiagnosticSelectionRecordInvalid) als Warnung und schreibt den ursprünglichen Body beim Speichern unverändert zurück. Dateien von unbekannten Exporteuren oder handgestrickten BIFF-Writern können solche Records enthalten. Bevor's ein Record in seine Pane-Gruppe schafft, prüft der Reader ein Pane-Byte von 3 oder weniger, zusammenhängende Records pro Pane, cref zwischen 1 und 1.369 mit einem Body von exakt 9 + cref × 6 Bytes, Chunks, die bei der aktiven Zelle und einem irefAct ohne Vorzeichenbit übereinstimmen, eine aktive Spalte auf dem Raster und keine umgedrehten Bereiche. Probleme innerhalb eines physischen Records werden einmal pro Record gemeldet; Widersprüche, die erst nach der Aggregation auftauchen, etwa ein irefAct, das über die Gesamtzahl der Bereiche hinauszeigt, oder eine aktive Zelle außerhalb des indizierten Bereichs, werden einmal pro Gruppe am EOF gemeldet
Eine ungültige Gruppe bleibt draußen vor der getypten API: GetSelectedAreas liefert für dieses Pane ein leeres Array mit Index -1, und die anderen Panes arbeiten normal weiter
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 ist Ihr eigenes TStrings; HotXLS füllt nur Diagnostics
Log.Add(Format('%s: record $%.4x kept opaque (%s)',
[D.SheetName, D.RecordId, D.Message]));
end;
end;
Den Record zu reparieren, indem man irefAct einklemmt oder den umgedrehten Bereich fallen lässt, war die andere Option. Ihn zu bewahren ist der sicherere Default: Eine reparierte Auswahl ist eine, die niemand gewählt hat, und die ursprünglichen Bytes wären weg. Die Diagnosemeldung benennt die Prüfung, die scheiterte, etwa "the area count does not match the record length" oder "records for the same pane are not contiguous", und RecordId ist für diesen Code immer $001D
Zeilen- und Spalteneinfügungen mappen jedes Pane's Auswahl um
Ganze Zeilen oder Spalten einzufügen oder zu löschen mappt die Auswahl jedes Panes über eine gemeinsame Routine um, in beiden Engines, der klassischen und der XLSX. Überlebende Bereiche behalten ihre Reihenfolge und verschieben sich mit den Zellen, die sie abdecken, und der aktive Bereich behält seine Identität. Wird der aktive Bereich gelöscht, wird der erste überlebende Bereich nach ihm aktiv; folgt nichts, der letzte überlebende Bereich vor ihm. Werden alle Bereiche gelöscht, kollabiert die Gruppe zu einer einzelnen Zelle an der Löschgrenze. Eine aktive Zelle, die nicht mehr innerhalb des gewählten Bereichs liegt, wandert an dessen obere linke Ecke, in der gespeicherten Datei gibt's für Index und Koordinate also keinen Widerspruch
Gruppen, die beim Laden als ungültig markiert wurden, lässt die UmMapping-Routine in Ruhe, also geht's mit ihren ursprünglichen Bytes auch nach der Bearbeitung noch sauber durch den Roundtrip. Ändert sich die Auswahl auf einem Pane, werden nur die Records dieses Panes umgeschrieben und's bleibt bei den Records der anderen Panes byte-identisch. ODS-Dateien tragen überhaupt keinen Pane-Auswahlzustand, denn OpenDocument hat keine entsprechende Struktur pro Pane in der Worksheet-View; Auswahlen und Pane-Scroll-Positionen gelten für XLS und XLSX
Wenn Ihre Anwendung Workbooks speichert, die Leute öffnen, um markierte Zellen zu prüfen, dort weiterzumachen, wo jemand aufgehört hat, oder ein fixiertes Dashboard zu lesen, kommen die Pane-bewussten Auswahl- und Scroll-Aufrufe mit der HotXLS-Delphi-Tabellenkalkulationskomponente und verhalten sich für .xls- und .xlsx-Ausgabe gleichermaßen