Pojedyncza strona formatu A4 wyrenderowana przy wygodnym powiększeniu to około kilku megabajtów 32-bitowej bitmapy. Pomnóż to przez 400-stronicową umowę, a obliczenia przestaną być czystą teorią: renderując każdą stronę z góry, żądasz od systemu Windows ponad gigabajta bitmap, które użytkownik będzie przeglądał po jednym ekranie na raz. Aplikacja w wersji 32-bitowej szybko wyczerpie przestrzeń adresową, albo spędzi pierwsze kilka sekund w stanie zawieszenia, podczas gdy karta graficzna (GPU) i parser stron będą przetwarzać strony, do których nikt jeszcze nie przewinął. Czytnik z ciągłym przewijaniem (continuous-scroll) musi sprawiać wrażenie jednej długiej wstęgi stron, lecz w rzeczywistości nie może przechowywać ich wszystkich w pamięci jednocześnie
To wyzwanie to kluczowy problem. Komponent PDFium Component rozwiązuje go wewnątrz klasy TPdfView, stąd większość pracy sprowadza się do wyboru właściwego trybu wyświetlania i zrozumienia, co komponent wykonuje automatycznie. Zadania, których komponent nie realizuje samodzielnie — czyli dopasowanie rozmiarów stron do układu tekstu oraz zapewnienie płynności przy szybkim przewijaniu — wymagają napisania kilku linii kodu. Jeśli budujesz pozostałe elementy aplikacji (pasek narzędzi, miniatury, wyszukiwarkę), artykuł o budowaniu zaawansowanej przeglądarki PDF opisuje te zagadnienia; tutaj skupimy się na samym przewijaniu
Układ to tryb wyświetlania, a nie panel bitmap
Nawyk z pracy z formularzami VCL podpowiada użycie komponentu scroll box i umieszczenie w nim kontrolek obrazów (Image), po jednej na każdą stronę. Unikaj tego rozwiązania. Zmusza ono do samodzielnego pozycjonowania stron, obliczeń przewijania oraz zarządzania pamięcią, co oznacza ponowne (i zazwyczaj nieoptymalne) pisanie tych mechanizmów. Klasa TPdfView modeluje dokument jako ciągły ciąg stron i udostępnia ten układ poprzez właściwość DisplayMode
Pdf := TPdf.Create(Self);
PdfView := TPdfView.Create(Self);
PdfView.Parent := Self;
PdfView.Align := alClient;
PdfView.Pdf := Pdf;
PdfView.DisplayMode := dmSingleContinuous; // szerokość jednej strony, przewijanie pionowe
Pdf.FileName := 'contract.pdf';
Pdf.Active := True;
if not Pdf.Active then
ShowMessage('Nie udało się otworzyć dokumentu');
To kompletna konfiguracja ciągłego przewijania. Tryb dmSingleContinuous układa strony w pojedynczej pionowej kolumnie z odstępami obsługiwanymi automatycznie, a widok przewija tę kolumnę jako jedną płaszczyznę. Nie ma potrzeby konfigurowania osobnych stron ani pisania kodu obsługi przewijania przy zwykłej nawigacji. Zwróć uwagę na sprawdzenie stanu Pdf.Active po przypisaniu: otwarcie dokumentu nie wywołuje wyjątków, więc uszkodzony lub zabezpieczony hasłem plik pozostawi właściwość Active w stanie False bez zgłaszania błędów, a program pomijający ten test wyrenderuje pusty panel
Ta sama właściwość obsługuje również tryby rozkładówki. Opcja dmTwoPageContinuous układa strony obok siebie, po dwie w rzędzie, co ułatwia czytanie w stylu książki; z kolei dmTwoPageContinuousWithCover działa podobnie, lecz pozostawia pierwszą stronę jako okładkę, dzięki czemu pozostałe rozkładówki wypadają na naturalnej granicy parzystości stron. Wszystkie trzy opcje przewijają się w sposób ciągły. Przełączanie między nimi to kwestia jednej przypisania, co ułatwia dodanie odpowiedniej listy wyboru w przyszłości
Tylko widoczne strony są rasteryzowane
Powodem, dla którego rozwiązanie to sprawdza się przy 400-stronicowych plikach, jest wirtualny charakter kolumny. Klasa TPdfView zna wysokość każdej strony na podstawie drzewa stron dokumentu, stąd potrafi wyliczyć całkowity zakres przewijania i pozycję każdej strony bez konieczności ich rasteryzacji. Rasteryzacja — kosztowny proces przekształcający strumień zawartości strony na piksele — odbywa się wyłącznie dla stron, które aktualnie znajdują się w obszarze widocznym, z niewielkim marginesem bezpieczeństwa, aby strona była gotowa w momencie jej przewinięcia. Podczas przewijania w dół strony wchodzące w obszar widoku są renderowane, a strony go opuszczające są zwalniane z pamięci. Zużycie pamięci zależy wyłącznie od liczby widocznych stron, a nie od całkowitej długości dokumentu
Warto to zapamiętać, ponieważ zmienia to podejście do optymalizacji wydajności. Otwarcie 400-stronicowego dokumentu jest szybkie: parser analizuje strukturę, a nie samą zawartość. Koszt procesora i pamięci jest ponoszony dla poszczególnych stron z opóźnieniem (lazy loading), dopiero w momencie ich przewinięcia. Przeglądarka, która otwiera się natychmiastowo i przewija płynnie, nie wykonuje mniej pracy — po prostu rozkłada ją wzdłuż ścieżki czytania użytkownika, odrzucając elementy niepotrzebne. W praktyce oznacza to, że nie należy wymuszać renderowania stron z wyprzedzeniem. Pozwól kontrolce widoku decydować o tym, co jest widoczne
Dopasuj strony do szerokości, a powiększenie pozostaw bez zmian
Wygodny podgląd wymaga dopasowania stron do szerokości panelu, a nie przypisania stałego powiększenia. Właściwość FitMode obsługuje to zachowanie i dostosowuje widok podczas zmiany rozmiaru okna
PdfView.FitMode := pfmFitWidth; // każda strona wypełnia szerokość kolumny; wysokość dopasowuje się automatycznie
Dzięki opcji pfmFitWidth komponent przelicza powiększenie przy każdej zmianie rozmiaru okna, stąd kolumna zawsze wypełnia dostępną szerokość, a wysokość stron (i tym samym zakres przewijania) dostosowuje się automatycznie. Istnieje tu jednak pewna pułapka: ręczne przypisanie wartości Zoom resetuje właściwość FitMode do pfmNone. Jest to celowe działanie, ponieważ ręczne powiększenie i automatyczne dopasowanie wykluczają się nawzajem, lecz oznacza to, że przypadkowe wywołanie typu PdfView.Zoom := 1.0 w kodzie cicho wyłączy dopasowanie do szerokości. Jeśli oferujesz zarówno kontrolę powiększenia, jak i przycisk dopasowania, traktuj te funkcje jako przełącznik trybu: włączenie jednej opcji wyłącza drugą
Na profesjonalny start, dla absolutnej kontroli powiększenia kontrolka udostępnia wartości dopasowania jako gotowe parametry: właściwość PageWidthZoom[PageNumber] zwraca stopień powiększenia wymagany do dopasowania danej strony do szerokości, a PageZoom dopasowuje całą stronę. Odczyt tych wartości pozwala na łatwe oprogramowanie opcji „Dopasuj do szerokości” / „Dopasuj do strony” bez wpisywania sztywnych wartości procentowych, które nie sprawdzą się przy stronach poziomych lub o niestandardowym formacie
Zapewnienie płynności przy szybkim przewijaniu dzięki renderowaniu progresywnemu
Domyślny proces rysuje stronę do końca przed zwróceniem sterowania. Dla pojedynczej strony to poprawne zachowanie, lecz przy szybkim przewijaniu długiego dokumentu już nie: każda przewijana strona inicjuje pełną rasteryzację. Jeśli użytkownik przewija plik szybciej, niż trwa generowanie stron, zadania te gromadzą się, co powoduje zacinanie obrazu (praca jest wykonywana dla stron, które zdążyły już opuścić ekran). Rozwiązaniem jest możliwość przerwania procesu renderowania i porzucenie go, gdy użytkownik przewinie ekran dalej
Funkcja RenderPageProgressive renderuje obraz w częściach i sprawdza znacznik przerwania (cancellation token) na granicy każdej z nich. Dzięki temu proces generowania strony, która właśnie opuściła ekran, może zostać przerwany przed jego zakończeniem
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
// Przerwij dotychczasowe renderowanie; stary znacznik zostanie zasygnalizowany.
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: ; // bitmapa jest kompletna, narysuj ją
prsCancelled: Exit; // przestarzałe, odrzuć ten wynik
prsFailed: ShowMessage('Błąd renderowania strony ' + IntToStr(PageNo));
end;
end;
Kluczowym elementem jest zwracana wartość. Status prsDone oznacza, że bitmapa została w pełni wyrenderowana i można ją wyświetlić; prsCancelled oznacza, że nowa pozycja przewijania zastąpiła tę stronę, stąd częściowy wynik należy odrzucić; natomiast prsFailed sygnalizuje błąd na stronie. Przerwanie renderowania jest sprawdzane na granicach bloków, stąd opóźnienie między wywołaniem Cancel a rzeczywistym zatrzymaniem procesu wynosi kilkadziesiąt milisekund. To jednak znacznie tańsze rozwiązanie niż blokowanie kolejki przez renderowanie niepotrzebnych stron. Przekazanie wartości nil jako znacznika powoduje renderowanie do końca, co jest odpowiednim wyborem przy generowaniu jednorazowym (np. w podglądzie wydruku), gdzie nie ma potrzeby przerywania operacji
W przypadku wywołania funkcji RenderPage zwracającej nowy obiekt TBitmap pamiętaj, że wywołujący przejmuje go na własność i musi go zwolnić za pomocą Free. W pętli przewijania alokującej osobną bitmapę dla każdej strony zapomnienie o tym spowoduje wyciek pamięci rosnący z każdą przewiniętą stroną — a to dokładnie ten rodzaj problemu, przed którym miało chronić przewijanie ciągłe. W miarę możliwości renderuj zawartość do ponownie używanej bitmapy
Co otrzymujesz w rezultacie
Obsługa czytnika z ciągłym przewijaniem leży głównie po stronie samego komponentu. Wybierasz tryb dmSingleContinuous dla układu, ustawiasz pfmFitWidth, aby kolumna dostosowywała się do okna, i sprawdzasz właściwość Pdf.Active, by uszkodzone pliki generowały błędy. Jedynym elementem, który warto napisać samodzielnie, jest obsługa przerywania renderowania, ponieważ jakość czytnika ocenia się po tym, jak zachowuje się, gdy użytkownik szybko przeciągnie suwak na sam dół długiego dokumentu. Wszystkie pozostałe funkcje — zaznaczanie tekstu na wielu stronach, podświetlanie wyników wyszukiwania czy drzewo zakładek — to elementy interfejsu działające na bazie tej płaszczyzny przewijania, a nie wewnątrz niej
Opisane tu interfejsy API TPdfView, DisplayMode oraz RenderPageProgressive wchodzą w skład PDFium Component dla Delphi i Lazarusa