Műszaki cikk

Folyamatos görgetésű PDF-néző Delphiben a PDFium komponens segítségével

Egyetlen A4-es oldal kényelmes olvasási nagyításban renderelve körülbelül néhány megabájtnyi 32 bites bitképet jelent. Szorozza meg ezt egy 400 oldalas szerződéssel, és a matematika máris kézzelfoghatóvá válik: ha minden oldalt előre lerenderel, jóval több mint egy gigabájtnyi bitképet kér a Windowstól, amelyet a felhasználó egyszerre csak egy képernyőnyi méretben fog nézni. Az alkalmazás 32 bites build esetén vagy kifogy a címtérből, vagy az első néhány másodpercben lefagy, miközben a GPU és az oldalelemző olyan oldalakon rágja át magát, amelyekre még senki sem görgetett. Egy folyamatos görgetésű olvasónak olyan érzést kell keltenie, mint egy hosszú, összefüggő oldalszalag, de valójában nem tarthatja mindegyiket egyszerre a memóriában

Ez a feszültség jelenti itt a teljes problémát. A PDFium komponens a TPdfView osztályon belül oldja meg ezt, így a munka nagy része a megfelelő megjelenítési mód kiválasztásából és annak megértéséből áll, amit a komponens az Ön nevében végez. Az azok a részek, amelyeket nem végez el Ön helyett — az oldalak méretezése az olvasási folyamathoz és a gyors görgetés válaszkészségének megőrzése —, ott egy kevés kód meghálálja magát. Ha még a környező felületet (eszköztár, bélyegképek, keresőmező) állítja össze, a funkciógazdag nézőke bemutatója lefedi ezt a területet; itt a téma maga a görgetés

Az elrendezés egy megjelenítési mód, nem pedig bitképek panele

A VCL formokkal való munka során az ösztönös reakció az, hogy egy scroll boxot hozzunk létre, és abba képvezérlőket (image control) pakoljunk, oldalanként egyet. Álljon ellent ennek. Ez a megközelítés arra kényszerítené, hogy egyszerre kezelje az oldalak pozicionálását, a görgetési matematikát és a memóriakezelést, és mindegyiket rosszul találná fel újra. A TPdfView már modellezi a dokumentumot mint oldalak folyamatos futását, és a megjelenítési módot a DisplayMode tulajdonságon keresztül teszi közzé

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');

Ez a teljes folyamatos görgetési beállítás. A dmSingleContinuous az oldalakat egyetlen függőleges oszlopba rendezi el, a köztük lévő réseket belsőleg kezeli, és a nézet ezen az oszlopon mint egyetlen felületen görget keresztül. Nincs elemenkénti vezérlő, amit be kellene kötni, és nincs görgetéskezelő (scroll handler), amit meg kellene írni a normál navigációhoz. Figyelje meg a Pdf.Active ellenőrzését az értékadás után: a dokumentum megnyitása soha nem vált ki kivételt, így a sérült vagy jelszóval védett fájloknál az Active értéke False marad, és az ellenőrzést kihagyó nézőke üres panelt jelenít meg, majd saját magát hibáztatja

Ugyanez a tulajdonság hordozza a kétoldalas módokat is. A dmTwoPageContinuous egymás mellé helyezi az oldalakat, soronként kettőt, a könyvszerű olvasáshoz; a dmTwoPageContinuousWithCover ugyanezt teszi, de hagyja az első oldalt borítóként egyedül állni, így a többi oldalpár a természetes páros-páratlan határhoz igazodik. Mindhárom folyamatosan görget. A köztük való váltás egyetlen értékadás, így a megjelenítési mód kiválasztására szolgáló legördülő lista (combo box) hozzáadása később rendkívül egyszerű

Only the visible pages get rasterized

Amiért ez egy 400 oldalas fájlnál is működik, az az, hogy az oszlop virtuális. A TPdfView a dokumentum oldafájából ismeri minden oldal magasságát, így ki tudja számítani a teljes görgetési tartományt és az egyes oldalak pozícióját anélkül, hogy bármit is raszterizálna. A raszterizálás — a költséges lépés, amely az oldal tartalomfolyamát képpontokká alakítja — csak a viewportot (nézetablakot) jelenleg metsző oldalakon történik meg, plusz egy kis margón, hogy az oldal készen álljon, mire a nézetbe görgetik. Ahogy lefelé görget, a viewportba belépő oldalak renderelődnek, a távozó oldalak bitképei pedig felszabadulnak. A memória arányos marad azzal, ami elfér a képernyőn, nem pedig a dokumentum hosszával

Ezt érdemes rögzíteni, mert megváltoztatja a költségekről való gondolkodást. Egy 400 oldalas dokumentum megnyitása olcsó: a szerkezetet elemzi, nem a tartalmat. A költség oldalanként merül fel, mégpedig lusta módon (lazily), abban a pillanatban, amikor egy oldal közelébe görgetnek. Egy olyan nézőke, amely megnyitáskor azonnalinak, görgetéskor pedig simának érződik, nem végez kevesebb munkát összességében; csak elosztja a munkát a felhasználó tényleges olvasási útvonalán, és eldobja azt, ami mögötte marad. Ennek a gyakorlati következménye az, hogy szinte soha nem akarja az oldalakat a felhasználó előtt kényszerítve renderelni. Hagyja, hogy a nézet döntse el, mi látható

Méretezze az oldalakat a szélességhez, majd hagyja békén a nagyítást

Egy olvasóoszlopban az oldalak szélességét a panel szélességéhez kell igazítani, nem pedig abszolút nagyításhoz rögzíteni. A FitMode ezt teszi, és a méretváltoztatáskor is fenntartja

PdfView.FitMode := pfmFitWidth;   // each page fills the column width; height follows

A pfmFitWidth beállítással a komponens újraszámítja a nagyítást, amikor a nézet mérete megváltozik, így az oszlop mindig kitölti a rendelkezésre álló szélességet, és az oldalak magassága, ezáltal a görgetési tartomány is ebből következik. Van egy csapda, ami megzavarhatja a fejlesztőket: a Zoom közvetlen hozzárendelése visszaállítja a FitMode-ot pfmNone-ra. Ez szándékos, mivel a kézi nagyítás és az automatikus illesztés ellentmondásos szándékok, de ez azt jelenti, hogy a kódban valahol elhelyezett PdfView.Zoom := 1.0 csendben kikapcsolja a szélességhez való igazítást, és a következő méretváltoztatásnál leáll az újratördelés. Ha nagyítási vezérlőt és igazítási gombot is kínál, kezelje őket módváltásként: az egyik beállítása törli a másikat, és Ön dönti el, melyik nyer

A természetesen olvasható abszolút nagyítási vezérlőkhöz a nézet az illesztési nagyításokat olyan értékekként teszi közzé, amelyeket alkalmazhat vagy megjeleníthet: a PageWidthZoom[PageNumber] visszaadja azt a nagyítást, amely a lapot a szélességhez igazítaná, a hozzáillő PageZoom pedig a teljes oldalt illeszti be. Ezek beolvasásával töltheti fel a „Szélesség igazítása” / „Oldal igazítása” menüt anélkül, hogy mágikus százalékokat égetne be a kódba, amelyek fekvő vagy túlméretezett oldalakon hibát okoznának

Tartsa válaszkészen a gyors görgetést a progresszív rendereléssel

Az alapértelmezett renderelési útvonal az oldal befejezéséig rajzol, mielőtt visszatérne. Egyetlen oldalnál ez rendben van. Egy sűrű dokumentum gyors görgetése közben azonban nem: minden elsuhanó oldal elindít egy teljes raszterizálást, és ha a felhasználó gyorsabban görget, mint ahogy az oldalak renderelődnek, ezek a feladatok felhalmozódnak, és a panel akadozik, mert olyan oldalakon végez munkát a rendszer, amelyek a befejezés pillanatában már nincsenek a képernyőn. A megoldás az, hogy a renderelést megszakíthatóvá tesszük, és eldobjuk abban a pillanatban, amikor a felhasználó továbbgörget

A RenderPageProgressive darabokban renderel, és minden darabhatáron ellenőrzi a megszakítási tokent (cancellation token), így a nemrég elgörgetett oldal folyamatban lévő renderelése elvethető ahelyett, hogy végigfutna

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;

Az érték, amely számít, a visszatérési érték. A prsDone azt jelenti, hogy a bitkép teljesen ki van rajzolva, és érdemes kirajzolni a képernyőre; a prsCancelled azt jelenti, hogy egy újabb görgetési pozíció felülírta ezt az oldalt, így eldobja a részleges eredményt ahelyett, hogy megjelenítené; a prsFailed pedig valós hibát jelent az adott oldalon. A megszakítást darabhatárokon ellenőrzi a rendszer, nem pedig preemptív módon, így a Cancel meghívása és a renderelés tényleges leállása között néhány tíz ezredmásodperces késleltetésre kell számítani. Ez még mindig sokkal olcsóbb, mint hagyni, hogy egy elavult, teljes oldalas renderelés blokkolja a sort. A nil átadása tokenként a teljes befejezésig futtatja a renderelést, ami a helyes választás az egyszeri renderelésekhez, például egy nyomtatási előnézethez, ahol nincs miért megszakítani a folyamatot

Amikor ehelyett a RenderPage függvényváltozatát hívja meg — azt, amelyik egy friss TBitmap-et ad vissza —, ne feledje, hogy a hívó a tulajdonosa, és neki kell felszabadítania (Free). Egy olyan görgetőciklusban, amely oldalanként foglal le egy bitképet, ennek elfelejtése olyan szivárgást okoz, amely minden elsuhanó oldallal növekszik, ami pontosan a korlátlan memóriafoglalási hiba, amelyet a folyamatos elrendezéssel el akartunk kerülni. Ahol csak lehet, rendereljen egy újrahasznosított bitképbe

Amivel dolga marad

A folyamatos görgetésű olvasó megvalósítása leginkább a komponens feladata. Kiválasztja a dmSingleContinuous elrendezést, beállítja a pfmFitWidth-et, hogy az oszlop abblakméretezéskor újratördelődjön, és ellenőrzi a Pdf.Active-ot, hogy a hibás fájloknál egyértelmű legyen a hiba. Az egyetlen rész, amit érdemes magának megírnia, a megszakítható renderelés, mert az olvasót az alapján ítélik meg, hogyan viselkedik, amikor valaki a görgetősávot egy hosszú dokumentum aljára húzza, és a panel vagy lépést tart, vagy sem. Minden ezen túli dolog — szövegkijelölés az oldalakon át, a keresési kiemelés, a könyvjelzőfa — a felület feladata, amely ezen a görgetőfelületen felül helyezkedik el, nem pedig azon belül

Az itt bemutatott TPdfView, DisplayMode és RenderPageProgressive API-k a Delphihez és Lazarushoz készült PDFium komponens részét képezik