HotPDF jakaa Delphi-PDF-katseluohjelmansa kahteen osaan: THPDFViewerModel, tavallinen luokka, joka omistaa zoomaus-, kierto-, haku-, korostus- ja navigointitilan ilman riippuvuutta mistään ikkunakahvasta, sekä THPDFViewer, TScrollBox-pohjainen ohjausobjekti, joka muuttaa tuon tilan pikseleiksi. Tämä jako on juuri se, mikä mahdollistaa katseluohjelman logiikan ajamisen ja testaamisen ilman, että lomaketta koskaan luodaan
Useimmat mukautetut katseluohjelmakomponentit eivät näytä tältä. Zoomaustaso asuu ohjausobjektin yksityisessä kentässä, sivunavigointi rajaa rajansa painikkeen OnClick-käsittelijän sisällä, ja ainoa tapa selvittää, kunnioittaako Ctrl+vieritys zoomauskattoa, on ajaa sovellus, klikata ja katsoa. Tällä tavalla rakennettu ohjausobjekti toimii hyvin, kunnes se tarvitsee regressiotestisarjan tai toisen isännän — tulostuksen esikatseluikkunan, pienoiskuvarivin, eräajokatselijan ilman näkyvää ikkunaa lainkaan — ja tarvittava tila osoittautuukin hitsatuksi kiinni TWinControl-luokkaan, joka vaatii todellisen kahvan ennen kuin se tekee mitään
Miksi PDF-katseluohjelmakomponentti tarvitsee MVC-jaon ylipäätään?
PDF-katseluohjelma tarvitsee tämänkaltaisen jaon, koska sen tila ja sen esitys muuttuvat eri syistä ja eri tahdissa. Sivuindeksi, zoomaus, näkymän kierto, hakuosumat ja korostusalueet ovat liiketoimintatilaa: ne voidaan laskea, validoida ja serialisoida ilman yhtäkään pikseliä näytöllä. Bittikartan piirtäminen, hiiren kaappaus ja valintasuorakulmion piirtäminen ovat esityskysymyksiä, jotka ovat järkeviä vasta kun ohjausobjekti on olemassa. HotPDF pitää ensimmäisen ryhmän luokassa THPDFViewerModel, luokassa, jolla ei ole lainkaan VCL-ikkunointi-esivanhempaa, ja toisen ryhmän luokassa THPDFViewer, joka omistaa mallin instanssin ja reagoi siihen — lähempänä Model-View-paria kuin oppikirjamaista kolmitasoista MVC:tä, koska erillistä Controller-luokkaa ei ole, ja THPDFViewer itse muuttaa raa'at näppäimistö- ja hiiritapahtumat mallikutsuiksi. Tärkeämpää kuin nimike on riippuvuuden suunta: mikään THPDFViewerModel:ssä ei vaadi Handle-kahvaa, viestisilmukkaa tai näkyvää työpöytää, ja juuri tämä on se, mikä mahdollistaa HotPDF:n omalle testisarjalle sivutuksen, zoomauksen rajaamisen, näppäinkomentojen ja koordinaattien edestakaisen muunnoksen ajamisen DUnitX:n läpi ilman ikkunan avaamista
uses
DUnitX.TestFramework,
HPDFDoc, HPDFViewerModel;
type
[TestFixture]
TViewerModelTests = class
public
[Test]
procedure ZoomInStopsAtTheTopPresetLevel;
end;
procedure TViewerModelTests.ZoomInStopsAtTheTopPresetLevel;
var
Doc: THotPDF;
Model: THPDFViewerModel;
begin
Doc := THotPDF.Create(nil);
Model := THPDFViewerModel.Create;
try
Doc.LoadFromFile('sample.pdf');
Model.Document := Doc;
Model.Zoom := 64.0; // top of the preset table (6400%)
Model.ZoomIn; // already at the ceiling
Assert.AreEqual(64.0, Model.Zoom, 0.0001);
finally
Model.Free;
Doc.Free;
end;
end;
Mitä THPDFViewerModel todella omistaa
THPDFViewerModel omistaa kaiken, mitä katseluohjelma tarvitsee vastatakseen kysymykseen, mitä näytöllä pitäisi juuri nyt olla, omistamatta sitä, miten se piirretään. PageIndex, PageNumber ja PageCount seuraavat sijaintia; Zoom ja ZoomMode (vzmActualSize, vzmFitPage, vzmFitWidth, vzmCustom) seuraavat mittakaavaa; ViewRotation seuraa ei-tuhoavaa näytöllä tapahtuvaa kiertoa, joka ei koskaan kosketa sivun omaa /Rotate-kenttää. Navigointimetodit — FirstPage, PriorPage, NextPage, LastPage — ja zoomausmetodit — ZoomIn, ZoomOut, jotka kulkevat kiinteän yhdeksäntoista esiasetetun tason taulukon läpi 5 %:sta 6400 %:iin — asuvat myös täällä, sekä FindAll/FindNext/FindPrevious tekstihakua varten ja AddHighlightRegion/RemoveHighlightRegion/ClearHighlightRegions pysyviä sivuannotaatioita varten, jotka kutsuja haluaa säilyttää renderöintien välillä. Malli omistaa sekä tulosteen että syötteen: CreateCurrentPageSnapshot ja CreateCurrentPageMetafile vievät täsmälleen sen sivun, joka on juuri nyt näytöllä, ja PrintCurrentView lähettää saman nykyisen näkymän — nykyisen sivun, nykyisestä zoomauksesta johdetun DPI:n, nykyisen kierron — TPrinter-luokalle, suppeamman, näkymään sidotun tulostustyön kuin koko asiakirjan kattava tulostusputki, joka käsitellään artikkelissa HotPDF:n TPrinter-tulostuksen läpikäynti. Jokainen merkityksellinen muutos nostaa myös vastaavan tapahtuman — OnPageChange, OnZoomChange, OnSearchChange, OnHighlightChange, OnViewRotationChange — jotta tilaaja saa tietää, mikä muuttui, ilman kyselyä (polling)
Mistä THPDFViewer tietää, milloin piirtää uudelleen?
THPDFViewer tietää, milloin piirtää uudelleen, koska se tilaa mallin sen sijaan, että arvaisi. THPDFViewerin konstruktori luo yksityisen THPDFViewerModel-instanssin, ja kytkee sitten jokaisen sen ilmoitustapahtuman — OnBeginUpdate, OnEndUpdate, OnHighlightChange, OnPageChange, OnSearchChange, OnViewRotationChange, OnZoomChange — vastaavaan yksityiseen käsittelijään. Jokaisen käsittelijän tehtävä on pieni: kutsua RefreshDocument-metodia, joka todella rasteroi nykyisen sivun saman välimuistitetun sivurenderöijän kautta, joka kuvataan artikkelissa HotPDF:n sivu-bittikartaksi-renderöinnin sisäiset toiminnot, ja sitten yhdistää korostuslaatikot ja hakuosumat päälle sekä soveltaa nykyistä näkymän kiertoa. Julkaistut ominaisuudet kuten PageIndex, Zoom, ZoomMode ja ViewRotation ovat ohuita välittäjiä — getter lukee FModel.PageIndex-arvon, setter kirjoittaa FModel.PageIndex-arvon — joten Object Inspectorista tai koodista katsottuna ohjausobjekti näyttää pitävän tilaa suoraan hallussaan, vaikka THPDFViewerModel on ainoa paikka, jossa tila todella asuu. Kutsujat eivät myöskään rajoitu välitettyyn osajoukkoon: THPDFViewer paljastaa mallin itsensä vain luku -ominaisuuden Model: THPDFViewerModel kautta, joten koodi, joka haluaa käyttää FindFormFieldAt- tai PrefetchCurrentPageSnapshots-metodeja — kumpaakaan ohjausobjekti ei paljasta uudelleen — voi ulottua kääreen ohi ja kutsua mallia suoraan
procedure THPDFViewer.RefreshDocument;
var
Bitmap: TBitmap;
DPI: Integer;
begin
// simplified: the real method also resolves fit-mode DPI
// and composites highlight and search-hit rectangles first
if (FModel.Document = nil) or (FModel.PageIndex < 0) then Exit;
DPI := Round(96 * FModel.Zoom);
Bitmap := FModel.Document.RenderLoadedPageToBitmapCached(FModel.PageIndex, DPI);
try
FModel.ApplyViewRotation(Bitmap);
FImage.Picture.Bitmap.Assign(Bitmap);
finally
Bitmap.Free;
end;
end;
BeginUpdate ja EndUpdate: uudelleenpiirtomyrskyjen pysäyttäminen
BeginUpdate ja EndUpdate ovat olemassa, koska yksi looginen muutos koskettaa usein useaa tilan osaa kerralla, ja uudelleenpiirtäminen jokaisen osan jälkeen olisi tuhlausta ja visuaalisesti levotonta. Ladatun asiakirjan vaihtaminen on selkein esimerkki: THPDFViewerModel.Document-arvon asettaminen nollaa näkymän kierron, tyhjentää hakuosumat, tyhjentää korostusalueet ja hyppää ensimmäiselle sivulle, ja jokainen näistä vaiheista laukaisee normaalisti oman muutostapahtumansa. THPDFViewerModel kääri tuon sekvenssin BeginUpdate/EndUpdate-pariin, viittauslaskuriin perustuvaan pariin, jossa sisäkkäiset kutsut laukaisevat OnBeginUpdate-tapahtuman vain siirryttäessä uloimpaan kutsuun ja OnEndUpdate-tapahtuman vain siirryttäessä siitä pois. THPDFViewer seuraa samaa syvyyttä omalla puolellaan ja ohittaa RefreshDocument-kutsun jokaisen yksittäisen tapahtuman kohdalla, kun laskuri on nollaa suurempi, ja piirtää sitten uudelleen täsmälleen kerran, kun erä sulkeutuu. Yksittäiset tapahtumat laukeavat silti erän aikana, joten tilaaja, joka välittää vain OnSearchChange-tapahtumasta, kuulee siitä yhä; vain ohjausobjektin oma uudelleenpiirto tiivistyy yhdeksi kutsuksi neljän sijaan
Miten valintakehyksellä tehty korostus muunnetaan takaisin PDF-koordinaateiksi?
Valintakehyksellä tehty korostus muuntaa hiiren vedon takaisin PDF-koordinaateiksi kahden mallimetodin kautta, jotka on rakennettu juuri tuota edestakaista muunnosta varten: PagePointToView ja ViewPointToPage. Molemmat ottavat sivuindeksin, DPI:n ja pisteen, ja molemmat ratkaisevat muunnoksen kahdessa vaiheessa — ensin sivun omaa /Rotate-kenttää ja sen vasenta alakulmaa PDF-origona, sitten näkymän erillistä, ei-tuhoavaa ViewRotation-arvoa ja katseluohjelman vasenta yläkulmaa laiteoriona — nimenomaan siksi, että käänteinen suunta voi purkaa nämä kaksi vaihetta tarkasti käänteisessä järjestyksessä ja tehdä edestakaisen muunnoksen oikein kaikissa kuudessatoista sivun kierron ja näkymän kierron yhdistelmässä. THPDFViewer kutsuu funktiota ViewPointToPage, kun käyttäjä vapauttaa hiiren painikkeen vedettyään suorakulmion vimHighlight-vuorovaikutustilassa, muuttaa kaksi laitepistettä THPDFRectangle-olioksi sivutilassa ja antaa sen funktiolle Model.AddHighlightRegion. Yksi yksityiskohta kannattaa tuntea, jos rakennat jotain vastaavaa: hiiren kaappaus kuuluu TScrollBox-luokasta periytyvälle katseluohjelmalle, ei lapsi-TImage-olioon, johon bittikartta piirretään, koska TControl.MouseCapture on suojattu (protected) ja vain vanhempi ohjausobjekti voi ottaa sen haltuunsa — joten veto, joka poistuu kuvan rajojen ulkopuolelle ennen kuin painike vapautetaan, ratkeaa silti katseluohjelman omien ylikirjoitettujen MouseMove/MouseUp-käsittelijöiden kautta sen sijaan, että lapsiohjausobjekti hylkäisi sen hiljaisesti
var
ViewPt, PagePt: THPDFViewerPoint;
Rect: THPDFRectangle;
begin
ViewPt.X := 240; // device pixels inside the rendered image
ViewPt.Y := 96;
if Model.ViewPointToPage(Model.PageIndex, ViewPt, PagePt,
RenderedDPI) then // DPI you last rendered at
begin
Rect.Left := PagePt.X - 40; Rect.Bottom := PagePt.Y - 10;
Rect.Right := PagePt.X + 40; Rect.Top := PagePt.Y + 10;
Model.AddHighlightRegion(Model.PageIndex, Rect);
end;
end;
Mitä jako antaa vihreää testisarjaa enemmän
Hyöty ei rajoitu testeihin, jotka menevät läpi CI-ajossa ilman työpöytäistuntoa. Koska THPDFViewer välittää kutsut THPDFViewerModel-luokalle sen sijaan, että se kopioisi sen logiikan, HotPDF pystyi lisäämään kolmannen kuluttajan — THPDFViewerAction ja konkreettiset aliluokat kuten THPDFZoomInAction ja THPDFFindNextAction — joka liittää navigoinnin, zoomauksen, haun ja kierron tavalliseen Delphi-TActionList-luokkaan, joten työkalupalkin painike tai valikkokohta voi ohjata katseluohjelmaa deklaratiivisesti, ottaen itsensä automaattisesti käyttöön sen mukaan, ratkeaako katseluohjelma juuri nyt toiminnon kohteeksi. Minkään tuon kerroksen ei tarvinnut tietää mitään bittikartoista tai GDI:stä; se kutsuu Viewer.NextPage- tai Viewer.Model.FindNext-metodia, ja olemassa oleva tapahtumaketju huolehtii uudelleenpiirrosta. Ja koska mikään THPDFViewerModel-luokassa ei viittaa TScrollBox-, TImage-luokkaan tai ikkunakahvaan, allaoleva tilakone ei ole hitsattu kiinni tuohonkaan yhteen ohjausobjektiin — sama malli voisi istua eri renderöintipinnan takana koskettamatta yhtäkään riviä navigointi-, zoomaus- tai hakulogiikkaa
Missä renderöintivälimuisti auttaa, ja missä se ei auta
THPDFViewerModelin renderöintivälimuisti auttaa ladatun asiakirjan sisällä, mutta se ei muuta sitä, mitä tuon asiakirjan lataaminen maksaa alun perin. CreatePageSnapshot, CreateCurrentPageSnapshot ja esilatausmetodit PrefetchPageSnapshots/PrefetchCurrentPageSnapshots kulkevat kaikki saman, sivun ja DPI:n mukaan avaimistetun välimuistitetun renderöijän kautta, joten takaisin sivulle siirtyminen, jota on jo katsottu samalla zoomaustasolla, on välimuistiosuma eikä uudelleenrenderöinti, ja pienen naapurisivujen säteen esilataaminen tasoittaa yleistä tapausta, jossa lukija selaa eteenpäin sivu kerrallaan. Mikään tästä ei kuitenkaan kosketa alkuperäisen LoadFromFile-kutsun kustannusta, ja katseluohjelma, joka on rakennettu avaamaan mitä tahansa käyttäjä siihen raahaa, kohtaa lopulta tiedoston, joka on tarpeeksi suuri tehdäkseen tuosta kutsusta todellisen pullonkaulan. Portaittaisesta, kahvapohjaisesta vaihtoehdosta täydelle latauksille — hyvä tuntea ennen kuin se päivä koittaa — katso rinnakkaisartikkeli Direct File API:sta suurille PDF-tiedostoille
Tässä kuvatut Model- ja View-luokat ovat kaksi lisäpalasta samaa ladatun asiakirjan pintaa, jota käytetään kauttaaltaan HotPDF-komponentissa Delphille ja C++Builderille, rakennettuna ohjattavaksi lomakkeesta, TActionList-luokasta tai ei kummastakaan