Eine einzelne DIN-A4-Seite, die mit einem angenehmen Lese-Zoom gerrendered wird, benötigt etwa einige Megabyte eines 32-Bit-Bitmaps. Multipliziert man das mit einem 400-seitigen Vertrag, wird die Rechnung schnell konkret: Wer jede Seite im Voraus rendert, fordert von Windows weit über ein Gigabyte an Bitmaps an, die sich der Benutzer ohnehin nur bildschirmweise ansieht. Die Anwendung geht entweder in einer 32-Bit-Build aufgrund des fehlenden Adressraums in die Knie oder verbringt die ersten Sekunden im eingefrorenen Zustand, während GPU und Seiten-Parser sich durch Seiten quälen, zu denen noch gar niemand gescrollt hat. Ein Reader mit fortlaufendem Bildlauf muss sich wie ein einziges langes Band anfühlen, darf aber nicht alle Seiten gleichzeitig im Speicher vorhalten
Dieses Spannungsfeld ist das Kernproblem. Die PDFium-Komponente löst dies innerhalb von TPdfView, sodass die meiste Arbeit darin besteht, den richtigen Anzeigemodus auszuwählen und zu verstehen, was die Komponente für Sie erledigt. An den Stellen, die sie nicht automatisch übernimmt – wie das Anpassen von Seiten für einen Lese-Workflow und das Aufrechterhalten der Reaktionsfähigkeit bei schnellem Scrollen –, zahlt sich etwas Code schnell aus. Wenn Sie noch dabei sind, die umgebenden Steuerelemente (Symbolleiste, Miniaturansichten, Suchfeld) zusammenzubauen, deckt die Anleitung zum Erstellen eines funktionsreichen Viewers diesen Bereich ab; hier liegt der Fokus auf dem Bildlauf selbst
Das Layout ist ein Anzeigemodus, kein Bitmap-Panel
Der VCL-Instinkt verleitet dazu, nach einer Scroll-Box zu greifen und darin Bild-Steuerelemente zu stapeln, eines pro Seite. Widerstehen Sie diesem Impuls. Dieses Design zwingt Sie dazu, die Seitenpositionierung, die Scroll-Mathematik und die Speicherverwaltung selbst zu übernehmen, und Sie werden jeden dieser Punkte schlecht neu erfinden. TPdfView modelliert das Dokument bereits als fortlaufende Seitenfolge 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; // one page wide, scrolls vertically
Pdf.FileName := 'contract.pdf';
Pdf.Active := True;
if not Pdf.Active then
ShowMessage('Could not open the document');
Das ist die gesamte Einrichtung für den fortlaufenden Bildlauf. dmSingleContinuous ordnet die Seiten in einer einzigen vertikalen Spalte an, wobei die Abstände dazwischen intern verwaltet werden, und die Ansicht scrollt durch diese Spalte als eine einzige Oberfläche. Es muss kein Steuerelement pro Seite verdrahtet und kein Scroll-Handler für die normale Navigation geschrieben werden. Beachten Sie die Prüfung auf Pdf.Active nach der Zuweisung: Das Öffnen eines Dokuments löst niemals eine Exception aus, sodass eine beschädigte oder passwortgeschützte Datei Active auf False belässt. Ein Viewer, der diese Prüfung überspringt, rendert ein leeres Panel und sucht den Fehler bei sich selbst
Dieselbe Eigenschaft stellt auch die Doppelseiten-Modi bereit. dmTwoPageContinuous platziert die Seiten nebeneinander, zwei pro Reihe, für das buchähnliche Lesen, das manche Dokumente erfordern; dmTwoPageContinuousWithCover tut dasselbe, lässt aber Seite eins als Cover allein stehen, sodass die übrigen Doppelseiten auf die natürliche Gerade-Ungerade-Grenze fallen. Alle drei Modi scrollen fortlaufend. Das Umschalten zwischen ihnen ist eine einfache Zuweisung, wodurch sich eine Display-Mode-Combobox später leicht hinzufügen lässt
Nur die sichtbaren Seiten werden gerastert
Der Grund, warum dies für eine 400-seitige Datei skaliert, liegt darin, dass die Spalte virtuell ist. TPdfView kennt die Höhe jeder Seite aus dem Seitenbaum des Dokuments, sodass die gesamte Scroll-Breite und die Position jeder Seite berechnet werden können, ohne irgendetwas zu rastern. Die Rasterung – der teure Schritt, der den Content-Stream einer Seite in Pixel verwandelt – erfolgt nur für die Seiten, die sich derzeit mit dem Viewport schneiden, plus einen kleinen Pufferbereich, damit eine Seite bereit ist, sobald sie ins Bild gescrollt wird. Wenn Sie nach unten scrollen, werden Seiten, die in den Viewport eintreten, gerendert, und Seiten, die ihn verlassen, geben ihre Bitmaps frei. Der Speicherbedarf bleibt proportional zu dem, was auf den Bildschirm passt, nicht zur Länge des Dokuments
Dies sollte man verinnerlichen, da es die Denkweise über Aufwände verändert. Das Öffnen eines 400-seitigen Dokuments ist billig: Es parst die Struktur, nicht den Inhalt. Der Aufwand fällt pro Seite an und wird verzögert (lazy) in dem Moment erbracht, in dem eine Seite in die Nähe des sichtbaren Bereichs gescrollt wird. Ein Viewer, der sich beim Öffnen sofort einsatzbereit anfühlt und beim Scrollen flüssig läuft, leistet insgesamt nicht weniger Arbeit; er verteilt sie lediglich auf den tatsächlichen Lesepfad des Benutzers und verwirft, was dahinter zurückbleibt. Die praktische Konsequenz ist, dass Sie fast nie versuchen sollten, Seiten im Voraus vor dem Benutzer zu rendern. Überlassen Sie der Ansicht die Entscheidung, was sichtbar ist
Seiten an die Breite anpassen und den Zoom unverändert lassen
Eine Lesespalte benötigt Seiten, die an die Breite des Panels angepasst sind, nicht an einen absoluten Zoom-Wert. FitMode übernimmt dies und behält die Anpassung auch bei einer Größenänderung des Fensters bei:
PdfView.FitMode := pfmFitWidth; // each page fills the column width; height follows
Mit pfmFitWidth berechnet die Komponente den Zoom bei jeder Größenänderung der Ansicht neu, sodass die Spalte immer die verfügbare Breite ausfüllt und die Seitenhöhen sowie die Scroll-Breite daraus folgen. Es gibt eine Falle, die oft übersehen wird: Die direkte Zuweisung von Zoom setzt FitMode wieder auf pfmNone zurück. Das ist Absicht, da ein manueller Zoom und eine automatische Anpassung gegensätzliche Absichten darstellen. Es bedeutet jedoch, dass ein versehentliches PdfView.Zoom := 1.0 an irgendeiner Stelle in Ihrem Code die Breitenanpassung lautlos deaktiviert und der nächste Größenänderungsvorgang keinen Reflow mehr auslöst. Wenn Sie sowohl eine Zoom-Steuerung als auch eine Anpassungstaste anbieten, behandeln Sie diese als Moduswechsel: Das Aktivieren des einen deaktiviert das andere, und Sie entscheiden, was Vorrang hat
Für absolute Zoom-Steuerungen, die sich natürlich verhalten, stellt die Ansicht die passenden Zoom-Stufen als Werte bereit, die Sie anwenden oder anzeigen können: PageWidthZoom[PageNumber] gibt den Zoom zurück, der diese Seite in die Breite einpassen würde, und das passende PageZoom passt die gesamte Seite ein. Das Auslesen dieser Werte ist der Weg, wie Sie ein Menü für „Breite anpassen“ / „Seite anpassen“ füllen, ohne feste Prozentwerte festzuschreiben, die bei Querformat oder übergroßen Seiten fehlschlagen
Schnelles Scrollen mit progressivem Rendern reaktionsschnell halten
Der Standard-Renderpfad zeichnet eine Seite vollständig fertig, bevor er zurückkehrt. Für eine einzelne Seite ist das in Ordnung. Beim schnellen Durchblättern eines dichten Dokuments ist dies jedoch nicht der Fall: Jede vorbeirauschende Seite löst eine vollständige Rasterung aus. Wenn der Benutzer schneller scrollt, als Seiten gerendert werden können, stauen sich diese Rendervorgänge auf, und das Panel gerät ins Stocken, da Arbeit für Seiten geleistet wird, die beim Abschluss des Renderns bereits wieder vom Bildschirm verschwunden sind. Die Lösung besteht darin, den Rendervorgang abbrechbar zu machen und ihn abzubrechen, sobald der Benutzer weiterscrollt
RenderPageProgressive rendert in Abschnitten (Chunks) und prüft an jeder Abschnittsgrenze ein Cancellation-Token, sodass ein laufendes Rendern einer Seite, die gerade weggescrollt wurde, abgebrochen werden kann, anstatt es bis zum Ende auszuführen:
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
// Cancel whatever was rendering; the old token is now signaled.
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 is complete, paint it
prsCancelled: Exit; // superseded, discard this result
prsFailed: ShowMessage('Render failed for page ' + IntToStr(PageNo));
end;
end;
Die entscheidende Form ist der Rückgabewert. prsDone bedeutet, dass das Bitmap vollständig gezeichnet wurde und auf dem Bildschirm dargestellt werden kann; prsCancelled bedeutet, dass eine neuere Scrollposition diese Seite überholt hat, sodass Sie das Teilergebnis verwerfen, anstatt es anzuzeigen; prsFailed steht für einen echten Fehler auf dieser Seite. Der Abbruch wird an den Abschnittsgrenzen abgefragt (polled) und erfolgt nicht präventiv, rechnen Sie also mit einigen Dutzend Millisekunden Latenz zwischen dem Aufruf von Cancel und dem tatsächlichen Stoppen des Renderns. Das ist immer noch weitaus günstiger, als ein veraltetes ganzseitiges Rendern die Warteschlange blockieren zu lassen. Die Übergabe von nil als Token führt das Rendern bis zur Fertigstellung durch, was die richtige Wahl für ein einmaliges Rendern wie eine Druckvorschau ist, bei der es nichts abzubrechen gibt
Wenn Sie stattdessen die Funktionsform von RenderPage aufrufen, die ein neues TBitmap zurückgibt, denken Sie daran, dass der Aufrufer dieses besitzt und es freigeben (Free) muss. In einer Scrollschleife, die ein Bitmap pro Seite belegt, ist das Vergessen dieser Freigabe ein Speicherleck, das mit jeder vom Benutzer überflogenen Seite wächst – genau das Fehlerszenario eines unbegrenzten Speicherbedarfs, das das fortlaufende Design eigentlich vermeiden sollte. Verwenden Sie nach Möglichkeit ein wiederverwendetes Bitmap für das Rendern
Was am Ende übrig bleibt
Den fortlaufenden Bildlauf übernimmt größtenteils die Komponente für Sie. Sie wählen dmSingleContinuous für das Layout, setzen pfmFitWidth, damit sich die Spalte bei Größenänderungen des Fensters anpasst, und prüfen Pdf.Active, damit eine fehlerhafte Datei deutlich fehlschlägt. Der einzige Teil, den Sie selbst schreiben sollten, ist das abbrechbare Rendern, da ein Reader danach beurteilt wird, wie er sich verhält, wenn jemand den Schieberegler an das Ende eines langen Dokuments zieht und das Panel entweder mithalten kann oder nicht. Alles darüber hinaus – Textauswahl über Seiten hinweg, Suchhervorhebung, ein Lesezeichenbaum – ist Benutzeroberflächen-Arbeit, die auf dieser Scroll-Oberfläche aufsetzt, anstatt in ihr implementiert zu sein
Die hier gezeigten APIs TPdfView, DisplayMode und RenderPageProgressive sind Teil der PDFium-Komponente für Delphi und Lazarus