Yksittäinen mukavalla lukukoolla renderöity A4-sivu vie muutaman megatavun verran 32-bittistä bittikarttamuistia. Kerro se 400-sivuisella sopimuksella, niin laskutoimitus lakkaa olemasta teoreettinen: renderöi jokainen sivu etukäteen, ja pyydät Windowsilta reilusti yli gigatavun verran bittikarttoja, joita käyttäjä katsoo vain ruudullinen kerrallaan. Sovellus joko kuluttaa loppuun osoiteavaruutensa 32-bittisessä kokoonpanossa tai viettää ensimmäiset sekuntinsa jäätyneenä, kun GPU ja sivujäsennin käyvät läpi sivuja, joita kukaan ei ole vielä edes vierittänyt näkyviin. Jatkuvasti vierivän lukijan on tunnutettava yhdeltä pitkältä sivunauhalta, mutta se ei voi pitää niitä kaikkia muistissa kerralla
Tämä ristiriita on koko ongelman ydin. PDFium-komponentti ratkaisee sen TPdfView-luokan sisällä, joten suurin osa työstä on oikean näyttötilan valitsemista ja sen ymmärtämistä, mitä komponentti tekee puolestasi. Osuudet, joita se ei tee puolestasi – kuten sivujen koon sovittaminen lukuvirtaan ja nopean vierityksen pitäminen sujuvana – ovat paikkoja, joissa pieni määrä koodia maksaa itsensä takaisin. Jos olet vielä kokoamassa ympäröiviä käyttöliittymäelementtejä (työkalupalkki, pikkukuvat, hakukenttä), monipuolisen katseluohjelman opas kattaa sen alueen; tässä aiheena on itse vieritys
Asettelu on näyttötila, ei paneeli bittikarttoja
VCL-lomakkeita rakentaessa ensireaktio on tarttua scroll box -komponenttiin ja pinota sen sisään kuvakomponentteja, yksi per sivu. Vastusta sitä. Tuo ratkaisu pakottaa sinut hallitsemaan sivujen asettelua, vieritysmatematiikkaa ja muistikysymystä samanaikaisesti, ja päädyt toteuttamaan ne kaikki itse huonosti. TPdfView mallintaa asiakirjan jo valmiiksi jatkuvana sivujonona ja tuo asettelun näkyville DisplayMode-propertyn kautta:
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');
Tämä on koko jatkuvan vierityksen asennus. dmSingleContinuous asettaa sivut yhteen pystysuoraan sarakkeeseen, jossa sivujen välit käsitellään sisäisesti, ja näkymä vierii tuon sarakkeen läpi yhtenä pintana. Tavallista navigointia varten ei tarvitse kytkeä sivukohtaisia säätimiä tai kirjoittaa vierityskäsittelijää. Huomaa tarkistus Pdf.Active-muuttujasta sijoituksen jälkeen: asiakirjan avaaminen ei koskaan heitä poikkeusta, joten vioittunut tai salasanalla suojattu tiedosto jättää Active-arvon tilaan False ilman poikkeusta. Katseluohjelma, joka ohittaa tämän tarkistuksen, renderöi vain tyhjän ruudun ja jättää kehittäjän ihmettelemään syytä
Sama property kantaa myös aukeamatilat. dmTwoPageContinuous asettaa sivut rinnakkain, kaksi rivilleen, jolloin saavutetaan kirjamainen lukukokemus, jota jotkin asiakirjat vaativat; dmTwoPageContinuousWithCover tekee saman, mutta antaa ensimmäisen sivun olla yksinään kansilehtenä, jolloin loput aukeamat asettuvat luonnollisille parillinen-pariton-rajoille. Kaikki kolme tilaa vierivät jatkuvasti. Vaihto niiden välillä on yksi sijoitus, mikä tekee näyttötilan valintavalikosta helpon toteuttaa myöhemmin
Vain näkyvät sivut rasteroidaan
Syy siihen, miksi tämä skaalautuu 400-sivuiseen tiedostoon, on se, että sarake on virtuaalinen. TPdfView tietää jokaisen sivun korkeuden asiakirjan sivupuustesta, joten se voi laskea kokonaisvierityksen laajuuden ja kunkin sivun sijainnin renderöimättä mitään. Rasterointi, joka on kallis vaihe ja muuttaa sivun sisältövirran pikseleiksi, tapahtuu vain sivuille, jotka kulloinkin leikkaavat näkymäalueen (viewport), sekä pienelle marginaalialueelle, jotta sivu on valmis silloin, kun se vierii näkyviin. Kun vierität alaspäin, näkymäalueelle saapuvat sivut renderöidään ja siitä poistuvien sivujen bittikartat vapautetaan. Muistinkulutus pysyy suhteessa siihen, mikä mahtuu ruudulle, eikä asiakirjan pituuteen
Tämä on tärkeä sisäistää, koska se muuttaa tapaa, jolla arvioit työn hintaa. 400-sivuisen asiakirjan avaaminen on halpaa: se jäsentää rakenteen, ei sisältöä. Kustannus maksetaan sivukohtaisesti ja laiskasti (lazy) vasta siinä vaiheessa, kun sivu vieritetään lähelle. Katseluohjelma, joka tuntuu heti valmiilta avattaessa ja sulavalta vieritettäessä, ei tee vähemmän työtä kokonaisuudessaan, vaan se jakaa työn käyttäjän todelliselle lukupolulle ja heittää taakse jäävän pois. Käytännön seurauksena on se, ettet melkein koskaan halua pakottaa sivuja renderöitymään etukäteen ennen kuin käyttäjä ehtii niihin. Anna näkymän päättää, mikä on näkyvissä
Mitoita sivut leveyden mukaan, jätä sitten zoomaus rauhaan
Lukusarake vaatii sivujen koon sopeuttamista paneelin leveyteen, ei kiinnittämistä absoluuttiseen zoomaukseen. FitMode tekee tämän ja jatkaa sen tekemistä myös ikkunan kokoa muutettaessa:
PdfView.FitMode := pfmFitWidth; // each page fills the column width; height follows
Asetuksella pfmFitWidth komponentti laskee zoomauksen uudelleen aina näkymän koon muuttuessa, joten sarake täyttää aina käytettävissä olevan leveyden ja sivujen korkeudet (ja sitä kautta vieritysalueen pituus) seuraavat tästä. Tässä on yksi sudenkuoppa, johon monet lankeavat: suora sijoitus Zoom-propertylle palauttaa FitMode-tilan arvoon pfmNone. Tämä on tarkoituksellista, koska manuaalinen zoomaus ja automaattinen sovitus ovat ristiriitaisia aikomuksia, mutta se tarkoittaa, että koodissasi oleva satunnainen PdfView.Zoom := 1.0 kytkee hiljaisesti sovituksen pois päältä ja seuraava koon muutos lakkaa reflow-käsittelystä. Jos tarjoat sekä zoomaussäätimen että sovituspainikkeen, käsittele niitä tilavaihtoehtoina: toisen asettaminen nollaa toisen, ja päätät kumpi voittaa
Jotta absoluuttiset zoomaussäätimet toimisivat luontevasti, näkymä tuo sovituszoomaukset arvoina, joita voit soveltaa tai näyttää: PageWidthZoom[PageNumber] palauttaa zoomauksen, joka sovittaisi kyseisen sivun leveyteen, ja sitä vastaava PageZoom sovittaa koko sivun. Näiden arvojen lukeminen on tapa täyttää "Sovita leveys" / "Sovita sivu" -valikot ilman kovakoodattuja taikaprosentteja, jotka menevät vigaan vaakasivujen tai ylikokoisten sivujen kohdalla
Pidä nopea vieritys responsiivisena progressiivisella renderöinnillä
Oletusarvoinen renderöintipolku piirtää sivun valmiiksi ennen paluuta. Yhdelle sivulle tämä on hienoa. Tiheän asiakirjan nopean vierityksen aikana näin ei ole: jokainen ohi vilahtava sivu käynnistää täyden rasteroinnin, ja jos käyttäjä vierittää nopeammin kuin sivut ehtivät renderöityä, nämä renderöinnit kasaantuvat ja paneeli alkaa nykiä, koska työtä tehdään sivuille, jotka ovat jo poistuneet ruudulta työn valmistuttua. Korjaus on tehdä renderöinnistä peruutettava ja hylätä se heti, kun käyttäjä siirtyy eteenpäin
RenderPageProgressive renderöi lohkoissa ja tarkistaa peruutuslipun kunkin lohkon rajalla, joten juuri näytöltä poistuneen sivun käynnissä oleva renderöinti voidaan keskeyttää sen sijaan, että se ajettaisiin loppuun asti:
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;
Merkitsevä muoto on paluuarvo. prsDone tarkoittaa, että bittikartta on maalattu kokonaan ja se voidaan piirtää ruudulle; prsCancelled tarkoittaa, että uudempi vieritysasema korvasi tämän sivun, joten heität osittaisen tuloksen pois sen näyttämisen sijaan; prsFailed on aito virhe kyseisellä sivulla. Peruutusta kysellään lohkorajoilla ennakoivan keskeytyksen sijaan, joten varaudu muutaman kymmenen millisekunnin viiveeseen Cancel-kutsun ja renderöinnin todellisen pysähtymisen välillä. Tämä on silti huomattavasti halvempaa kuin antaa vanhentuneen kokosivun renderöinnin tukkia jonoa. Arvon nil välittäminen peruutuslippuna renderöi suoraan valmiiksi asti, mikä on oikea valinta kertaluonteiselle renderöinnille, kuten tulostuksen esikatselulle, jossa ei ole mitään peruutettavaa
Kun kutsut sen sijaan RenderPage-metodin funktioversiota, joka palauttaa uuden TBitmap-olion, muusta että kutsuja omistaa sen ja sen on vapautettava se (Free). Vierityssilmukassa, joka varaa bittikartan sivua kohden, tämän unohtaminen on vuoto, joka kasvaa jokaisen käyttäjän ohittaman sivun myötä. Se on juuri se rajoittamattoman muistin vikatila, joka jatkuvalla asettelulla oli tarkoitus välttää. Renderöi mahdollisuuksien mukaan uudelleenkäytettävään bittikarttaan
Mitä käteesi jää
Jatkuvasti vierivä lukija on pääosin komponentin itsensä tarjoama. Valitset asetteluksi dmSingleContinuous, asetat pfmFitWidth-tilan jotta sarake sopeutuu ikkunaan ja tarkistat Pdf.Active-arvon, jotta virheellinen tiedosto epäonnistuu kuuluvasti. Ainoa osa, joka kannattaa kirjoittaa itse, on peruutettava renderöinti, sillä katseluohjelmaa arvioidaan sen mukaan, miten se käyttäytyy, kun joku vetää vierityspalkin pitkän asiakirjan pohjaan ja ruutu joko pysyy perässä tai ei. Kaikki tämän jälkeen – tekstin valinta sivujen yli, haun korostus, kirjanmerkkipuu – on käyttöliittymätyötä, joka istuu tämän vierityspinnan päällä eikä sen sisällä
Tässä näytetyt TPdfView-, DisplayMode- ja RenderPageProgressive-API:t ovat osa Delphille ja Lazarukselle tarkoitettua PDFium-komponenttia