HotPDF dijeli svoj Delphi PDF preglednik na dva dijela: THPDFViewerModel, običnu klasu koja bez ovisnosti o prozorskoj ručki upravlja stanjem zumiranja, rotacije, pretraživanja, isticanja i navigacije, te THPDFViewer, kontrolu temeljenu na TScrollBoxu koja to stanje pretvara u piksele. Upravo to razdvajanje omogućuje pokretanje i testiranje logike preglednika bez stvaranja obrasca
Većina prilagođenih kontrola preglednika ne izgleda ovako. Razina zumiranja živi u privatnom polju kontrole, navigacija stranica ograničava svoje granice unutar rukovatelja gumba OnClick, a jedini način da saznate poštuje li Ctrl+pomicanje gornju granicu zumiranja jest pokrenuti aplikaciju, kliknuti i pogledati. Takva kontrola dobro radi dok joj ne zatreba regresijski paket testova ili drugi domaćin — dijaloški okvir za pretpregled ispisa, traka minijatura ili paketni preglednik bez vidljivog prozora — a tada se potrebno stanje pokaže zavarenim za TWinControl koji inzistira na stvarnoj ručki prije nego što išta učini
Zašto PDF preglednik uopće treba MVC razdvajanje
PDF pregledniku treba ovakvo razdvajanje jer se njegovo stanje i prezentacija mijenjaju iz različitih razloga i različitom brzinom. Indeks stranice, zumiranje, rotacija prikaza, rezultati pretraživanja i područja isticanja poslovno su stanje: mogu se izračunati, provjeriti i serijalizirati bez ijednog piksela na zaslonu. Crtanje bitmape, hvatanje miša i crtanje pravokutnika za odabir imaju smisla tek kada kontrola postoji. HotPDF prvu skupinu drži u THPDFViewerModel, klasi bez ikakvog VCL pretka za prozore, a drugu u THPDFViewer, koji posjeduje instancu modela i reagira na nju — bliže paru Model-View nego školskom troslojnom MVC-u jer ne postoji zasebna klasa Controller, a sam THPDFViewer pretvara sirove događaje tipkovnice i miša u pozive modela. Važniji od naziva je smjer ovisnosti: ništa u THPDFViewerModelu ne zahtijeva Handle, petlju poruka ni vidljivu radnu površinu, što upravo omogućuje da HotPDF-ov vlastiti paket testova kroz DUnitX upravlja listanjem stranica, ograničavanjem zumiranja, naredbama tipkovnice i povratnim pretvorbama koordinata bez otvaranja prozora
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;
Čime zapravo upravlja THPDFViewerModel
THPDFViewerModel upravlja svime što pregledniku treba da odgovori na pitanje što se trenutno treba nalaziti na zaslonu, a ne upravlja načinom crtanja. PageIndex, PageNumber i PageCount prate položaj; Zoom i ZoomMode (vzmActualSize, vzmFitPage, vzmFitWidth, vzmCustom) prate mjerilo; ViewRotation prati nedestruktivnu rotaciju na zaslonu koja nikada ne dira vlastiti unos stranice /Rotate. Ovdje žive i metode navigacije — FirstPage, PriorPage, NextPage, LastPage — te metode zumiranja — ZoomIn, ZoomOut, koje prolaze kroz fiksnu tablicu od devetnaest unaprijed postavljenih razina od 5% do 6400% — zajedno s metodama FindAll/FindNext/FindPrevious za pretraživanje teksta i AddHighlightRegion/RemoveHighlightRegion/ClearHighlightRegions za trajne oznake stranice koje pozivatelj želi zadržati između prikaza. Model upravlja izlazom jednako kao i ulazom: CreateCurrentPageSnapshot i CreateCurrentPageMetafile izvoze upravo stranicu koja je trenutačno na zaslonu, a PrintCurrentView šalje isti trenutačni prikaz — trenutačnu stranicu, DPI izveden iz trenutačnog zumiranja i trenutačnu rotaciju — u TPrinter, kao uži posao vezan uz prikaz u odnosu na cjevovod ispisa cijelog dokumenta opisan u vodiču za ispis HotPDF-a pomoću TPrintera. Svaka važna izmjena podiže i odgovarajući događaj — OnPageChange, OnZoomChange, OnSearchChange, OnHighlightChange, OnViewRotationChange — pa pretplatnik saznaje što se promijenilo bez anketiranja
Kako THPDFViewer zna kada treba ponovno crtati
THPDFViewer zna kada treba ponovno crtati jer se pretplaćuje na model umjesto da nagađa. Konstruktor THPDFViewera stvara privatni THPDFViewerModel, a zatim svaki njegov događaj obavijesti — OnBeginUpdate, OnEndUpdate, OnHighlightChange, OnPageChange, OnSearchChange, OnViewRotationChange, OnZoomChange — povezuje s odgovarajućim privatnim rukovateljem. Posao svakog rukovatelja je malen: pozvati RefreshDocument, metodu koja zapravo rasterizira trenutačnu stranicu kroz isti predmemorirani renderer stranice opisan u unutarnjem postupku renderiranja HotPDF stranice u bitmapu, zatim preko slike složiti okvire isticanja i rezultate pretraživanja te primijeniti trenutačnu rotaciju prikaza. Objavljena svojstva poput PageIndex, Zoom, ZoomMode i ViewRotation tanki su prosljeđivači — dohvat čita FModel.PageIndex, a postavljač zapisuje FModel.PageIndex — pa iz Object Inspectora ili koda kontrola izgleda kao da stanje drži izravno, iako je THPDFViewerModel jedino mjesto na kojem to stanje stvarno postoji. Pozivatelji nisu ograničeni ni na proslijeđeni podskup: THPDFViewer sam model izlaže kroz svojstvo samo za čitanje Model: THPDFViewerModel, pa kod koji želi FindFormFieldAt ili PrefetchCurrentPageSnapshots — nijedno od toga kontrola ponovno ne izlaže — može zaobići omotač i izravno pozvati model
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 i EndUpdate zaustavljaju oluje ponovnog crtanja
BeginUpdate i EndUpdate postoje jer jedna logička promjena često istodobno dodiruje nekoliko dijelova stanja, a ponovno crtanje nakon svakog dijela bilo bi rasipno i vizualno bučno. Zamjena učitanog dokumenta najjasniji je primjer: dodjela THPDFViewerModel.Document resetira rotaciju prikaza, briše rezultate pretraživanja, briše područja isticanja i skače na prvu stranicu, a svaki od tih koraka obično pokreće vlastiti događaj promjene. THPDFViewerModel taj slijed omata u BeginUpdate/EndUpdate, par s brojanjem referenci u kojemu ugniježđeni pozivi pokreću OnBeginUpdate samo pri prijelazu u najvanjskiji poziv, a OnEndUpdate pri prijelazu natrag iz njega. THPDFViewer istu dubinu prati na svojoj strani i preskače RefreshDocument za svaki pojedinačni događaj dok je brojač veći od nule, a zatim ponovno crta točno jednom kada se paket zatvori. Pojedinačni događaji i dalje se pokreću tijekom paketa, pa pretplatnik koji prati samo OnSearchChange i dalje prima obavijest; samo se ponovno crtanje same kontrole sažima u jedan poziv umjesto četiri
Kako se označavanje pravokutnikom mišem vraća u PDF koordinate
Označavanje pravokutnikom vraća povlačenje miša u PDF koordinate kroz par metoda modela napravljenih upravo za taj povratni put: PagePointToView i ViewPointToPage. Obje primaju indeks stranice, DPI i točku, a transformaciju rješavaju u dvije faze — najprije vlastiti unos stranice /Rotate i ishodište PDF-a u donjem lijevom kutu, zatim zasebnu nedestruktivnu vrijednost ViewRotation i ishodište uređaja preglednika u gornjem lijevom kutu — upravo kako bi inverzni smjer mogao poništiti te dvije faze strogim obrnutim redoslijedom i ispravno vratiti izvornu točku kroz svih šesnaest kombinacija rotacije stranice i rotacije prikaza. THPDFViewer poziva ViewPointToPage kada korisnik nakon povlačenja pravokutnika otpusti miš u načinu interakcije vimHighlight, pretvara dvije točke uređaja u THPDFRectangle u prostoru stranice i predaje ga metodi Model.AddHighlightRegion. Ako izrađujete nešto slično, vrijedi znati jedan detalj: hvatanje miša pripada pregledniku izvedenom iz TScrollBoxa, a ne podređenom TImageu u koji se crta bitmapa, jer je TControl.MouseCapture zaštićen i samo ga nadređena kontrola može preuzeti — zato se povlačenje koje napusti granice slike prije otpuštanja gumba i dalje rješava kroz vlastiti nadjačani MouseMove/MouseUp preglednika, umjesto da ga podređena kontrola tiho odbaci
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;
Što vam ovo razdvajanje donosi osim zelene testne zbirke
Dobit nije ograničena na prolazak testova u CI poslu bez radne površine. Budući da THPDFViewer prosljeđuje pozive THPDFViewerModelu umjesto da duplicira njegovu logiku, HotPDF je mogao dodati trećeg korisnika — THPDFViewerAction i konkretne podklase poput THPDFZoomInAction i THPDFFindNextAction — koje navigaciju, zumiranje, pretraživanje i rotaciju priključuju na standardni Delphi TActionList, pa gumb alatne trake ili stavka izbornika mogu deklarativno upravljati preglednikom i automatski se omogućiti ovisno o tome je li preglednik trenutačno razriješen kao cilj akcije. Taj sloj nije morao znati ništa o bitmapama ni GDI-ju; poziva Viewer.NextPage ili Viewer.Model.FindNext, a postojeći lanac događaja brine o ponovnom crtanju. A budući da ništa u THPDFViewerModelu ne upućuje na TScrollBox, TImage ni prozorsku ručku, ni donji stroj stanja nije zavaren za tu jednu kontrolu — isti bi model mogao stajati iza druge površine za prikaz bez izmjene ijednog retka logike navigacije, zumiranja ili pretraživanja
Gdje predmemorija renderiranja pomaže, a gdje ne
Predmemorija renderiranja THPDFViewerModela pomaže unutar učitanog dokumenta, ali ne mijenja cijenu njegova početnog učitavanja. CreatePageSnapshot, CreateCurrentPageSnapshot i metode prethodnog dohvaćanja PrefetchPageSnapshots/PrefetchCurrentPageSnapshots sve prolaze kroz isti predmemorirani renderer, čiji je ključ stranica i DPI, pa je povratak na stranicu koju ste već gledali pri istoj razini zumiranja pogodak u predmemoriji umjesto novog renderiranja, a prethodno dohvaćanje malog raspona susjednih stranica ublažava uobičajeni slučaj čitatelja koji napreduje jednu po jednu stranicu. Ništa od toga ne dira cijenu početnog poziva LoadFromFile, a preglednik napravljen da otvori sve što korisnik povuče na njega prije ili poslije naiđe na dovoljno veliku datoteku da taj poziv postane stvarno usko grlo. O alternativi punom učitavanju koja se temelji na razinama i ručki — o njoj vrijedi znati prije nego što dođe taj dan — pročitajte u pratećem članku o Direct File API-ju za velike PDF-ove
Klase Model i View opisane ovdje još su dva dijela iste površine učitanog dokumenta koja se koristi u cijeloj HotPDF komponenti za Delphi i C++Builder, napravljene za upravljanje iz obrasca, iz TActionLista ili ni iz jednog od njih