En enkelt A4-side gjengitt (rendered) i et behagelig lese-zoomnivå er i størrelsesorden noen få megabyte med 32-bits bitmap. Multipliser det med en 400-siders kontrakt, og regnestykket slutter å være abstrakt: gjengi hver side på forhånd, og du ber Windows om godt over en gigabyte med bitmapp som brukeren vil se på én skjermfull av gangen. Applikasjonen går enten tom for adresserom (address space) på en 32-bits bygging (build) eller bruker de første sekundene på å fryse mens GPU-en og side-tolkeren (page parser) maler seg gjennom sider ingen har rullet to ennå. En kontinuerlig rullende leser må føles som ett langt bånd med sider, men den kan faktisk ikke holde alle i minnet samtidig
Den spenningen er hele problemet her. PDFium-komponenten løser det inne i TPdfView, så mesteparten av arbeidet er å velge riktig visningsmodus og forstå hva komponenten gjør på dine vegne. Delene den ikke gjør for deg, dimensjonering av sider for en leseflyt og det å holde rask rulling responsiv, er der litt kode tjener sin rett. Hvis du fremdeles setter sammen den omkringliggende krommen (verktøylinje, miniatyrbilder, søkeboks), dekker gjennomgangen av den funksjonsrike PDF-leseren den grunnen; her er emnet selve rullingen
Layouten er en visningsmodus, ikke et panel med bitmapp
Instinktet fra VCL-skjemarbeid er å strekke seg etter en rulleboks (scroll box) og stable bildekontroller inni den, én per side. Motstå det. Dette designet tvinger deg til å eie sideposisjonering, rullematematikk og minnespørsmålet på en gang, og du vil finne opp alle på nytt på en dårlig måte. TPdfView allerede modellerer dokumentet som en kontinuerlig kjøring (run) av sider og eksponerer oppsettet gjennom DisplayMode-egenskapen
Pdf := TPdf.Create(Self);
PdfView := TPdfView.Create(Self);
PdfView.Parent := Self;
PdfView.Align := alClient;
PdfView.Pdf := Pdf;
PdfView.DisplayMode := dmSingleContinuous; // én side bred, ruller vertikalt
Pdf.FileName := 'contract.pdf';
Pdf.Active := True;
if not Pdf.Active then
ShowMessage('Kunne ikke åpne dokumentet');
Det er hele det kontinuerlig-rullende oppsettet. dmSingleContinuous legger ut sidene i en enkelt vertikal kolonne med hullene mellom dem håndtert internt, og visningen ruller gjennom den kolonnen som én overflate. Det er ingen per-side-kontroll (per-page control) å koble opp, og ingen rulle-håndterer (scroll handler) å skrive for ordinær navigasjon. Legg merke til sjekken på Pdf.Active etter tildelingen (assignment): å åpne et dokument utløser (raises) aldri et unntak, så en skadet eller passordbeskyttet fil lar Active sitte på False uten noe unntak (exception) å fange, og en leser som hopper over denne sjekken gjengir et tomt panel og klandrer seg selv
Den samme egenskapen bærer spredningsmodusene (spread modes). dmTwoPageContinuous plasserer sider side om side, to på en rad, for den bok-aktige lesingen noen dokumenter krever; dmTwoPageContinuousWithCover gjør det samme, men lar side én stå alene som et omslag (cover) slik at de gjenværende oppslagene (spreads) faller på den naturlige partall-oddetall-grensen. Alle tre ruller kontinuerlig. Bytte mellom dem er en enkelt tildeling (assignment), noe som gjør en visningsmodus-kombinasjonsboks (display-mode combo box) triviell å legge til senere
Bare de synlige sidene blir rastrert (rasterized)
Grunnen til at dette skalerer til en 400-siders fil er at kolonnen er virtuell. TPdfView kjenner høyden til hver side fra dokumentets sidetre, så den kan beregne det totale rulleomfanget (scroll extent) og posisjonen til hver side uten å rastrere (rasterizing) noe. Rastrering, det dyre trinnet som gjør en sides innholdsstrøm om til piksler, skjer bare for sidene som for øyeblikket krysser (intersect) visningsporten (viewport), pluss en liten margin slik at en side er klar innen den ruller inn i syne. Mens du ruller ned, blir sider som går inn i visningsporten gjengitt (rendered), og sider som forlater den får frigitt bitmappene sine (bitmaps released). Minnet forblir proporsjonalt med det som får plass på skjermen, ikke med dokumentets lengde
Dette er verdt å internalisere fordi det endrer hvordan du tenker (reason) om kostnader. Å åpne et dokument på 400 sider er billig: det tolker (parses) strukturen, ikke innholdet. Utgiften er per side, og den betales dovent (lazily), i det øyeblikket en side rulles nær. En leser (viewer) som føles umiddelbar ved åpning og smidig ved rulling, gjør ikke mindre arbeid totalt, den sprer arbeidet over brukerens faktiske lesebane (reading path) og forkaster det som faller bak. Den praktiske konsekvensen er at du nesten aldri ønsker å tvangs-gjengi (force-render) sider foran brukeren. La visningen bestemme hva som er synlig
Tilpass sider til bredden, og la deretter zoom være i fred
En lesekolonne vil ha sider som er dimensjonert til panelbredden, ikke festet to en absolutt zoom. FitMode gjør dette, og fortsetter å gjøre det etter hvert som vinduet endrer størrelse (resizes)
PdfView.FitMode := pfmFitWidth; // hver side fyller kolonnebredden; høyden følger
Med pfmFitWidth beregner (recomputes) komponenten zoomen på nytt hver gang visningen endrer størrelse, slik at kolonnen alltid fyller den tilgjengelige bredden, og sidehøydene, og dermed rulleomfanget (scroll extent), følger fra det. Det er én felle som fanger folk: å tilordne Zoom direkte tilbakestiller FitMode til pfmNone. Det er bevisst, fordi en manuell zoom og en automatisk tilpasning (automatic fit) er motstridende intensjoner, men det betyr at en villfaren (stray) PdfView.Zoom := 1.0 et sted i koden din stille slår av tilpass-til-bredde, og neste størrelsesendring (resize) slutter å flyte på nytt (reflowing). Hvis du tilbyr både en zoomkontroll og en tilpassings-knapp, bør du behandle dem som en modusbryter (mode switch): innstilling av en sletter den andre, og du bestemmer hvem som vinner
For absolutte zoomkontroller som leses naturlig, eksponerer visningen tilpassings-zoomene (fit zooms) som verdier du kan bruke eller vise: PageWidthZoom[PageNumber] returnerer zoomen som ville tilpasse den siden til bredden, og den matchende PageZoom tilpasser hele siden. Å lese disse er hvordan du fyller ut (populate) en "Tilpass bredde" / "Tilpass side"-meny uten hardkodede magiske prosenter som går galt på landskapssider eller overdimensjonerte sider
Hold rask rulling responsiv med progressiv gjengivelse
Standard gjengivelsesbane (render path) tegner en side til fullførelse før den returnerer. For en enkelt side går det bra. Under en flikk-rulling (flick-scroll) gjennom et tett (dense) dokument er det ikke: hver side som suser forbi setter i gang en full rastrering, og hvis brukeren ruller raskere enn sider kan gjengis, hoper disse gjengivelsene (renders) seg opp og panelet hakker (stutters) fordi arbeid gjøres for sider som allerede er utenfor skjermen innen den er ferdig. Løsningen (fix) er å gjøre en gjengivelse avbrytbar (cancellable) og forlate den i det øyeblikket brukeren går videre
RenderPageProgressive gjengir i stykker (chunks) og sjekker et avbrytelsestoken (cancellation token) ved hver stykke-grense (chunk boundary), slik at en pågående gjengivelse av en side som nettopp rullet bort, kan droppes (dropped) i stedet for å kjøre til enden
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
// Avbryt det som gjengav; det gamle symbolet er nå signalisert.
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 er ferdig, mal den
prsCancelled: Exit; // avløst, forkast dette resultatet
prsFailed: ShowMessage('Gjengivelse mislyktes for side ' + IntToStr(PageNo));
end;
end;
Formen som gjelder er returverdien. prsDone betyr at bitmappen (bitmap) er fullstendig malt og verdt å sende (blitting) til skjermen; prsCancelled betyr at en nyere rulleposisjon avløste (superseded) denne siden, så du kaster det delvise resultatet i stedet for å vise det; prsFailed er en genuin feil (error) på den siden. Avbrytelse spores (polled) ved stykkegrenser (chunk boundaries) fremfor forebyggende (preemptively), så forvent tilsvarende millisekunder med latens mellom anrop av Cancel og at gjengivelsen (render) faktisk stopper. Det er fortsatt langt billigere enn å la en gammel heldags-gjengivelse (full-page render) blokkere køen. Å sende nil som symbolet (token) gjengir rett gjennom til ferdigstillelse, noe som er det riktige valget for en engangs-gjengivelse (one-off render) som en forhåndsvisning (print preview) for utskrift der det ikke er noe å avbryte mot
Når du kaller funksjonsformen av RenderPage i stedet, den som returnerer en fersk TBitmap, husk at oppringeren eier den og må Free den. I en rulle-løkke (scroll loop) som allokerer (allocates) en bitmapp per side, er å glemme dette en lekkasje som vokser for hver side brukeren passerer, noe som er nøyaktig den ubegrensede minnesfeilen (unbounded-memory failure) den kontinuerlige designen var ment å unngå. Gjengi i en gjenbrukt bitmapp (reused bitmap) der du kan
Det du sitter igjen med
Den kontinuerlig-rullende leseren (continuous-scroll reader) er stort sett opp til komponenten å levere. Du velger dmSingleContinuous for oppsettet (layout), setter pfmFitWidth slik at kolonnen flyter (reflows) med vinduet, og sjekker Pdf.Active slik at en dårlig fil feiler høyt. Den ene brikken som er verdt å skrive selv, er avbrytbar gjengivelse (cancellable rendering), fordi en leser blir bedømt på hvordan den oppfører seg når noen drar rullefeltet til bunnen av et langt dokument og panelet enten holder tritt eller ikke. Alt forbi det, tekstvalg på tvers av sider, søkeutheving (search highlighting), et bokmerketre, er grensesnittsarbeid (interface work) som sitter på toppen av denne rulleoverflaten fremfor inni den
TPdfView, DisplayMode og RenderPageProgressive-API-ene vist her er en del av PDFium-komponenten for Delphi og Lazarus