HotPDF razdeli svoj pregledovalnik PDF v Delphiju na dva dela: THPDFViewerModel, navaden razred, ki brez odvisnosti od ročaja okna upravlja stanje povečave, vrtenja, iskanja, označevanja in krmarjenja, ter THPDFViewer, kontrolnik na osnovi TScrollBox, ki to stanje pretvori v slikovne pike. Prav ta razdelitev omogoča izvajanje in testiranje logike pregledovalnika, ne da bi bilo treba kadar koli ustvariti obrazec
Večina kontrolnikov za pregledovalnike po meri ni zasnovana tako. Raven povečave živi v zasebnem polju kontrolnika, krmarjenje po straneh omeji svoje meje znotraj obravnavalnika OnClick gumba, edini način za preverjanje, ali Ctrl+pomikanje upošteva zgornjo mejo povečave, pa je zagon aplikacije, klikanje in opazovanje. Tak kontrolnik deluje dobro, dokler ne potrebuje regresijskega nabora ali drugega gostitelja — pogovornega okna za predogled tiskanja, traku s sličicami, pregledovalnika paketov brez vidnega okna — nato pa ugotovite, da je potrebno stanje privarjeno na TWinControl, ki vztraja pri pravem ročaju, preden kar koli naredi
Zakaj kontrolnik za pregledovanje PDF sploh potrebuje razdelitev MVC
Pregledovalnik PDF potrebuje takšno razdelitev, ker se njegovo stanje in predstavitev spreminjata iz različnih razlogov in z različno hitrostjo. Indeks strani, povečava, vrtenje pogleda, zadetki iskanja in območja označevanja so poslovno stanje: mogoče jih je izračunati, preveriti in serializirati brez ene same slikovne pike na zaslonu. Slikanje bitne slike, zajem miške in risanje pravokotnika za izbiro z vlečenjem pa so predstavitvene naloge, ki imajo smisel šele, ko kontrolnik obstaja. HotPDF prvo skupino ohranja v THPDFViewerModel, razredu brez prednika za VCL-okna, drugo pa v THPDFViewer, ki ima instanco modela in se nanjo odziva — bolj par Model-View kot učbeniški večnivojski MVC, saj ni ločenega razreda Controller in THPDFViewer sam pretvarja neobdelane dogodke tipkovnice in miške v klice modela. Pomembnejša od oznake je smer odvisnosti: nič v THPDFViewerModel ne zahteva Handle, zanke sporočil ali vidnega namizja, zato lahko HotPDF-ov lastni testni nabor prek DUnitX poganja listanje, omejevanje povečave, ukaze tipkovnice in povratne preslikave koordinat, ne da bi odprl okno
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;
Kaj dejansko upravlja THPDFViewerModel
THPDFViewerModel upravlja vse, kar pregledovalnik potrebuje za odgovor na vprašanje, kaj mora biti trenutno na zaslonu, ne da bi upravljal način risanja. PageIndex, PageNumber in PageCount sledijo položaju; Zoom in ZoomMode (vzmActualSize, vzmFitPage, vzmFitWidth, vzmCustom) sledijo merilu; ViewRotation sledi nedestruktivnemu vrtenju na zaslonu, ki se nikoli ne dotakne lastnega vnosa strani /Rotate. Tu živijo tudi metode za krmarjenje — FirstPage, PriorPage, NextPage, LastPage — in metode povečave — ZoomIn, ZoomOut, ki se sprehajata po stalni tabeli devetnajstih prednastavljenih ravni od 5 % do 6400 % — skupaj z FindAll/FindNext/FindPrevious za iskanje besedila in AddHighlightRegion/RemoveHighlightRegion/ClearHighlightRegions za trajne oznake strani, ki jih želi klicatelj ohraniti med izrisi. Model upravlja izhod enako kot vhod: CreateCurrentPageSnapshot in CreateCurrentPageMetafile izvozita točno stran, ki je trenutno na zaslonu, PrintCurrentView pa ta isti trenutni pogled — trenutno stran, trenutno DPI-vrednost, izpeljano iz povečave, in trenutno vrtenje — pošlje v TPrinter, kar je ožje opravilo, omejeno na pogled, od postopka tiskanja celotnega dokumenta, opisanega v navodilih za tiskanje HotPDF s TPrinter. Vsaka pomembna sprememba sproži tudi ustrezen dogodek — OnPageChange, OnZoomChange, OnSearchChange, OnHighlightChange, OnViewRotationChange — zato naročnik izve, kaj se je spremenilo, brez periodičnega preverjanja
Kako THPDFViewer ve, kdaj mora znova izrisati vsebino
THPDFViewer ve, kdaj mora znova izrisati vsebino, ker se naroči na model, namesto da bi ugibal. Konstruktor THPDFViewer ustvari zasebni THPDFViewerModel, nato pa vsak njegov obvestilni dogodek — OnBeginUpdate, OnEndUpdate, OnHighlightChange, OnPageChange, OnSearchChange, OnViewRotationChange, OnZoomChange — poveže z ustreznim zasebnim obravnavalnikom. Naloga vsakega obravnavalnika je majhna: pokliče RefreshDocument, metodo, ki z isto predpomnjeno upodabljalno stranjo dejansko rasterizira trenutno stran, kot je opisano v notranjosti upodabljanja strani HotPDF v bitno sliko, nato čeznjo sestavi polja označevanja in zadetke iskanja ter uporabi trenutno vrtenje pogleda. Objavljene lastnosti, kot so PageIndex, Zoom, ZoomMode in ViewRotation, so tanki posredniki — pridobivalnik bere FModel.PageIndex, nastavljalnik zapisuje v FModel.PageIndex — zato je z vidika Object Inspectorja ali kode videti, kot da kontrolnik stanje hrani neposredno, čeprav je THPDFViewerModel edino mesto, kjer stanje dejansko živi. Klicatelji niso omejeni niti na posredovani podnabor: THPDFViewer izpostavi sam model prek lastnosti samo za branje Model: THPDFViewerModel, zato lahko koda, ki želi FindFormFieldAt ali PrefetchCurrentPageSnapshots — česar kontrolnik ne izpostavi ponovno — poseže mimo ovoja in model pokliče neposredno
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 in EndUpdate: kako preprečiti poplavo ponovnih izrisov
BeginUpdate in EndUpdate obstajata, ker ena logična sprememba pogosto hkrati poseže v več delov stanja, ponovni izris po vsakem delu pa bi bil potraten in vizualno moteč. Zamenjava naloženega dokumenta je najbolj jasen primer: dodelitev THPDFViewerModel.Document ponastavi vrtenje pogleda, počisti zadetke iskanja, počisti območja označevanja in skoči na prvo stran, vsak od teh korakov pa običajno sproži svoj dogodek spremembe. THPDFViewerModel to zaporedje ovije v BeginUpdate/EndUpdate, referenčno štet par, pri katerem gnezdeni klici sprožijo OnBeginUpdate samo ob prehodu v zunanji klic in OnEndUpdate ob prehodu nazaj iz njega. THPDFViewer enako globino spremlja na svoji strani in preskoči RefreshDocument za vsak podrobni dogodek, dokler je števec večji od nič, nato pa ob zaključku paketa izriše vsebino natanko enkrat. Podrobni dogodki se med paketom še vedno sprožijo, zato jih naročnik, ki ga zanima samo OnSearchChange, še vedno prejme; skrčen je samo lasten ponovni izris kontrolnika, in sicer na en klic namesto štirih
Kako označevanje z vlečenjem preslika poteg miške nazaj v koordinate PDF
Označevanje z vlečenjem preslika poteg miške nazaj v koordinate PDF prek para metod modela, zgrajenih prav za to povratno pot: PagePointToView in ViewPointToPage. Obe sprejmeta indeks strani, DPI in točko, transformacijo pa rešita v dveh stopnjah — najprej lasten vnos strani /Rotate in njen izvor PDF spodaj levo, nato ločeno, nedestruktivno ViewRotation pogleda in izvor naprave pregledovalnika zgoraj levo — prav zato, da lahko obratna smer obe stopnji razveljavi v strogo obratnem vrstnem redu in pravilno tvori povratno preslikavo vseh šestnajstih kombinacij vrtenja strani in pogleda. THPDFViewer pokliče ViewPointToPage, ko uporabnik po vlečenju pravokotnika v načinu interakcije vimHighlight spusti miško, pretvori dve točki naprave v THPDFRectangle v prostoru strani in ju preda v Model.AddHighlightRegion. Če gradite nekaj podobnega, je pomembna še ena podrobnost: zajem miške pripada pregledovalniku, izpeljanemu iz TScrollBox, ne podrejeni TImage, v katero se izriše bitna slika, saj je TControl.MouseCapture zaščiten in ga lahko prevzame samo nadrejeni kontrolnik — zato se poteg, ki zapusti meje slike, preden se gumb sprosti, še vedno reši prek lastnih prepisanih metod pregledovalnika MouseMove/MouseUp, namesto da bi ga podrejeni kontrolnik tiho zavrgel
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;
Kaj vam razdelitev prinese poleg zelene zbirke testov
Korist ni omejena na uspešno izvajanje testov v opravilu CI brez namizne seje. Ker THPDFViewer posreduje v THPDFViewerModel, namesto da bi podvajal njegovo logiko, je lahko HotPDF dodal tretjega uporabnika — THPDFViewerAction in konkretna podrazreda, kot sta THPDFZoomInAction in THPDFFindNextAction — ki krmarjenje, povečavo, iskanje in vrtenje priključijo na standardni Delphi TActionList, zato lahko gumb orodne vrstice ali menijski element deklarativno upravlja pregledovalnik in se samodejno omogoči glede na to, ali je pregledovalnik trenutno razrešen kot cilj dejanja. Nobena od teh plasti ni morala vedeti ničesar o bitnih slikah ali GDI; pokliče Viewer.NextPage ali Viewer.Model.FindNext, obstoječa veriga dogodkov pa poskrbi za ponovni izris. In ker se nič v THPDFViewerModel ne sklicuje na TScrollBox, TImage ali ročaj okna, tudi spodnji avtomat stanj ni privarjen na ta en kontrolnik — isti model bi lahko stal za drugo upodabljalno površino, ne da bi se dotaknili ene vrstice logike krmarjenja, povečave ali iskanja
Kje pomaga predpomnilnik upodabljanja in kje ne
Predpomnilnik upodabljanja THPDFViewerModel pomaga znotraj naloženega dokumenta, ne spremeni pa cene prvega nalaganja tega dokumenta. CreatePageSnapshot, CreateCurrentPageSnapshot in metode vnaprejšnjega pridobivanja PrefetchPageSnapshots/PrefetchCurrentPageSnapshots vse potekajo prek istega predpomnjenega upodabljalnika, ključanega po strani in DPI, zato je vrnitev na stran, ki ste si jo že ogledali pri isti ravni povečave, zadetek v predpomnilniku namesto novega izrisa, vnaprejšnje pridobivanje majhnega obsega sosednjih strani pa zgladi običajen primer bralca, ki se pomika naprej po eno stran. Vse to pa se ne dotakne stroška začetnega klica LoadFromFile, pri pregledovalniku, zgrajenem za odpiranje vsega, kar uporabnik povleče nanj, pa se prej ali slej pojavi dovolj velika datoteka, da ta klic postane pravo ozko grlo. O stopenjski alternativi, ki temelji na ročajih in ne zahteva celotnega nalaganja — koristno jo je poznati, preden pride ta dan — preberite spremljevalni članek o Direct File API za velike PDF-je
Razreda Model in View, opisana tukaj, sta še dva dela iste površine za naložene dokumente, ki se uporablja po celotni komponenti HotPDF za Delphi in C++Builder, zasnovani za uporabo iz obrazca, iz TActionList ali pa sploh brez enega in drugega