HotPDF padalija savo Delphi PDF peržiūros programą į dvi dalis: THPDFViewerModel yra paprasta klasė, valdanti mastelio keitimo, pasukimo, paieškos, paryškinimo ir naršymo būseną be priklausomybės nuo lango rankenos, o THPDFViewer yra TScrollBox pagrindu sukurtas valdiklis, paverčiantis šią būseną pikseliais. Dėl šio atskyrimo peržiūros programos logiką galima vykdyti ir testuoti visiškai nesukuriant formos
Dauguma pasirinktinių peržiūros valdiklių taip neatrodo. Mastelio lygis laikomas privačiame valdiklio lauke, puslapių naršymas apriboja ribas mygtuko OnClick apdorotojo viduje, o vienintelis būdas sužinoti, ar Ctrl+slankiklis gerbia mastelio ribą, yra paleisti programą, spustelėti ir pažiūrėti. Taip sukurtas valdiklis veikia gerai, kol prireikia regresijos rinkinio arba antrojo naudotojo — spausdinimo peržiūros dialogo, miniatiūrų juostos ar paketinio tikrintuvo, veikiančio be matomo lango — ir paaiškėja, kad reikalinga būsena suvirinta su TWinControl, kuris prieš ką nors darydamas reikalauja tikros rankenos
Kodėl PDF peržiūros valdikliui apskritai reikia MVC atskyrimo?
PDF peržiūros programai tokio atskyrimo reikia todėl, kad jos būsena ir pateikimas keičiasi dėl skirtingų priežasčių ir skirtingu tempu. Puslapio indeksas, mastelis, rodinio pasukimas, paieškos rezultatai ir paryškinimo sritys yra verslo būsena: juos galima apskaičiuoti, patikrinti ir serializuoti be nė vieno ekrano pikselio. Taškinio piešinio atvaizdavimas, pelės fiksavimas ir pažymėjimo stačiakampio piešimas yra pateikimo rūpesčiai, prasmingi tik tada, kai egzistuoja valdiklis. HotPDF pirmąją grupę laiko THPDFViewerModel, klase, neturinčia jokio VCL langų protėvio, o antrąją — THPDFViewer, kuris turi modelio egzempliorių ir į jį reaguoja, todėl tai labiau Model-View pora nei vadovėlinis trijų sluoksnių MVC, nes nėra atskiros Controller klasės, o pats THPDFViewer neapdorotus klaviatūros ir pelės įvykius paverčia modelio iškvietimais. Svarbiau už pavadinimą yra priklausomybių kryptis: THPDFViewerModel nereikia Handle, pranešimų ciklo ar matomo darbalaukio, būtent todėl HotPDF testų rinkinys gali per DUnitX tikrinti puslapiavimą, mastelio ribojimą, klaviatūros komandas ir koordinačių apvalius transformavimus neatverdamas lango
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;
Ką iš tikrųjų valdo THPDFViewerModel?
THPDFViewerModel valdo viską, ko peržiūros programai reikia atsakyti, kas šiuo metu turi būti ekrane, tačiau nevaldo, kaip tai piešti. PageIndex, PageNumber ir PageCount seka padėtį; Zoom ir ZoomMode (vzmActualSize, vzmFitPage, vzmFitWidth, vzmCustom) seka mastelį; ViewRotation seka neardomą pasukimą ekrane, kuris niekada nepakeičia paties puslapio /Rotate įrašo. Naršymo metodai — FirstPage, PriorPage, NextPage, LastPage — ir mastelio metodai — ZoomIn, ZoomOut, pereinantys fiksuotą devyniolikos lygių lentelę nuo 5% iki 6400% — taip pat gyvena čia, kartu su FindAll/FindNext/FindPrevious teksto paieškai ir AddHighlightRegion/RemoveHighlightRegion/ClearHighlightRegions ilgalaikėms puslapio anotacijoms, kurias iškvietėjas nori išlaikyti tarp atvaizdavimų. Modelis valdo ir išvestį, ir įvestį: CreateCurrentPageSnapshot bei CreateCurrentPageMetafile eksportuoja tiksliai šiuo metu ekrane rodomą puslapį, o PrintCurrentView siunčia tą patį dabartinį rodinį — dabartinį puslapį, iš mastelio išvestą DPI ir dabartinį pasukimą — į TPrinter. Tai siauresnė, į rodinį orientuota užduotis nei viso dokumento spausdinimo procesas, aprašytas HotPDF TPrinter spausdinimo apžvalgoje. Kiekviena svarbi mutacija taip pat sukelia atitinkamą įvykį — OnPageChange, OnZoomChange, OnSearchChange, OnHighlightChange, OnViewRotationChange — todėl prenumeratorius sužino, kas pasikeitė, be apklausos
Kaip THPDFViewer žino, kada reikia perpiešti?
THPDFViewer žino, kada reikia perpiešti, nes prenumeruoja modelį, o ne spėlioja. THPDFViewer konstruktorius sukuria privatų THPDFViewerModel, tada kiekvieną jo pranešimo įvykį — OnBeginUpdate, OnEndUpdate, OnHighlightChange, OnPageChange, OnSearchChange, OnViewRotationChange, OnZoomChange — susieja su atitinkamu privačiu apdorotoju. Kiekvieno apdorotojo darbas nedidelis: iškviesti RefreshDocument, metodą, kuris iš tikrųjų rastruoja dabartinį puslapį per tą pačią spartinančiąją puslapių atvaizdavimo programą, aprašytą HotPDF puslapio atvaizdavimo į taškinį piešinį viduje, tada ant viršaus sudėti paryškinimo langelius ir paieškos rezultatus bei pritaikyti dabartinį rodinio pasukimą. Publikuotos savybės, tokios kaip PageIndex, Zoom, ZoomMode ir ViewRotation, yra ploni tarpininkai: getter skaito FModel.PageIndex, setter rašo FModel.PageIndex, todėl iš Object Inspector arba kodo valdiklis atrodo taip, lyg būseną laikytų tiesiogiai, nors iš tikrųjų ji gyvena tik THPDFViewerModel. Iškvietėjai taip pat neapsiriboja persiųstu pogrupiu: THPDFViewer patį modelį atskleidžia per tik skaityti skirtą Model: THPDFViewerModel savybę, todėl kodas, kuriam reikia FindFormFieldAt arba PrefetchCurrentPageSnapshots, kurių valdiklis nepersiunčia, gali apeiti apvalkalą ir tiesiogiai iškviesti 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 ir EndUpdate: kaip sustabdyti perpiešimo audras
BeginUpdate ir EndUpdate egzistuoja todėl, kad vienas loginis pakeitimas dažnai vienu metu paliečia kelias būsenos dalis, o perpiešti po kiekvienos dalies būtų neefektyvu ir vizualiai triukšminga. Įkelto dokumento pakeitimas yra aiškiausias pavyzdys: priskyrus THPDFViewerModel.Document iš naujo nustatomas rodinio pasukimas, išvalomi paieškos rezultatai, išvalomos paryškinimo sritys ir pereinama į pirmą puslapį, o kiekvienas iš šių veiksmų paprastai sukelia atskirą pakeitimo įvykį. THPDFViewerModel šią seką apgaubia BeginUpdate/EndUpdate, nuorodų skaičiumi pagrįsta pora, kurioje įdėtieji iškvietimai sukelia OnBeginUpdate tik pereinant į išorinį iškvietimą ir OnEndUpdate tik grįžtant iš jo. THPDFViewer savo pusėje seka tą patį gylį ir praleidžia RefreshDocument kiekvienam smulkiam įvykiui, kol skaičius didesnis už nulį, tada paketui užsidarius perpiešia tiksliai vieną kartą. Smulkūs įvykiai paketo metu vis tiek sukelia įvykius, todėl prenumeratorius, kuriam rūpi tik OnSearchChange, apie pakeitimą sužino; sutraukiamas tik paties valdiklio perpiešimas, iš keturių iškvietimų paverčiamas vienu
Kaip pažymėjimo vilkimas pelės tempimą grąžina į PDF koordinates?
Pažymėjimo vilkimas pelės tempimą grąžina į PDF koordinates per porą modelio metodų, sukurtų būtent šiam apvaliam transformavimui: PagePointToView ir ViewPointToPage. Abu gauna puslapio indeksą, DPI ir tašką, o transformaciją išsprendžia dviem etapais: pirmiausia paties puslapio /Rotate įrašą ir jo PDF pradžią apatiniame kairiajame kampe, tada atskirą, neardomą rodinio ViewRotation ir viršutinėje kairėje esančią žymeklio įrenginio pradžią, kad atvirkštinė kryptis galėtų griežta atvirkštine tvarka anuliuoti abu etapus ir teisingai grąžinti rezultatą per visas šešiolika puslapio pasukimo ir rodinio pasukimo kombinacijų. THPDFViewer iškviečia ViewPointToPage, kai naudotojas paleidęs pelę baigia tempti stačiakampį vimHighlight sąveikos režimu, abu įrenginio taškus paverčia THPDFRectangle puslapio erdvėje ir perduoda juos Model.AddHighlightRegion. Dar viena svarbi detalė: pelės fiksavimas priklauso TScrollBox paveldėtam peržiūros valdikliui, o ne antriniam TImage, į kurį piešiamas taškinis piešinys, nes TControl.MouseCapture yra apsaugota ir ją gali perimti tik tėvinis valdiklis, todėl tempimas, išeinantis už vaizdo ribų prieš atleidžiant mygtuką, vis tiek apdorojamas per paties peržiūros valdiklio perrašytus MouseMove/MouseUp, o ne tyliai prarandamas antriniame valdiklyje
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;
Ką šis atskyrimas suteikia be žalio testų rinkinio?
Nauda neapsiriboja testų, praeinančių CI užduotyje be darbalaukio seanso, rezultatu. Kadangi THPDFViewer persiunčia į THPDFViewerModel, o ne dubliuoja jo logiką, HotPDF galėjo pridėti trečią naudotoją — THPDFViewerAction ir konkrečius poklasius, tokius kaip THPDFZoomInAction bei THPDFFindNextAction, kurie prijungia naršymą, mastelį, paiešką ir pasukimą prie standartinio Delphi TActionList, todėl įrankių juostos mygtukas arba meniu elementas gali deklaratyviai valdyti peržiūros programą ir automatiškai įsijungti pagal tai, ar šiuo metu nustatytas peržiūros valdiklis kaip veiksmo paskirties objektas. Šiam sluoksniui nereikėjo nieko žinoti apie taškinius piešinius ar GDI: jis iškviečia Viewer.NextPage arba Viewer.Model.FindNext, o esama įvykių grandinė pasirūpina perpiešimu. Kadangi niekur THPDFViewerModel nenurodo TScrollBox, TImage ar lango rankenos, apačioje esanti būsenos mašina taip pat nepririšta prie vieno valdiklio — tas pats modelis galėtų būti už kito atvaizdavimo paviršiaus nekeičiant nė vienos naršymo, mastelio ar paieškos logikos eilutės
Kur atvaizdavimo spartinančioji atmintinė padeda, o kur ne
THPDFViewerModel atvaizdavimo spartinančioji atmintinė padeda įkelto dokumento viduje, tačiau nepakeičia to, kiek iš pradžių kainuoja šio dokumento įkėlimas. CreatePageSnapshot, CreateCurrentPageSnapshot ir išankstinio nuskaitymo metodai PrefetchPageSnapshots/PrefetchCurrentPageSnapshots visi naudoja tą pačią pagal puslapį ir DPI indeksuojamą atvaizdavimo spartinančiąją atmintinę, todėl grįžimas į jau peržiūrėtą puslapį tuo pačiu masteliu yra spartinančiosios atmintinės pataikymas, o ne pakartotinis atvaizdavimas, ir nedidelio gretimų puslapių spindulio išankstinis nuskaitymas sušvelnina įprastą atvejį, kai skaitytojas pereina po vieną puslapį pirmyn. Tačiau visa tai nepaveikia pradinio LoadFromFile iškvietimo kainos, o peržiūros programa, sukurta atverti bet ką, ką naudotojas į ją nuvelka, galiausiai susidurs su pakankamai dideliu failu, kad būtent šis iškvietimas taptų tikruoju stabdžiu. Apie sluoksniuotą, rankena pagrįstą alternatyvą visam įkėlimui — kurią verta žinoti iš anksto — skaitykite gretimame straipsnyje apie Direct File API dideliems PDF
Čia aprašytos Model ir View klasės yra dar dvi tos pačios įkelto dokumento paviršiaus dalys, naudojamos visame HotPDF komponente, skirtame Delphi ir C++Builder, ir jos gali būti valdomos iš formos, iš TActionList arba visai be jų