HotPDF razdvaja svoj Delphi PDF preglednik na dva dela: THPDFViewerModel, običnu klasu koja bez zavisnosti od ručice prozora upravlja stanjem zumiranja, rotacije, pretrage, isticanja i navigacije, i THPDFViewer, kontrolu zasnovanu na TScrollBox koja to stanje pretvara u piksele. Zahvaljujući toj podeli logika preglednika može da se izvršava i testira bez ikakvog kreiranja forme
Većina prilagođenih kontrola za pregled dokumenata ne izgleda ovako. Nivo zumiranja živi u privatnom polju kontrole, navigacija stranica ograničava svoje granice unutar obrađivača dugmeta OnClick, a jedini način da saznate da li Ctrl+točkić poštuje gornju granicu zumiranja jeste da pokrenete aplikaciju, kliknete i pogledate rezultat. Takva kontrola radi dobro dok joj ne zatreba regresioni skup testova ili drugi domaćin — dijalog za pregled štampe, traka sa sličicama ili grupni pregled bez vidljivog prozora — a tada se ispostavi da je potrebno stanje čvrsto povezano sa kontrolom TWinControl koja zahteva stvarnu ručicu pre nego što išta uradi
Zašto je PDF pregledniku uopšte potrebna MVC podela?
PDF pregledniku je potrebna ovakva podela zato što se njegovo stanje i prikaz menjaju iz različitih razloga i različitom brzinom. Indeks stranice, zumiranje, rotacija prikaza, rezultati pretrage i oblasti isticanja predstavljaju poslovno stanje: mogu da se izračunaju, provere i serijalizuju bez ijednog piksela na ekranu. Iscrtavanje bitmapa, hvatanje miša i crtanje pravougaonika za izbor povlačenjem predstavljaju pitanja prikaza koja imaju smisla tek kada kontrola postoji. HotPDF prvu grupu čuva u THPDFViewerModel, klasi koja nema nijednog VCL pretka za rad sa prozorima, a drugu u THPDFViewer, koji poseduje instancu modela i reaguje na nju — bliže paru Model–View nego školskom troslojnom MVC-u, jer ne postoji posebna klasa Controller, a sam THPDFViewer pretvara sirove događaje tastature i miša u pozive modela. Važniji od same oznake je smer zavisnosti: ništa u THPDFViewerModel ne zahteva Handle, petlju poruka ili vidljivu radnu površinu, što upravo omogućava da HotPDF-ov skup testova kroz DUnitX proverava listanje stranica, ograničavanje zumiranja, komande tastature i povratnu transformaciju 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 se zapravo bavi THPDFViewerModel
THPDFViewerModel poseduje sve što je pregledniku potrebno da odredi šta trenutno treba da bude na ekranu, ali ne i način na koji će to nacrtati. PageIndex, PageNumber i PageCount prate položaj; Zoom i ZoomMode (vzmActualSize, vzmFitPage, vzmFitWidth, vzmCustom) prate razmeru; ViewRotation prati nedestruktivnu rotaciju na ekranu koja nikada ne menja sopstveni unos /Rotate stranice. Metode za navigaciju — FirstPage, PriorPage, NextPage, LastPage — i metode za zumiranje — ZoomIn, ZoomOut, koje prolaze kroz fiksnu tabelu od devetnaest unapred podešenih nivoa od 5% do 6400% — takođe žive ovde, zajedno sa FindAll/FindNext/FindPrevious za pretragu teksta i AddHighlightRegion/RemoveHighlightRegion/ClearHighlightRegions za trajne oznake stranice koje pozivalac želi da zadrži između iscrtavanja. Model upravlja i izlazom i ulazom: CreateCurrentPageSnapshot i CreateCurrentPageMetafile izvoze upravo stranicu koja je trenutno na ekranu, a PrintCurrentView šalje isti trenutni prikaz — trenutnu stranicu, DPI izveden iz trenutnog zumiranja i trenutnu rotaciju — u TPrinter, kao uži posao vezan za prikaz u odnosu na štampanje celog dokumenta opisano u HotPDF vodiču za štampanje pomoću TPrinter-a. Svaka važna promena podiže odgovarajući događaj — OnPageChange, OnZoomChange, OnSearchChange, OnHighlightChange, OnViewRotationChange — pa pretplatnik saznaje šta se promenilo bez anketiranja stanja
Kako THPDFViewer zna kada treba ponovo da iscrta prikaz?
THPDFViewer zna kada treba ponovo da iscrta prikaz zato što se pretplaćuje na model umesto da nagađa. Konstruktor THPDFViewer-a kreira privatni THPDFViewerModel, a zatim povezuje svaki njegov događaj obaveštenja — OnBeginUpdate, OnEndUpdate, OnHighlightChange, OnPageChange, OnSearchChange, OnViewRotationChange, OnZoomChange — sa odgovarajućim privatnim obrađivačem. Zadatak svakog obrađivača je mali: poziva RefreshDocument, metodu koja rasterizuje trenutnu stranicu kroz isti keširani prikazivač stranice opisan u unutrašnjim detaljima HotPDF renderovanja stranice u bitmapu, zatim preko toga iscrtava okvire isticanja i rezultate pretrage i primenjuje trenutnu rotaciju prikaza. Objavljena svojstva kao što su PageIndex, Zoom, ZoomMode i ViewRotation samo prosleđuju vrednosti — getter čita FModel.PageIndex, setter upisuje FModel.PageIndex — pa iz Object Inspectora ili koda izgleda kao da kontrola direktno čuva stanje, iako je THPDFViewerModel jedino mesto na kojem to stanje stvarno postoji. Pozivaoci nisu ograničeni ni na prosleđeni podskup: THPDFViewer izlaže sam model kroz svojstvo samo za čitanje Model: THPDFViewerModel, pa kodu koji želi FindFormFieldAt ili PrefetchCurrentPageSnapshots — nijednu od tih metoda kontrola ne izlaže ponovo — omogućava da zaobiđe omotač i direktno pozove 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: zaustavljanje lavine ponovnog iscrtavanja
BeginUpdate i EndUpdate postoje zato što jedna logička promena često istovremeno dotiče nekoliko delova stanja, a ponovno iscrtavanje posle svakog dela bilo bi rasipno i vizuelno bučno. Zamena učitanog dokumenta najjasniji je primer: dodeljivanje THPDFViewerModel.Document poništava rotaciju prikaza, briše rezultate pretrage, briše oblasti isticanja i prelazi na prvu stranicu, a svaki od tih koraka obično pokreće sopstveni događaj promene. THPDFViewerModel obuhvata taj niz pozivima BeginUpdate/EndUpdate, parom sa brojanjem referenci u kojem ugnježdeni pozivi pokreću OnBeginUpdate samo pri prelasku u krajnji spoljašnji poziv, a OnEndUpdate pri povratku iz njega. THPDFViewer prati istu dubinu na svojoj strani i preskače RefreshDocument za svaki pojedinačni događaj dok je brojač veći od nule, a zatim ponovo iscrtava prikaz tačno jednom kada se paket zatvori. Pojedinačni događaji se i dalje pokreću tokom paketa, pa pretplatnik koji prati samo OnSearchChange i dalje dobija obaveštenje; samo se ponovno iscrtavanje same kontrole sabija u jedan poziv umesto u četiri
Kako se isticanje povlačenjem vraća na PDF koordinate?
Isticanje povlačenjem vraća pokret miša na PDF koordinate pomoću para metoda modela napravljenih upravo za tu povratnu transformaciju: PagePointToView i ViewPointToPage. Obe primaju indeks stranice, DPI i tačku, a transformaciju rešavaju u dve faze — najpre sopstveni unos /Rotate stranice i njeno PDF poreklo u donjem levom uglu, zatim zasebnu, nedestruktivnu rotaciju prikaza ViewRotation i koordinatni sistem uređaja sa poreklom u gornjem levom uglu — upravo tako da inverzni smer može da poništi te dve faze strogo obrnutim redosledom i ispravno vrati početnu tačku kroz svih šesnaest kombinacija rotacije stranice i prikaza. THPDFViewer poziva ViewPointToPage kada korisnik pusti dugme miša nakon povlačenja pravougaonika u režimu interakcije vimHighlight, pretvara dve tačke uređaja u THPDFRectangle u prostoru stranice i prosleđuje ga metodi Model.AddHighlightRegion. Vredi znati još jedan detalj ako pravite nešto slično: hvatanje miša pripada pregledniku izvedenom iz TScrollBox, a ne podređenom TImage u koji se iscrtava bitmapa, jer je TControl.MouseCapture zaštićen i samo roditeljska kontrola može da ga preuzme — zato se povlačenje koje napusti granice slike pre otpuštanja dugmeta i dalje obrađuje kroz redefinisane metode preglednika MouseMove/MouseUp, umesto da ga podređena kontrola nečujno 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;
Šta ova podela donosi osim zelenog skupa testova
Dobit se ne svodi na prolazak testova u CI poslu bez sesije radne površine. Pošto THPDFViewer prosleđuje pozive ka THPDFViewerModel-u umesto da duplira njegovu logiku, HotPDF je mogao da doda trećeg korisnika — THPDFViewerAction i konkretne podklase kao što su THPDFZoomInAction i THPDFFindNextAction — koje navigaciju, zumiranje, pretragu i rotaciju povezuju sa standardnim Delphi TActionList, pa dugme na traci sa alatkama ili stavka menija mogu deklarativno da upravljaju preglednikom i automatski se omoguće na osnovu toga da li je preglednik trenutno pronađen kao cilj radnje. Nijedan deo tog sloja nije morao da zna bilo šta o bitmapama ili GDI-ju; poziva Viewer.NextPage ili Viewer.Model.FindNext, a postojeći lanac događaja brine o ponovnom iscrtavanju. Pošto se ništa u THPDFViewerModel-u ne poziva na TScrollBox, TImage ili ručicu prozora, ni osnovna mašina stanja nije čvrsto vezana za tu jednu kontrolu — isti model može da stoji iza druge površine za iscrtavanje bez izmene ijednog reda logike navigacije, zumiranja ili pretrage
Gde keš renderovanja pomaže, a gde ne pomaže
Keš renderovanja THPDFViewerModel-a pomaže unutar učitanog dokumenta, ali ne menja cenu prvog učitavanja tog dokumenta. CreatePageSnapshot, CreateCurrentPageSnapshot i metode za unapred učitavanje PrefetchPageSnapshots/PrefetchCurrentPageSnapshots sve usmeravaju obradu kroz isti keširani prikazivač, čiji su ključ stranica i DPI, pa je povratak na stranicu koju ste već gledali pri istom nivou zumiranja pogodak u keš umesto novog renderovanja, a unapred učitavanje malog broja susednih stranica ublažava uobičajeni slučaj čitaoca koji prelazi napred stranicu po stranicu. Ništa od toga, međutim, ne utiče na cenu početnog poziva LoadFromFile, pa preglednik napravljen da otvori sve što korisnik prevuče na njega na kraju nailazi na dovoljno veliku datoteku da taj poziv postane stvarno usko grlo. Za alternativu sa nivoima i ručicom zasnovanu na radu bez potpunog učitavanja — korisno je znati za nju pre nego što taj dan dođe — pogledajte prateći članak o Direct File API-ju za velike PDF datoteke
Klase Model i View opisane ovde još su dva dela iste površine učitanog dokumenta koja se koristi u okviru HotPDF komponente za Delphi i C++Builder, napravljene da se njima upravlja iz forme, iz TActionList-a ili bez ijednog od toga