Jediná stránka A4 vykreslená pri pohodlnom priblížení na čítanie predstavuje niekoľko megabajtov 32-bitovej bitmapy. Vynásobte to zmluvou s rozsiahlych 400 stránkami a táto matematika prestane byť abstraktná: ak vykreslíte každú stránku vopred, budete od systému Windows požadovať vyše gigabajtu bitmap, ktoré si používateľ aj tak bude prezerať po jednej obrazovke. Aplikácia buď vyčerpá adresný priestor pri 32-bitovom zostavení, alebo strávi prvých niekoľko sekúnd zamrznutá, kým GPU a parser stránok spracujú listy, na ktoré ešte nikto neposunul zobrazenie. Prehliadač s plynulým posúvaním (continuous scroll) musí pôsobiť ako jedna dlhá stuha stránok, no nemôže ich všetky držať v pamäti naraz
Toto napätie predstavuje jadro problému. PDFium Component ho rieši vo vnútri triedy TPdfView, takže väčšina práce spočíva vo výbere správneho režimu zobrazenia a pochopení toho, čo komponent robí za vás. Časti, ktoré za vás nevyrieši, ako prispôsobenie veľkosti stránok toku čítania a zabezpečenie rýchlej odozvy pri posúvaní, sú miestom, kde malý kus kódu prinesie veľký úžitok. Ak stále zostavujete okolité prvky rozhrania (panel nástrojov, miniatúry, vyhľadávacie pole), sprievodca funkciami nabitým prehliadačom pokrýva túto tému; hier sa zameriame na samotné posúvanie
Rozvrhnutie je režim zobrazenia, nie panel s bitmapami
Pri práci s formulármi VCL býva inštinktom použiť komponent scroll box a doňho naskladať obrázkové prvky (image controls), jeden pre každú stránku. Odolajte tomu. Tento prístup vás núti riadiť pozicionovanie stránok, matematiku posúvania a otázku pamäte súčasne, pričom každú z týchto vecí reimplementujete len s ťažkosťami. Trieda TPdfView už modeluje dokument ako súvislý pás stránok a sprístupňuje toto rozvrhnutie prostredníctvom vlastnosti DisplayMode
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');
To je celé nastavenie plynulého posúvania. Režim dmSingleContinuous usporiada stránky do jedného zvislého stĺpca, pričom medzery medzi nimi rieši interne, a zobrazenie prechádza týmto stĺpcom ako jedným súvislým povrchom. Na bežnú navigáciu netreba prepájať žiadne ovládacie prvky pre jednotlivé stránky ani písať obsluhu posúvania. Všimnite si kontrolu vlastnosti Pdf.Active po priradení: otvorenie dokumentu nikdy nevyvolá výnimku, takže poškodený súbor alebo súbor chránený heslom ponechá Active na hodnote False bez výnimky na zachytenie, a prehliadač, ktorý túto kontrolu vynechá, vykreslí len prázdny panel a bude hľadať chybu v sebe
Rovnaká vlastnosť riadi aj režimy dvojstránok. dmTwoPageContinuous umiestňuje stránky vedľa seba, dve v jednom riadku, pre čítanie štýlom knihy, ktoré niektoré dokumenty vyžadujú; dmTwoPageContinuousWithCover robí to isté, no ponecháva prvú stránku samostatne ako obálku, takže zvyšné dvojstránky spadajú na prirodzenú hranicu párnych a nepárnych strán. Všetky tri režimy sa posúvily plynule. Prepínanie medzi nimi je otázkou jediného priradenia, takže pridanie výberového poľa pre režim zobrazenia je neskôr úplne triviálne
Rastrujú sa iba viditeľné stránky
Dôvod, prečo to funguje aj pri 400-stranovom súbore, spočíva v tom, že stĺpec je virtuálny. Trieda TPdfView pozná výšku každej stránky zo štruktúry stránok dokumentu, takže dokáže vypočítať celkový rozsah posúvania a pozíciu každej stránky bez toho, aby čokoľvek rastrovala. Rasterizácia, ten náročný krok, ktorý premieňa dátový prúd obsahu stránky na pixely, prebieha len pre stránky, ktoré sa práve nachádzajú vo výreze zobrazenia (viewport), plus malý okraj, aby bola stránka pripravená v momente, keď na ňu používateľ posunie zobrazenie. Pri posúvaní nadol sa stránky vstupujúce do výrezu vykreslia a stránkam, ktoré ho opúšťajú, sa uvoľnia bitmapy z pamäte. Spotreba pamäte tak zostáva úmerná tomu, čo sa zmestí na obrazovku, a nie dĺžke celého dokumentu
Toto je dôležité si uvedomiť, pretože to mení spôsob, akým uvažujete o nárokoch na výkon. Otvorenie 400-stranového dokumentu je nenáročné: analyzuje sa iba štruktúra, nie samotný obsah. Náklady sa platia len pre konkrétne stránky a to odložene (lazy), v momente, keď sa k nim používateľ posunie. Prehliadač, ktorý reaguje okamžite pri otvorení a posúva sa plynule, nevykonáva celkovo menej práce; len ju rozkladá na skutočnú trasu čítania používateľa a zahadzuje to, čo zostáva pozadu. Praktickým dôsledkom je, že takmer nikdy netreba nútene vykresľovať stránky vopred pred používateľom. Nechajte zobrazenie rozhodnúť o tom, čo je viditeľné
Prispôsobiť stránky šírke a nepoužívať manuálny zoom
Súvislý stĺpec na čítanie vyžaduje, aby boli stránky prispôsobené šírke panelu a neboli pevne pripútané k absolútnemu priblíženiu. Vlastnosť FitMode to zabezpečuje a udržiava toto správanie aj pri zmene veľkosti okna
PdfView.FitMode := pfmFitWidth; // each page fills the column width; height follows
Pri nastavení pfmFitWidth komponent prepočítava priblíženie pri každej zmene veľkosti okna, takže stĺpec a vždy vypĺňa dostupnú šírku. Výšky stránok, a tým aj rozsah posúvania, sa odvodzujú od tohto prispôsobenia. Je tu jedna pasca, do ktorej sa dá ľahko chytiť: priame priradenie hodnoty k vlastnosti Zoom resetuje FitMode späť na pfmNone. Je to zámerné, keďže manuálny zoom a automatické prispôsobenie sú protichodné požiadavky, no znamená to, že nejaké náhodné volanie PdfView.Zoom := 1.0 vo vašom kóde potichu vypne prispôsobenie šírke a pri ďalšej zmene veľkosti okna reflow prestane fungovať. Ak ponúkate ovládanie zoomu aj tlačidlo pre prispôsobenie okna, zaobchádzajte s nimi ako s prepínačom režimov: nastavenie jedného zruší druhé a vy rozhodnete, čo má prednosť
Pre ovládacie prvky absolútneho zoomu, ktoré pôsobia prirodzene, zobrazenie sprístupňuje hodnoty zoomu prispôsobenia: PageWidthZoom[PageNumber] vracia úroveň priblíženia, ktorá by prispôsobila danú stránku na šírku, a zodpovedajúca vlastnosť PageZoom prispôsobí celú stránku do okna. Tieto hodnoty sú ideálne pre naplnenie ponúk typu „Prispôsobiť šírke“ / „Prispôsobiť stránku“ bez toho, aby ste museli napevno kódovať magické percentá, ktoré zlyhávajú pri stránkach orientovaných na šírku alebo pri nadmerných rozmeroch
Udržať odozvu pri rýchlom posúvaní pomocou progresívneho vykresľovania
Predvolená cesta vykresľovania nakreslí celú stránku pred tým, ako sa vráti riadenie. Pri jednej stránke je to v poriadku. Pri rýchlom posúvaní (flick-scroll) cez rozsiahly dokument to však zlyháva: každá preblikujúca stránka spustí plnú rasterizáciu a ak používateľ posúva zobrazenie rýchlejšie, než sa stíhajú stránky vykresľovať, požiadavky na vykreslenie sa hromadia a panel začne sekať, pretože sa spracovávajú stránky, ktoré už v čase dokončenia vykreslenia dávno zmizli z obrazovky. Riešením je umožniť zrušenie vykresľovania a opustiť ho v momente, keď sa používateľ posunie ďalej
Metóda RenderPageProgressive vykresľuje stránku po častiach (chunks) a na hranici každej časti kontroluje token zrušenia, takže prebiehajúce vykresľovanie stránky, ktorá sa práve posunula mimo obrazovku, sa môže zahodiť a nemusí sa dokončovať až do konca
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;
Návratová hodnota je kľúčová. prsDone znamená, že bitmapa je plne vykreslená a pripravená na zobrazenie; prsCancelled signalizuje, že nová pozícia posunutia nahradila túto stránku, takže čiastočný výsledok zahodíte namiesto jeho zobrazenia; prsFailed značí reálnu chybu na stránke. Zrušenie sa overuje na hraniciach jednotlivých blokov a nie vopred, preto očakávajte oneskorenie niekoľko desiatok milisekúnd medzi zavolaním Cancel a skutočným zastavením vykresľovania. Je to však stále omnoho výhodnejšie, ako nechať staré vykresľovanie blokovať celý rad. Odovzdanie hodnoty nil namiesto tokenu vykoná vykreslenie až do konca, čo je správna voľba pre jednorazové kreslenie, ako je napríklad náhľad tlače, kde nie je dôvod na predčasné rušenie
Ak namiesto toho voláte funkčnú verziu metódy RenderPage, ktorá vracia novú inštancie TBitmap, nezabudnite, že volajúci ju vlastní a musí ju uvoľniť pomocou Free. V cykle posúvania, ktorý alokuje bitmapu pre každú stránku, by opomenutie uvoľnenia viedlo k úniku pamäte rastúcemu s každou posunutou stránkou, čo je presne to neobmedzené plytvanie pamäťou, ktorému malo plynulé posúvanie zabrániť. Kde je to možné, vykresľujte radšej do opakovane používanej bitmapy
Čo vám zostane
Prehliadač s plynulým posúvaním z veľkej časti zabezpečuje samotný komponent. Pre rozvrhnutie zvolíte režim dmSingleContinuous, nastavíte pfmFitWidth, aby sa stĺpec prispôsoboval oknu, a overíte Pdf.Active, aby poškodený súbor viditeľne zlyhal. Jediná časť, ktorú stojí za to napísať sami, je prerušiteľné vykresľovanie, pretože prehliadač sa hodnotí podľa toho, ako reaguje, keď niekto pretiahne posuvník na koniec dlhého dokumentu a panel buď stíha, alebo nie. Všetko ostatné — výber textu naprieč stránkami, zvýrazňovanie vyhľadávania, strom záložiek — je už záležitosťou používateľského rozhrania, ktoré leží nad touto posuvnou plochou a nie priamo v nej
Rozhrania API pre TPdfView, DisplayMode a RenderPageProgressive zobrazené v tomto článku sú súčasťou produktu PDFium Component pre Delphi a Lazarus