Technischer Artikel

Interaktiven PDF-Viewer in Delphi mit TPDFlibViewer erstellen

Das Einbetten einer PDF-Leseansicht in eine Delphi-Geschäftsanwendung erfordert weder das Schreiben einer Renderingschleife, einer Scroll-Engine noch eines Zoom-Modells. Die losLab PDF Library enthält TPDFlibViewer, ein VCL-Steuerelement, das ein Dokument mit einem einzigen Aufruf öffnet und Textauswahl, Schwenken, Bereichszoom (Marquee Zoom), Schnappschüsse, Bereichshervorhebung, Volltextsuch-Hervorhebung, Interaktion mit Anmerkungen und Drucken als integriertes Verhalten bereitstellt. Dieser Artikel beschreibt die fünf Schritte, mit denen Sie ein leeres Formular in einen funktionierenden Viewer verwandeln: Platzieren des Steuerelements, Verknüpfen der vorgefertigten Aktionen, Hinzufügen der Suchhervorhebung, Verarbeiten von Klicks auf Anmerkungen und Drucken

Warum ein Viewer-Steuerelement verwenden, anstatt eine Renderingschleife zu schreiben?

Das Schichten-Argument steht an erster Stelle, da es erklärt, was Sie tatsächlich erwerben. Das Rendern einer Seite in eine Bitmap macht nur die einfachen 20 Prozent aus; die anderen 80 Prozent betreffen alles, was darum herum aufgebaut ist: ein Seiten-Bitmap-Cache mit einem Bereinigungsbudget, das Scroll-Layout über Seiten unterschiedlicher Größe hinweg, Zoom-Modi, die sich bei Größenänderungen neu berechnen, Hit-Testing, das ein Mauspixel bei beliebigem Zoom und jeder Rotation wieder in Seitenkoordinaten umrechnet, und eine flackerfreie Neuzeichnung. TPDFlibViewer übernimmt all dies und delegiert die Rasterarbeiten an dieselbe Engine, die im Artikel über Multi-Engine-Rendering beschrieben wird, sodass die Steuerungsschicht und die Renderingschicht separat austauschbar bleiben. Ihr Formularcode kommuniziert mit einem scrollbaren Windows-Steuerelement, nicht mit einem Renderer

Wie bettet man einen PDF-Viewer in eine Delphi-Anwendung ein?

TPDFlibViewer.LoadFromFile öffnet ein Dokument über die steuereigene interne TPDFlib-Instanz und gibt bei Erfolg 1 zurück. Somit besteht der minimale Viewer aus einem Konstruktoraufruf, einer Parent-Zuweisung und einem Ladevorgang. Wenn Sie bereits an anderer Stelle in der Anwendung eine TPDFlib-Instanz halten, zeigt AttachLibrary stattdessen deren ausgewähltes Dokument an, wobei die Eigentümerschaft auf Ihrer Seite verbleibt

uses PDFlibViewer;

procedure TMainForm.FormCreate(Sender: TObject);
begin
  FViewer := TPDFlibViewer.Create(Self);
  FViewer.Parent := Self;
  FViewer.Align := alClient;
  if FViewer.LoadFromFile('quarterly-report.pdf', '') = 1 then
    FViewer.ViewerMode := vmHand;  // pan by dragging; a plain click still follows links
end;

Die Eigenschaft ViewerMode bestimmt, was die Maus tut, und zwar anhand von fünf Werten: vmSelect ist das Standardwerkzeug zur Textauswahl, vmHand schwenkt durch Ziehen mit einem Hand-Cursor (wobei ein einfacher Klick weiterhin Links folgt), vmZoom ist ein Bereichszoom, der den gezogenen Bereich vergrößert, um den Viewport auszufüllen, vmSnapshot erfasst einen gezogenen Bereich in die Zwischenablage (mit einem OnSnapshot-Ereignis und einem SnapshotToClipboard-Schalter) und vmHighlight zeichnet dauerhafte, transluzente Bereichshervorhebungen. Das Hervorhebungswerkzeug verdient eine Anmerkung zu den Koordinaten: Bereiche werden im Seiten-Benutzerbereich gespeichert, nicht in Client-Pixeln. Daher überstehen AddHighlightRegion-Markierungen das Zoomen, die Ansichtsrotation und die Seitennavigation ohne jeglichen Verwaltungsaufwand Ihrerseits. In einer rotierten Ansicht kann die Zugrichtung sogar entgegen den Seitenachsen verlaufen, was nach der inversen Rotationsabbildung zu einer negativen Breite oder Höhe führt – das Steuerelement normalisiert das Rechteck für Sie

Wie verdrahtet man eine PDF-Symbolleiste in Delphi ohne Glue-Code?

Die Unit PDFlibViewerActions erübrigt den Glue-Code für Symbolleisten vollständig. Sie stellt zwanzig vorgefertigte, von TAction abgeleitete Klassen bereit, die die Seitennavigation (TPDFlibViewerFirstPage, TPDFlibViewerPriorPage, TPDFlibViewerNextPage, TPDFlibViewerLastPage), den Zoom (TPDFlibViewerZoomIn, TPDFlibViewerZoomOut, TPDFlibViewerFitWidth, TPDFlibViewerFitPage, TPDFlibViewerActualSize), die Ansichtsrotation (TPDFlibViewerRotateClockwise, TPDFlibViewerRotateAntiClockwise), das Drucken, Datei-Öffnen- und Speichern-unter-Dialoge, das Kopieren von markiertem Text sowie jeweils eine Umschaltaktion pro Mauswerkzeug (TPDFlibViewerSelectMode, TPDFlibViewerHandMode, TPDFlibViewerMarqueeZoomMode, TPDFlibViewerSnapshotMode, TPDFlibViewerHighlightMode) abdecken. Jede Aktion verweist über ihre veröffentlichte Eigenschaft Viewer auf ein Steuerelement und verwaltet ihren eigenen Zustand: Jede Aktion deaktiviert sich selbst, wenn kein Dokument geöffnet ist, die Navigationsaktionen verfolgen die Seitenposition, das Kopieren prüft, ob eine Auswahl vorhanden ist, und die Modus- und Einpassungsaktionen halten ihren aktivierten Zustand synchron mit dem aktiven Werkzeug und Zoom-Modus. Zur Entwurfszeit registrieren sie sich alle in der losLab PDF-Kategorie des ActionList-Editors; zur Laufzeit bestehen dieselben Klassen aus jeweils drei Zeilen

uses System.Actions, Vcl.ActnList, PDFlibViewer, PDFlibViewerActions;

procedure TMainForm.BuildToolbar;
var
  NextPage: TPDFlibViewerNextPage;
  FitWidth: TPDFlibViewerFitWidth;
  HandMode: TPDFlibViewerHandMode;
begin
  NextPage := TPDFlibViewerNextPage.Create(Self);
  NextPage.ActionList := ActionList1;
  NextPage.Viewer := FViewer;
  btnNext.Action := NextPage;

  FitWidth := TPDFlibViewerFitWidth.Create(Self);
  FitWidth.ActionList := ActionList1;
  FitWidth.Viewer := FViewer;
  btnFitWidth.Action := FitWidth;

  HandMode := TPDFlibViewerHandMode.Create(Self);
  HandMode.ActionList := ActionList1;
  HandMode.Viewer := FViewer;
  btnHand.Action := HandMode;
end;

Eine Falle im Unit-Gültigkeitsbereich sollte man kennen, wenn man eine eigene Aktionsregistrierung auf diesem Muster aufbaut. RegisterActions ist in System.Actions deklariert, nicht in Vcl.ActnList, und der Compilerfehler, den man erhält, wenn nur Vcl.ActnList in der uses-Klausel steht, ist irreführend: Das Klassen-Array-Literal [TAction1, TAction2, ...] wird fälschlicherweise als Set geparst, was eine Reihe von Fehlern des Typs E2010 Incompatible types: 'Integer' and 'class of ...' erzeugt, die wie ein Typkonflikt in Ihren Aktionsklassen aussehen. Die Lösung ist ein einziger uses-Eintrag, aber die Diagnose verweist überall hin, nur nicht darauf. Bei Zoom-Symbolleisten meldet TPDFlibViewer.EffectiveZoomPercent den tatsächlich wirksamen Zoom – einschließlich des berechneten Faktors in den Modi „Breite anpassen“ und „Seite anpassen“ –, sodass ein Zoom-Schritt auf der realen aktuellen Vergrößerung aufbauen kann und nicht auf dem zuletzt zugewiesenen Wert

Suchtreffer-Hervorhebung und der Koordinaten-Flip dahinter

HighlightSearchHits durchsucht das gesamte Dokument nach einer Suchanfrage, zeichnet einen transluzenten Balken über jeden Treffer und gibt die Anzahl der Treffer zurück. ClearSearchHighlights, SearchHighlightCount und die Eigenschaft SearchHighlightColor vervollständigen die Schnittstelle. Da die Treffer-Rechtecke im Seitenbereich gespeichert und bei jedem Zeichnen neu projiziert werden, bleiben die Balken an ihrem Text fixiert, während der Benutzer scrollt, zoomt und die Ansicht rotiert. Wenn Sie das zugehörige TPDFlibSearchPanel verwenden, dessen RunSearch dieselben Balken im angehängten Viewer automatisch über die standardmäßig aktivierte Eigenschaft HighlightInView

procedure TMainForm.btnSearchClick(Sender: TObject);
var
  Hits: Integer;
begin
  FViewer.SearchHighlightColor := clYellow;
  Hits := FViewer.HighlightSearchHits(edtQuery.Text, False);  // case-insensitive
  StatusBar1.SimpleText := Format('%d matches', [Hits]);
end;

Der knifflige Teil und der Grund, warum der integrierte Pfad dem Erstellen einer eigenen Lösung aus rohen Suchergebnissen vorzuziehen ist, ist die Konvertierung des Koordinatensystems. Die Text-Engine meldet Treffer-Rechtecke in Koordinaten, die von der Origin-Einstellung der Bibliothek abhängen: Bei Origin 0 oder 3 verläuft die Y-Achse von unten nach oben wie im nativen PDF-Benutzerbereich, und bei Origin 2 oder 3 wird die X-Achse ab dem rechten Rand gemessen. Das Overlay des Viewers arbeitet in einem von oben nach unten und von links ausgerichteten Seitenbereich, sodass jedes Treffer-Rechteck umgedreht werden muss (Y = PageHeight - Bottom und gegebenenfalls X = PageWidth - Right), bevor es gespeichert wird. Wenn man die gemeldete Oberkante unter der Standard-Origin eins zu eins übernimmt, wird die Hervorhebung an der vertikal gespiegelten Position auf der Seite gezeichnet. TPDFlibViewer führt diese Konvertierung gemäß der aktiven Origin durch, wenn jeder Treffer aufgezeichnet wird, und wendet dann zur Laufzeit die Vorwärtsrotationsabbildung an, weshalb die Balken in jedem Winkel korrekt platziert werden. Die Transluzenz selbst nutzt die Windows AlphaBlend-API mit einem konstanten Quell-Alpha-Wert, da das VCL-Canvas kein natives Alpha-Compositing besitzt. Die zugrunde liegenden APIs für Textsuche und Geometrie werden im Artikel über Textsuche und Aufzählung von Seitenelementen ausführlich behandelt

Klicks auf Anmerkungen, Hinweise und Export von Anhängen

TPDFlibViewer behandelt Anmerkungen als interaktive Objekte erster Klasse. Das Klicken auf eine Nicht-Link-Anmerkung löst das Ereignis OnAnnotClick mit der Seitennummer, dem 1-basierten Anmerkungsindex und der Subtyp-Zeichenfolge aus; AnnotAtPagePoint stellt denselben Treffertest programmatisch bereit und überspringt Popup-Begleitanmerkungen, sodass Sie immer die Anmerkung erhalten, die der Benutzer tatsächlich sieht. Das Bewegen des Mauszeigers über eine Anmerkung zeigt deren Inhaltstext als Steuerelement-Hinweis über den AnnotHints-Schalter an, der standardmäßig aktiviert ist und mit der standardmäßigen VCL-Eigenschaft ShowHint zusammenarbeitet. Bei FileAttachment-Anmerkungen schreibt SaveAnnotAttachmentToFile die eingebettete Datei auf die Festplatte und gibt bei Erfolg 1 zurück, was einen Click-to-Save-Workflow in wenigen Zeilen vervollständigt

procedure TMainForm.ViewerAnnotClick(Sender: TObject; APage, AnnotIndex: Integer;
  const Subtype: WideString);
begin
  if Subtype = 'FileAttachment' then
    if FViewer.SaveAnnotAttachmentToFile(APage, AnnotIndex,
        'C:\Temp\attached-invoice.xml') = 1 then
      StatusBar1.SimpleText := 'Attachment saved';
end;

Drucken und die Grenzen, die Sie einplanen sollten

Das Drucken erfordert keine separate Integration. PrintDoc zeigt den standardmäßigen Windows-Druckdialog an – Druckerauswahl, Seitenbereich, Kopien, Sortierung – und übergibt den Auftrag an die Druck-Engine der Bibliothek, während PrintDoc(False) das gesamte Dokument ohne Benutzeroberfläche an den Standarddrucker sendet. PrintDocRange arbeitet vollständig programmatisch mit einem expliziten Druckernamen, Start- und Endseite, Kopienanzahl und Sortierungs-Flag, was der richtige Einstiegspunkt für stapel- oder servergesteuerte Druckaufträge ist, die von derselben Viewer-Instanz aus initiiert werden

Zwei Grenzen verdienen eine ehrliche Erwähnung. Erstens ist TPDFlibViewer ein Windows-VCL-Steuerelement. Die Viewer-Schicht ist von NOVCL-Builds ausgeschlossen, daher sollten Konsolendienste und Nicht-VCL-Ziele direkt die zugrunde liegende TPDFlib-Rendering-API verwenden. Zweitens wird die kopflose (headless) Nutzung bewusst unterstützt, ist jedoch begrenzt: CaptureRegion rendert jeden Client-Bereich – Seiten plus Hervorhebungen – in eine vom Aufrufer verwaltete Bitmap und funktioniert auf einem elternlosen Steuerelement ohne Fenster-Handle, da der Zeichenpfad VCL-Aufrufe vermeidet, die die Handle-Erstellung erzwingen würden. Dies macht die Überprüfung auf Pixelebene in automatisierten Tests praktisch, aber alles, was tatsächlich eine Nachrichtenschleife erfordert, wie die interaktiven Mauswerkzeuge, gehört immer noch in eine echte UI-Sitzung. Auf der Leistungsseite verwaltet das Steuerelement einen Speicherseiten-Cache mit einem konfigurierbaren Budget über SetCacheLimit und kann gerenderte Seiten über Sitzungen hinweg beibehalten; der Artikel über den Viewer-Festplatten-Seitencache behandelt diese Schicht und ihr DPI-Verhalten pro Monitor im Detail

Zusammenfassend also fünf Schritte: Platzieren Sie das Steuerelement und rufen Sie LoadFromFile auf, hängen Sie die fertigen Aktionen aus PDFlibViewerActions an, rufen Sie HighlightSearchHits für die Suche auf der Seite auf, verarbeiten Sie OnAnnotClick für Anmerkungsworkflows und machen Sie PrintDoc verfügbar. Jedes hier gezeigte Teil ist in der Standard-losLab PDF Library für Delphi, wo das Viewer-Steuerelement, die Aktionsklassen und die Rendering-Engine zusammen installiert werden