Technischer Artikel

Ein eigenes Tabellenkalkulations-Grid in Delphi mit HotXLS

HotXLS liefert TXLSWorkbookViewer aus, ein natives VCL-Steuerelement, das XLS-, XLSX-, XLSM- und ODS-Arbeitsmappen als interaktives Tabellenkalkulations-Grid innerhalb eines Delphi- oder C++Builder-Formulars rendert, ohne Excel zu installieren oder es über OLE-Automatisierung zu steuern. Ein solches Steuerelement gut zu bauen bedeutet, drei spezifische Probleme zu lösen: einen Mausklick, der innerhalb einer verbundenen Zelle landet, auf die korrekte logische Zelle abzubilden, Scrollposition, Kopfzeilenbänder und Zellauswahl konsistent zu halten, während ein Benutzer über ein Blatt schwenkt, das weit größer ist als das sichtbare Fenster, und zu entscheiden, was ein Klick auf einen Kommentar-Marker oder eine Hyperlink-Zelle tatsächlich tun soll

Die meisten Delphi-Häuser greifen aus Gründen zu einem Tabellenkalkulations-Viewer, die nichts mit Bearbeiten zu tun haben: eine Audit-Station, die hochgeladene Arbeitsmappen vor Eintritt in eine Pipeline vorschaut, ein Kiosk- oder Report-Viewer, bei dem Microsoft Office nicht Teil des Bereitstellungsabbilds ist, oder ein QA-Werkzeug, das den Inhalt einer Arbeitsmappe zeigen muss, ohne die Unvorhersehbarkeit, einen echten Excel-Prozess über COM zu automatisieren. Ein schlichtes String-Grid liefert schnell Text in Zellen, aber eine Tabellenkalkulationsdatei ist kein schlichtes Grid: Zellen verbinden sich zu Blöcken, die im zugrunde liegenden Modell nur einmal existieren, Blätter tragen feste Kopfzeilenbänder und unabhängige horizontale und vertikale Scrollpositionen, und einzelne Zellen tragen Kommentare und Hyperlinks, die ihr eigenes Interaktionsmodell brauchen. TXLSWorkbookViewer ist HotXLSs Antwort auf diese Lücke, und sein internes Design ist ein vernünftiger Bauplan für jeden, der ein ähnliches Steuerelement von Grund auf baut

Wie vermeidet ein Arbeitsmappen-Viewer eine Abhängigkeit von Excel?

TXLSWorkbookViewer vermeidet Excel vollständig, indem er durch HotXLSs eigenes geparstes Objektmodell liest, statt ein Dokument über Excel zu öffnen und es fernzusteuern. Die Eigenschaft Workbook bindet ein bestehendes TXLSWorkbook für klassische XLS-Dateien, und XlsxWorkbook bindet ein TXLSXWorkbook für XLSX-, XLSM- und Template-Varianten; jedes von beiden kann bereits anderswo in der Anwendung geöffnet sein, und der Viewer liest nur davon. Wenn das Steuerelement die Datei selbst besitzen soll, prüft LoadFromFile die Erweiterung, leitet XLSX, XLSM, XLTX, XLTM und ODS über die moderne Engine und alles andere über die klassische, und gibt frei, welche Arbeitsmappe auch immer es erzeugt hat, sobald das Steuerelement geleert oder zerstört wird

var
  Viewer: TXLSWorkbookViewer;
  Book: TXLSXWorkbook;
begin
  Book := TXLSXWorkbook.Create;
  if Book.Open('quarterly-report.xlsx') <> 1 then
    raise Exception.Create('Could not open workbook');

  Viewer := TXLSWorkbookViewer.Create(Self);
  Viewer.Parent := Self;
  Viewer.Align := alClient;
  Viewer.XlsxWorkbook := Book;        // the viewer does not take ownership
  Viewer.GoToCell(1, 1);

  Caption := Viewer.WorksheetName + ': ' + Viewer.SelectedCellText;
end;

Die richtige Zelle innerhalb eines verbundenen Bereichs lokalisieren

Einen Klick in TXLSWorkbookViewer auf die korrekte Zelle aufzulösen ist ein zweistufiges Lookup, und die Aufteilung zählt, weil Pixelgeometrie und Tabellenkalkulations-Semantik tatsächlich unterschiedliche Probleme sind. Die erste Stufe ist reine Geometrie: Eine private Methode CellAtPoint durchläuft Spaltenbreiten und Zeilenhöhen ab der aktuellen Scrollposition, bis sie das Band findet, das die geklickte X- und Y-Koordinate enthält, ganz ohne Bewusstsein für verbundene Zellen. Die zweite Stufe ist semantisch: Jeder Pfad, der die Auswahl ändert, ein Mausklick, eine Pfeiltaste, Tab, oder ein direkter Aufruf von GoToCell, mündet in eine einzige interne Routine ChangeSelection, die die rohe Zeile und Spalte gegen jede Verbindung normalisiert und sie auf die Ankerzelle der Verbindung einrastet, bevor sich die Auswahl tatsächlich ändert

Der Anker ist die obere linke Zelle des verbundenen Bereichs, und sie ist die einzige Zelle in diesem Block, die im zugrunde liegenden Arbeitsmappen-Modell tatsächlich einen Wert, ein Format, einen Kommentar oder einen Hyperlink trägt; jede andere Zelle, die die Verbindung visuell abdeckt, ist in den Daten selbst leer. Bei klassischen XLS-Arbeitsmappen kommt der Anker von Cell.MergeArea, einem IXLSRange, dessen Row und Column auf die besitzende Zelle zeigen; bei XLSX- und ODS-Arbeitsmappen gibt MergedCells.FindAt einen TXLSXMergedRange zurück, der denselben Anker als Row1 und Col1 freilegt. Das Zeichnen löst ein gleichwertiges Problem unabhängig davon, indem es das Rechteck einer verbundenen Zelle auf ihre volle Zeilen- und Spaltenspanne erweitert und die Zellen innerhalb dieser Spanne überspringt, sodass die Auswahlkontur den gesamten verbundenen Block umschließt statt nur seine Ankerecke, und verbundene Layouts zu schreiben statt sie nur zurückzulesen ist ein verwandtes, aber eigenständiges Problem, das im begleitenden Artikel zum Layout verbundener Zellen für Report-Vorlagen behandelt wird

var
  Sheet: TXLSXWorksheet;
begin
  Sheet := Book.Sheets.Add('Summary');
  Sheet.MergeCells(2, 2, 3, 4);       // B2:D3
  Sheet.Cells[2, 2].Value := 'Region totals';

  Viewer.XlsxWorkbook := Book;
  Viewer.GoToCell(3, 4);              // targets the bottom-right corner of the merge
  // SelectedRow is now 2 and SelectedCol is now 2: normalized to the anchor cell
end;

Was hält Scrollen, Kopfzeilen und Auswahl synchron?

TXLSWorkbookViewer hält drei separate Zustandsstücke kohärent: die logische Scrollposition, gehalten in TopRow und LeftCol, die nativen Windows-Scrollbars, die das Steuerelement über WS_HSCROLL und WS_VSCROLL in CreateParams anfordert, und die aktuelle Auswahl in SelectedRow und SelectedCol. Das Ziehen einer Scrollbar oder das Drehen des Mausrads löst WM_HSCROLL, WM_VSCROLL oder WM_MOUSEWHEEL aus, was TopRow oder LeftCol aktualisiert und neu zeichnet; die Auswahl bewegt sich nicht, was dazu passt, wie Excel selbst Schwenken von Auswählen trennt. Nach jeder dieser Aktualisierungen schiebt UpdateScrollBars die neue Position über SetScrollInfo zurück in die native Scrollbar, sodass der Schieber nie aus dem Gleichgewicht mit dem gerät, was das Grid tatsächlich anzeigt

Tastaturnavigation führt dieselbe Synchronisation in der entgegengesetzten Richtung durch: Wird die Auswahl über den Rand des sichtbaren Grids hinaus bewegt, ruft das EnsureSelectionVisible auf, das TopRow oder LeftCol anstößt, indem es tatsächliche Spaltenbreiten und Zeilenhöhen aufsummiert statt einfach um eins zu erhöhen, da Zeilen und Spalten benutzerdefinierte Größen tragen können, und ruft dann UpdateScrollBars auf, sodass der Schieber widerspiegelt, wohin die Tastatur die Ansicht gerade bewegt hat. Die Zeilennummer- und Spaltenbuchstaben-Kopfzeilenbänder, dimensioniert über RowHeaderWidth und ColumnHeaderHeight, sind der Teil dieses Steuerelements, der auf dem Bildschirm fest bleibt, während TopRow und LeftCol die darunterliegenden Daten scrollen, und das ist das Ausmaß an Fixierung, das dieses Steuerelement von sich aus leistet: Es ist nicht Excels Fenster-Fixierung-Feature (Freeze Panes), und es gibt keine eingebaute Möglichkeit, eine beliebige Datenzeile oder -spalte zu fixieren, während der Rest des Blatts daran vorbeiscrollt. Eine Grenze, die es sich lohnt zu testen, bevor man einen Viewer über Dateien ausliefert, die man nicht vollständig kontrolliert, ist, dass TopRow und LeftCol nicht gegen den tatsächlich genutzten Bereich des Arbeitsblatts begrenzt sind, sodass ein an seine strukturelle Grenze gezogener Schieber auf Zeile 1.048.576 oder Spalte 16.384 landen und ein leeres Grid statt der letzten tatsächlich Daten enthaltenden Zeile oder Spalte zeigen kann; Arbeitsmappen, die groß genug sind, um das bemerkbar zu machen, sind meist auch groß genug, um die im Artikel zur Leistung großer Arbeitsmappen behandelte Aufmerksamkeit auf der Ladeseite zu benötigen

Kommentare und Hyperlinks an Maus- und Auswahlereignisse anschließen

TXLSWorkbookViewer behandelt Kommentare und Hyperlinks als Attribute der jeweils aktuell ausgewählten Zelle statt als Hover-Ziele, sodass SelectedCellCommentText, SelectedCellCommentAuthor und SelectedCellHyperlink jedes Mal aktualisieren, wenn OnSelectionChange feuert, egal ob sich die Auswahl per Mausklick, Pfeiltaste oder einem Aufruf von GoToCell bewegt hat. Eine kommentierte Zelle bekommt ein kleines rotes Dreieck in ihre obere rechte Ecke gemalt, als visuellen Hinweis, ähnlich Excels eigener Kommentar-Markierung, aber dieser Marker ist rein visuell; es gibt keinen im Steuerelement eingebauten, hover-ausgelösten Tooltip, sodass eine Anwendung, die ein Popup bei Mouse-over statt bei Auswahl wünscht, diese Schicht selbst bauen muss. Hyperlink-Aktivierung funktioniert auf dieselbe auswahl-zuerst-Art: Ein Doppelklick auf eine Zelle ruft ActivateSelectedCell auf, das SelectedCellHyperlink liest und, falls nicht leer, OnHyperlinkClick mit der Zieladresse und einem var Handled: Boolean-Parameter auslöst, den der Handler setzen kann

Was OnHyperlinkClick nicht tut, ist ebenso wichtig: TXLSWorkbookViewer ruft nie ShellExecute auf oder öffnet von sich aus einen Browser, unabhängig davon, ob der Handler Handled auf true setzt oder auf false belässt. Navigation, und jede Entscheidung darüber, was als sicheres Ziel zählt, liegt vollständig in der Verantwortung der Host-Anwendung, was der richtige Standard für eine Komponente ist, die keine Ahnung hat, ob sie in ein vertrauenswürdiges internes Werkzeug oder einen Viewer für Dateien eingebettet ist, die ein Kunde gerade hochgeladen hat

procedure TMainForm.ViewerSelectionChange(Sender: TObject; Row, Col: Integer);
begin
  if Viewer.SelectedCellCommentText <> '' then
    StatusBar.SimpleText := Viewer.SelectedCellCommentAuthor + ': ' +
      Viewer.SelectedCellCommentText
  else
    StatusBar.SimpleText := Viewer.SelectedCellHyperlink;
end;

procedure TMainForm.ViewerHyperlinkClick(Sender: TObject;
  const Target: WideString; var Handled: Boolean);
begin
  ShellExecute(0, 'open', PWideChar(Target), nil, nil, SW_SHOWNORMAL);
  Handled := True;
end;

Auswahlumfang und Grenzen der Tastaturnavigation

Die Auswahl in TXLSWorkbookViewer ist immer eine einzelne logische Zelle, verfolgt als SelectedRow und SelectedCol; es gibt im Basis-Steuerelement keine rechteckige Mehrzellen-Bereichsauswahl, sodass jedes Feature, das auf einen Zellblock einwirken muss, darüber gebaut werden muss, statt aus einem Auswahlobjekt gelesen zu werden. Die Tastaturabdeckung ist absichtlich einfach: Pfeiltasten bewegen jeweils eine Zelle, Pos1 kehrt zum Zeilenanfang zurück oder, mit Strg, zu Zelle A1, Bild-auf und Bild-ab springen zehn Zeilen, und Tab und Umschalt+Tab schreiten über Spalten; es gibt keinen Strg+Pfeil-Sprung an den Rand eines Datenbereichs und keine Umschalt-erweiterte Bereichsauswahl, sodass Benutzer, die direkt von Excel kommen, die Lücke auf einem dichten Blatt bemerken werden

Spaltenlimits werden am selben ChangeSelection-Engpass durchgesetzt, der die Verbindungs-Normalisierung handhabt, und sie unterscheiden sich absichtlich je nach Engine: Ein an ein klassisches TXLSWorkbook gebundener Viewer klemmt bei Spalte 256, der strukturellen Obergrenze des BIFF8-Formats, während einer, der an TXLSXWorkbook gebunden ist, das moderne 16.384-Spalten-Limit respektiert, das XLSX ab Excel 2007 geerbt hat. Zeilen sind so oder so bei 1.048.576 begrenzt, sodass der praktische Unterschied zwischen dem Öffnen einer Legacy-XLS-Datei und einer XLSX-Datei im selben Viewer vollständig darin liegt, wie weit nach rechts das Grid Sie gehen lässt

Nichts davon ist exotisch, sobald es in Pixel-Lookup, Anker-Normalisierung und eine Handvoll Nachrichten-Handler zerlegt ist, aber die drei unter echten Dateien, mit echten Verbindungen, Kommentaren und Hyperlinks, zur Übereinstimmung zu bringen, ist der Großteil der Arbeit in einer Komponente wie dieser. TXLSWorkbookViewer wird als Teil der Standard-HotXLS-Excel-Komponente für Delphi und C++Builder ausgeliefert, zusammen mit den klassischen und XLSX-Objektmodellen, aus denen sie rendert