Eine einzelne A4-Seite, bei einem angenehmen Lesezoom gerendert, belegt einige Megabyte als 32-Bit-Bitmap. Multipliziert man das mit einem 400-seitigen Vertrag, wird die Rechnung konkret: Alle Seiten im Voraus zu rendern bedeutet, Windows nach weit mehr als einem Gigabyte an Bitmaps zu fragen, die sich der Benutzer immer nur bildschirmweise anschaut. Die Anwendung entweder geht dem 32-Bit-Build der Adressraum aus, oder sie friert in den ersten Sekunden ein, während CPU und Seiten-Parser Seiten durcharbeiten, zu denen der Benutzer noch gar nicht gescrollt hat. Ein Reader mit kontinuierlichem Scrollen muss sich wie ein einziges, hohes Band aus Seiten anfühlen, darf aber nicht alle gleichzeitig im Speicher halten.
Diese Spannung ist das eigentliche Problem. PDFium VCL löst es intern in TPdfView, sodass die meiste Arbeit darin besteht, den richtigen Anzeigemodus zu wählen und zu verstehen, was die Komponente im Hintergrund tut. Die Punkte, bei denen sie nicht einspringt – Seiten für einen Lesefluss bemessen und schnelles Scrollen reaktionsfähig halten – sind jene, wo ein wenig Code seinen Wert beweist. Wer noch die umgebende Oberfläche zusammenstellt (Symbolleiste, Miniaturansichten, Suchfeld), findet dafür in der Anleitung für einen funktionsreichen Viewer Orientierung; hier ist das Scrollen selbst das Thema.
Das Layout ist ein Anzeigemodus, kein Bedienelemente-Stapel
Der Instinkt aus der VCL-Formularentwicklung ist es, zu einer Scrollbox zu greifen und darin Image-Steuerelemente zu stapeln, eines pro Seite. Diesem Impuls sollte man widerstehen. Dieses Design zwingt einen dazu, Seitenpositionierung, Scroll-Arithmetik und die Speicherfrage gleichzeitig zu lösen – und jede davon wird man schlecht neu erfinden. TPdfView modelliert das Dokument bereits als eine ununterbrochene Abfolge von Seiten und stellt das Layout über die Eigenschaft DisplayMode bereit.
Pdf := TPdf.Create(Self);
PdfView := TPdfView.Create(Self);
PdfView.Parent := Self;
PdfView.Align := alClient;
PdfView.Pdf := Pdf;
PdfView.DisplayMode := dmSingleContinuous; // eine Seite breit, scrollt vertikal
Pdf.FileName := 'vertrag.pdf';
Pdf.Active := True;
if not Pdf.Active then
ShowMessage('Das Dokument konnte nicht geöffnet werden');
Das ist die gesamte Einrichtung für kontinuierliches Scrollen. dmSingleContinuous ordnet die Seiten in einer einzelnen vertikalen Spalte an, wobei die Abstände zwischen ihnen intern verwaltet werden, und die Ansicht scrollt durch diese Spalte als eine einzige Fläche. Es gibt kein seitenspezifisches Steuerelement zu verdrahten und keinen Scroll-Handler für die gewöhnliche Navigation zu schreiben. Beachten Sie die Prüfung von Pdf.Active nach der Zuweisung: Das Öffnen eines Dokuments löst nie eine Exception aus, sodass eine beschädigte oder passwortgeschützte Datei Active auf False belässt, ohne dass eine Exception abgefangen werden kann. Ein Viewer, der diese Prüfung weglässt, rendert ein leeres Panel und sucht den Fehler an der falschen Stelle.
Dieselbe Eigenschaft enthält auch die Doppelseiten-Modi. dmTwoPageContinuous platziert Seiten nebeneinander, zwei pro Zeile, für die buchähnliche Lektüre, die manche Dokumente erfordern; dmTwoPageContinuousWithCover macht dasselbe, lässt aber die erste Seite als Einzelseite stehen, sodass die verbleibenden Doppelseiten auf die natürliche gerade-ungerade-Grenze fallen. Alle drei scrollen kontinuierlich. Der Wechsel zwischen ihnen ist eine einzige Zuweisung, was das spätere Hinzufügen einer Auswahlbox für den Anzeigemodus zum Kinderspiel macht.
Nur die sichtbaren Seiten werden gerastert
Der Grund, weshalb dies bei einer 400-seitigen Datei skaliert, liegt darin, dass die Spalte virtuell ist. TPdfView kennt die Höhe jeder Seite aus dem Seitenbaum des Dokuments und kann daher die Gesamtausdehnung des Scrollbereichs sowie die Position jeder Seite berechnen, ohne irgendetwas zu rastern. Das Rastern – der aufwendige Schritt, der den Inhaltsstrom einer Seite in Pixel umwandelt – findet nur für Seiten statt, die aktuell den Anzeigebereich schneiden, plus einem kleinen Puffer, damit eine Seite bereit ist, bevor sie ins Bild scrollt. Beim Scrollen nach unten werden eintretende Seiten gerendert, und die Bitmaps von Seiten, die den Bereich verlassen, werden freigegeben. Der Speicherverbrauch bleibt proportional zum, was auf den Bildschirm passt – nicht zur Dokumentlänge.
Das ist wichtig zu verinnerlichen, weil es ändert, wie man über Kosten nachdenkt. Ein 400-seitiges Dokument zu öffnen ist günstig: Es wird die Struktur, nicht der Inhalt geparst. Die Kosten fallen pro Seite an und werden verzögert fällig – in dem Moment, wenn eine Seite in die Nähe des Sichtbereichs scrollt. Ein Viewer, der sich beim Öffnen sofort und beim Scrollen flüssig anfühlt, verrichtet insgesamt nicht weniger Arbeit; er verteilt sie auf den tatsächlichen Lesepfad des Benutzers und gibt zurückliegende Seiten frei. Die praktische Konsequenz: Man sollte Seiten vor dem Benutzer so gut wie nie erzwungen rendern. Man lässt die Ansicht entscheiden, was sichtbar ist.
Seiten auf die Breite anpassen und den Zoom in Ruhe lassen
Eine Lesespalte möchte Seiten, die an die Breite des Panels angepasst sind, nicht auf einen absoluten Zoom fixiert. FitMode erledigt das und tut es auch nach einer Fenstergrößenänderung erneut.
PdfView.FitMode := pfmFitWidth; // jede Seite füllt die Spaltenbreite; Höhe folgt
Mit pfmFitWidth berechnet die Komponente den Zoom neu, sobald die Ansicht ihre Größe ändert, sodass die Spalte immer die verfügbare Breite ausfüllt und die Seitenhöhen – und damit der Scrollbereich – daraus folgen. Es gibt eine Falle, die viele trifft: Ein direktes Zuweisen von Zoom setzt FitMode auf pfmNone zurück. Das ist beabsichtigt, weil ein manueller Zoom und eine automatische Anpassung einander widersprechende Absichten sind. Aber es bedeutet, dass ein unbeabsichtigtes PdfView.Zoom := 1.0 irgendwo im Code die Breitenpassung lautlos ausschaltet und die nächste Fenstergrößenänderung den Umbruch stoppt. Wer sowohl ein Zoom-Steuerelement als auch eine Anpassen-Schaltfläche anbietet, sollte beides als Modusschalter behandeln: Das Setzen des einen löscht das andere, und man entscheidet, was Vorrang hat.
Für absolute Zoom-Steuerelemente, die sich natürlich lesen lassen, stellt die Ansicht die Anpassungs-Zoomwerte als lesbare Werte bereit: PageWidthZoom[PageNumber] gibt den Zoom zurück, der diese Seite an die Breite anpassen würde, und das entsprechende PageZoom passt die gesamte Seite ein. Diese Werte zu lesen ist die Art, ein Menü „Breite anpassen" / „Seite anpassen" zu befüllen, ohne magische Prozentwerte fest zu verdrahten, die bei Quer- oder übergroßen Seiten falsch liegen.
Flüssiges Scrollen mit progressivem Rendering
Der Standard-Renderpfad zeichnet eine Seite vollständig, bevor er zurückkehrt. Für eine einzelne Seite ist das in Ordnung. Bei einem schnellen Wischen durch ein dichtes Dokument nicht: Jede vorbeifliegende Seite löst ein vollständiges Rastern aus, und scrollt der Benutzer schneller als Seiten gerendert werden können, häufen sich diese Render-Aufgaben auf, und das Panel ruckelt, weil Arbeit für Seiten geleistet wird, die beim Abschluss längst nicht mehr sichtbar sind. Die Lösung besteht darin, einen Render abbrechbar zu machen und ihn aufzugeben, sobald der Benutzer weiterfährt.
RenderPageProgressive rendert in Blöcken und prüft an jeder Blockgrenze ein Abbruch-Token; ein laufender Render einer Seite, die gerade weggescrollt wurde, kann daher verworfen werden statt bis zum Ende zu laufen.
type
TFormMain = class(TForm)
// ...
private
FRenderCancel: IPdfCancellationTokenSource;
procedure RenderPageToBitmap(PageNo: Integer; Bmp: TBitmap);
end;
procedure TFormMain.RenderPageToBitmap(PageNo: Integer; Bmp: TBitmap);
var
Status: TPdfProgressiveStatus;
begin
// Laufenden Render abbrechen; das alte Token ist nun signalisiert.
if Assigned(FRenderCancel) then
FRenderCancel.Cancel;
FRenderCancel := TPdfCancellationTokenSource.New;
Pdf.PageNumber := PageNo;
Status := Pdf.RenderPageProgressive(Bmp, 0, 0, Bmp.Width, Bmp.Height,
FRenderCancel.Token);
case Status of
prsDone: ; // Bitmap vollständig, anzeigen
prsCancelled: Exit; // veraltet, Ergebnis verwerfen
prsFailed: ShowMessage('Render fehlgeschlagen für Seite ' + IntToStr(PageNo));
end;
end;
Entscheidend ist der Rückgabewert. prsDone bedeutet, die Bitmap ist vollständig gemalt und es lohnt sich, sie auf den Bildschirm zu übertragen; prsCancelled bedeutet, eine neuere Scrollposition hat diese Seite verdrängt, und man verwirft das Teilergebnis statt es anzuzeigen; prsFailed ist ein echter Fehler bei dieser Seite. Der Abbruch wird an Blockgrenzen abgefragt, nicht präemptiv; zwischen dem Aufruf von Cancel und dem tatsächlichen Stopp des Renders sind einige Zehntel-Millisekunden Latenz zu erwarten. Das ist immer noch weit günstiger als einen veralteten vollständigen Seitenrender die Warteschlange blockieren zu lassen. Wird nil als Token übergeben, läuft der Render bis zum Abschluss durch – die richtige Wahl für einen Einzel-Render wie eine Druckvorschau, bei der es nichts abzubrechen gibt.
Was am Ende bleibt
Der Reader mit kontinuierlichem Scrollen ist größtenteils Aufgabe der Komponente. Man wählt dmSingleContinuous für das Layout, setzt pfmFitWidth, damit die Spalte mit dem Fenster umfließt, und prüft Pdf.Active, damit eine fehlerhafte Datei laut scheitert. Der einzige Teil, der sich zu schreiben lohnt, ist abbrechierbares Rendering, denn ein Reader wird daran gemessen, wie er sich verhält, wenn jemand die Scrollleiste eines langen Dokuments nach unten zieht – ob das Panel mitkommt oder nicht. Alles darüber hinaus – Textauswahl über Seiten, Such-Hervorhebungen, ein Lesezeichen-Baum – ist Interface-Arbeit, die über dieser Scroll-Fläche liegt, nicht in ihr.
Die hier gezeigten APIs TPdfView, DisplayMode und RenderPageProgressive sind Teil der PDFium VCL-Komponente für Delphi und Lazarus.