Zwei gleichzeitig geöffnete Dokumente, dieselbe Seitenzahl, jedes in seinem eigenen scrollbaren Panel: Das ist der Kern eines Vergleichsviewers. PDFium Component liefert das über ein geradliniges Objektmodell, in dem TPdf die Datei besitzt und TPdfView die Anzeige. Ein Dokument, ein TPdf, ein TPdfView. Wollen Sie drei Panels, haben Sie drei Paare. Die schwierigen Teile sind nicht die API-Aufrufe; es sind die Layoutrechnungen bei einer Größenänderung des Fensters und die Logik der Seitensynchronisation, wenn Sie entscheiden, welche Ansicht welcher folgen soll
Formularlayout
Das VCL-Formular enthält drei TScrollBox-Container nebeneinander, jeder mit einer TPdfView darin, die auf alClient ausgerichtet ist und so die Box füllt. Zwei TSplitter-Komponenten sitzen zwischen den Boxen, damit der Benutzer die Spaltenbreiten zur Laufzeit anpassen kann. Eine Symbolleiste über den Panels trägt die Öffnen-Schaltflächen, die Zoom-Bedienelemente und den Umschalter zwischen Zwei- und Drei-Ansicht-Modus
Der Drei-Ansicht-Modus ist ein Boolean, den das Formular intern führt. Beim Umschalten berechnen Sie die Breiten neu und zeigen oder verbergen die dritte Spalte. Am einfachsten ist es, alle Align-Eigenschaften zu löschen, die Splitter zu verbergen und dann absolute Positionen zu setzen:
procedure TFormMain.UpdateLayout;
var
TotalWidth: Integer;
begin
TotalWidth := ClientWidth;
if ThreeViewMode then
begin
ScrollBox3.Visible := True;
ScrollBox1.Left := 0;
ScrollBox1.Width := TotalWidth div 3;
ScrollBox2.Left := ScrollBox1.Width;
ScrollBox2.Width := TotalWidth div 3;
ScrollBox3.Left := ScrollBox2.Left + ScrollBox2.Width;
ScrollBox3.Width := TotalWidth - ScrollBox3.Left;
// Denselben Wert (ClientHeight - Höhe der Symbolleiste) auf alle drei Height-Werte anwenden
end
else
begin
ScrollBox3.Visible := False;
ScrollBox1.Left := 0;
ScrollBox1.Width := TotalWidth div 2;
ScrollBox2.Left := ScrollBox1.Width;
ScrollBox2.Width := TotalWidth - ScrollBox2.Left;
end;
end;
Align := alNone auf allen drei Boxen zu setzen, bevor die ganzzahlige Rechnung läuft, verhindert, dass die Constraint-Engine der VCL gegen Ihre Zuweisungen arbeitet. Stellen Sie die Sichtbarkeit der Splitter nach der Positionierung wieder her, wenn Sie im Zwei-Ansicht-Modus das Ziehen zum Ändern der Breite wollen
Die Höhe jeder Scrollbox ist die Clientfläche abzüglich der Höhe des Symbolleisten-Panels. Da die Symbolleiste mit alTop oben angedockt ist, ergibt ClientHeight - PanelButtons.Height den nutzbaren vertikalen Raum. Weisen Sie diesen Wert innerhalb desselben UpdateLayout-Aufrufs allen drei Boxen zu, damit es nie ein Einzelbild gibt, in dem eine Box höher ist als die anderen und ein Layoutflackern verursacht
Ein Dokument öffnen
Jedes Panel-Paar braucht seine eigene Öffnen-Prozedur. Das Muster ist kurz: die Komponente deaktivieren, den Dateinamen setzen, aktivieren, dann Active prüfen; blieb es False, nach einem Passwort fragen und es erneut versuchen. Beachten Sie, dass TPdfView.Active das Rendern steuert, während TPdf.Active die Datei tatsächlich öffnet; die beiden sind unabhängig voneinander. PdfView.Active := True zu setzen, während die verknüpfte TPdf noch nicht aktiv ist, ist harmlos, zeigt aber nichts an
procedure TFormMain.OpenPdfFile(PdfComponent: TPdf;
PdfViewComponent: TPdfView);
var
Password: string;
begin
if not OpenDialog.Execute then
Exit;
PdfComponent.Active := False;
PdfComponent.FileName := OpenDialog.FileName;
PdfComponent.Password := '';
PdfComponent.Active := True;
// Ladefehler sind still: Active bleibt False, statt eine Ausnahme auszulösen.
if not PdfComponent.Active then
begin
// Höchstwahrscheinlich eine passwortgeschützte Datei; dem Benutzer einen Versuch geben.
if InputQuery('Password', 'Enter document password:', Password) then
begin
PdfComponent.Password := Password;
PdfComponent.Active := True;
end;
end;
if not PdfComponent.Active then
begin
ShowMessage('Could not open ' + OpenDialog.FileName +
' (damaged file or wrong password)');
Exit;
end;
PdfViewComponent.PageNumber := 1;
SetActivePdfView(PdfViewComponent);
end;
Prüfen Sie nach der Zuweisung immer PdfComponent.Active; eine beschädigte Datei oder ein falsches Passwort lässt das Laden im Standardpfad stillschweigend fehlschlagen, ohne eine Ausnahme auszulösen. PdfViewComponent.PageNumber := 1 nach einem erfolgreichen Öffnen ausdrücklich zu setzen verhindert eine veraltete Seitenzahl aus dem vorherigen Dokument
Der Meldungsdialog am Ende ist Absicht: Beschädigte oder nicht unterstützte Dateien sollen sofort auffallen, statt als stilles leeres Panel geschluckt zu werden. Wer nichts sieht, weiß nicht, ob die Datei geladen wurde und schlicht leer ist oder ob die Komponente sie abgelehnt hat. Den Fehlschlag zu melden hält den Fehler sichtbar
Verfolgen des aktiven Panels
Klickt der Benutzer in ein Panel, wird dieses Panel aktiv. Das Formular führt ein privates Feld FActivePdfView: TPdfView. Die visuelle Rückmeldung ist eine geänderte Rahmenfarbe der umgebenden TScrollBox: clHighlight für die aktive, clWindow für die übrigen. Verdrahten Sie das mit jedem TPdfView.OnClick und mit der Öffnen-Prozedur, damit der Fokus dem gerade geöffneten Dokument folgt
Manche Operationen gelten für alle sichtbaren Panels statt nur für das aktive. Ein Boolean FAllViewsMode am Formular steuert diesen Zweig. Ist er wahr, verteilen sich Zoomänderungen und Seitennavigation auf jedes Panel, das ein aktives Dokument hat:
procedure TFormMain.ApplyZoomToAll(NewZoom: Double);
begin
if PdfView1.Active then PdfView1.Zoom := NewZoom;
if PdfView2.Active then PdfView2.Zoom := NewZoom;
if ThreeViewMode and PdfView3.Active then PdfView3.Zoom := NewZoom;
end;
Synchronisierte Seitennavigation
Die synchronisierte Navigation ist optional, aber nützlich für Revisionsabläufe, in denen beide Dateien denselben Seitenbereich abdecken. Die Logik gehört in einen Ereignishandler, der auslöst, nachdem der Benutzer eine Ansicht navigiert hat. Ändert eine Quellansicht ihre PageNumber, gibt der Handler diese Zahl an die anderen Ansichten weiter, mit einer Absicherung: Die Zielansicht muss mindestens so viele Seiten haben, sonst wird sie übersprungen
Die PageNumber von TPdfView und die von TPdf sind unabhängig. TPdf.PageNumber verfolgt, welche Seite die Dokumentkomponente als aktuell ansieht; TPdfView.PageNumber verfolgt, was auf dem Bildschirm angezeigt wird. Für Navigationszwecke wollen Sie die Eigenschaft der Ansicht, nicht die des Dokuments
Ein Kontrollkästchen mit einer Beschriftung wie „Seiten synchronisieren“ gibt dem Benutzer die Kontrolle. Ist es nicht angehakt, navigiert jedes Panel unabhängig und der Handler kehrt sofort zurück. Diese Unabhängigkeit ist wichtig für Fälle, in denen die beiden Dokumente unterschiedliche Seitenzahlen haben oder in denen der Benutzer die entsprechende Passage in einer Übersetzung sucht, die auf einer anderen Seite beginnt. Die Synchronisation immer zu erzwingen würde das Werkzeug schwerer benutzbar machen als eine schlichte Anordnung aus zwei Fenstern auf dem Desktop
Eines ist zu beachten: PdfView.PageNumber im Synchronisationshandler programmatisch zu setzen löst das Änderungsereignis dieser Ansicht selbst wieder aus. Sichern Sie sich gegen unendliche Rekursion mit einem Boolean-Flag ab, das Sie vor der Zuweisung setzen und unmittelbar danach löschen. Das Flag gilt pro Formular, nicht pro Ansicht, weil sich alle drei Ansichten denselben Handler teilen
Zoom je Panel
Jede TPdfView trägt ihre eigene Eigenschaft Zoom, ein Double in Prozent, wobei Zoom := 100 die tatsächliche Größe (100 %) bedeutet. Sie zu setzen überschreibt jeden aktiven FitMode. Für eine Schaltfläche zur Breitenanpassung im aktiven Panel lesen Sie den passenden Zoom aus PdfView.PageWidthZoom[PdfView.PageNumber] und weisen ihn zu. Für die Seitenanpassung nehmen Sie PageZoom[PageNumber]. Beide sind Array-Eigenschaften mit 1-basiertem Seitenindex, sichern Sie sich also gegen eine Seitenzahl von null ab, bevor Sie darauf zugreifen
Wenn Sie die aktuelle Seite als Bild exportieren, lesen Sie die Drehung aus der Ansicht, rufen RenderPage aber auf der TPdf-Komponente auf, nicht auf der Ansicht. Die Bitmap-Form von TPdf.RenderPage nimmt explizite Pixelmaße sowie einen TRotation-Wert und eine TRenderOptions-Menge entgegen. Die Funktionsvariante gibt eine TBitmap im Besitz des Aufrufers zurück, die Sie nach dem Speichern selbst freigeben:
procedure TFormMain.SaveActiveViewAsImage;
var
Pdf: TPdf;
Bmp: TBitmap;
Jpeg: TJpegImage;
begin
if not Assigned(FActivePdfView) or not FActivePdfView.Active then
Exit;
Pdf := FActivePdfView.Pdf;
Pdf.PageNumber := FActivePdfView.PageNumber;
Bmp := Pdf.RenderPage(
0, 0,
Round(Pdf.PageWidth * 2),
Round(Pdf.PageHeight * 2),
FActivePdfView.Rotation, [], clWhite);
try
if SavePictureDialog.Execute then
begin
Jpeg := TJpegImage.Create;
try
Jpeg.Assign(Bmp);
Jpeg.CompressionQuality := 90;
Jpeg.SaveToFile(SavePictureDialog.FileName);
finally
Jpeg.Free;
end;
end;
finally
Bmp.Free;
end;
end;
Der Faktor 2 auf Breite und Höhe liefert schärfere Ausgaben bei Dokumenten mit feinem Text. Das try/finally um die Freigabe der Bitmap ist nicht optional; auch ein Abbruch im TSaveDialog läuft in den finally-Block, und die Bitmap soll unabhängig davon freigegeben werden, was der Benutzer getan hat
DLL-Voraussetzungen
PDFium Component kapselt die native pdfium-Bibliothek. Ein 32-Bit-Hostprozess braucht pdfium32.dll; ein 64-Bit-Host braucht pdfium64.dll. Varianten mit der JavaScript-Engine V8 tragen das Suffix v8 und wiegen rund 23-27 MB gegenüber den 5-6 MB der Standardbuilds. Für einen Vergleichsviewer, der das Formularausfüllen abschaltet (Pdf.FormFill := False), reicht der Standardbuild ohne V8 und hält die Distribution kleiner
Legen Sie die DLL in dasselbe Verzeichnis wie die ausführbare Datei oder in ein beliebiges Verzeichnis im System-PATH. Die Komponente lädt sie bei Bedarf, wenn die erste TPdf aktiviert wird, sodass eine fehlende DLL an dieser Stelle auffällt und nicht beim Start der Anwendung. Wenn Sie einen Installer ausliefern, ist es am verlässlichsten, die DLL bei der Installation in den Anwendungsordner zu kopieren, statt sich auf ein Systemverzeichnis zu verlassen, das ein Administrator später aufräumen könnte
Die V8-Builds sind vor allem dann nützlich, wenn Sie mit JavaScript-Aktionen im PDF interagieren müssen, etwa um Berechnungsfelder oder Submit-Handler auszulösen. Ein passiver Vergleichsviewer hat keinen Grund, JavaScript auszuführen; Pdf.FormFill := False vor Active := True zu setzen überspringt die Formularausfüllumgebung vollständig, was auch bedeutet, dass selbst beim Standardbuild keine JS-Engine initialisiert wird. Das ist unabhängig von der ausgelieferten DLL-Variante der richtige Standard für einen schreibgeschützten Viewer
Weitere Einzelheiten zur PDFium Component und ihrer vollständigen API finden Sie auf der Produktseite der Delphi PDFium Component