HotPDF își separă vizualizatorul PDF Delphi în două piese: THPDFViewerModel, o clasă simplă care deține starea de zoom, rotație, căutare, evidențiere și navigare fără nicio dependență de handle de fereastră, și THPDFViewer, un control bazat pe TScrollBox care transformă acea stare în pixeli. Această separare este ceea ce permite logicii vizualizatorului să ruleze, și să fie testată, fără a crea vreodată un formular
Majoritatea controalelor de vizualizator personalizate nu arată așa. Nivelul de zoom trăiește într-un câmp privat al controlului, navigarea între pagini își limitează valorile în interiorul unui handler OnClick al unui buton, iar singura modalitate de a ști dacă Ctrl+derulare respectă un plafon de zoom este să rulați aplicația, să faceți clic și să priviți. Un control construit așa funcționează bine până când are nevoie de o suită de regresie, sau de un al doilea host — un dialog de previzualizare tipărire, o bandă de miniaturi, un revizor de loturi fără nicio fereastră vizibilă deloc — iar starea de care aveți nevoie se dovedește a fi sudată de un TWinControl care insistă asupra unui handle real înainte de a face orice
De ce are nevoie un control de vizualizator PDF de o separare MVC?
Un vizualizator PDF are nevoie de acest tip de separare pentru că starea și prezentarea sa se schimbă din motive diferite și în ritmuri diferite. Indexul de pagină, zoom-ul, rotația de vizualizare, rezultatele căutării și regiunile de evidențiere sunt stare de business: pot fi calculate, validate și serializate fără un singur pixel pe ecran. Desenarea unui bitmap, capturarea mouse-ului și trasarea unui dreptunghi de selecție marquee sunt aspecte de prezentare care au sens doar odată ce există un control. HotPDF păstrează primul grup în THPDFViewerModel, o clasă fără niciun strămoș de ferestruire VCL, și al doilea grup în THPDFViewer, care deține o instanță de model și reacționează la ea — mai aproape de o pereche Model-View decât de un MVC clasic pe trei niveluri, întrucât nu există o clasă Controller separată, iar THPDFViewer însuși transformă evenimentele brute de tastatură și mouse în apeluri de model. Ceea ce contează mai mult decât eticheta este direcția dependenței: nimic din THPDFViewerModel nu necesită un Handle, o buclă de mesaje sau un desktop vizibil, ceea ce este exact ceea ce permite propriei suite de teste a HotPDF să conducă paginarea, limitarea zoom-ului, comenzile de tastatură și conversiile bidirecționale de coordonate prin DUnitX fără a deschide o fereastră
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;
Ce deține de fapt THPDFViewerModel
THPDFViewerModel deține tot ce are nevoie un vizualizator pentru a răspunde la ce ar trebui să fie pe ecran în prezent, fără a deține modul cum se desenează asta. PageIndex, PageNumber și PageCount urmăresc poziția; Zoom și ZoomMode (vzmActualSize, vzmFitPage, vzmFitWidth, vzmCustom) urmăresc scara; ViewRotation urmărește o rotație pe ecran non-distructivă, care nu atinge niciodată intrarea proprie /Rotate a paginii. Metodele de navigare — FirstPage, PriorPage, NextPage, LastPage — și metodele de zoom — ZoomIn, ZoomOut, parcurgând un tabel fix de nouăsprezece niveluri predefinite de la 5% la 6400% — trăiesc și ele aici, alături de FindAll/FindNext/FindPrevious pentru căutarea de text și AddHighlightRegion/RemoveHighlightRegion/ClearHighlightRegions pentru adnotări persistente de pagină pe care un apelant dorește să le păstreze între redări. Modelul deține atât ieșirea, cât și intrarea: CreateCurrentPageSnapshot și CreateCurrentPageMetafile exportă exact pagina aflată curent pe ecran, iar PrintCurrentView trimite acea aceeași vizualizare curentă — pagina curentă, DPI-ul derivat din zoom-ul curent, rotația curentă — către un TPrinter, o sarcină mai restrânsă, la nivel de vizualizare, decât pipeline-ul de tipărire la nivel de document acoperit în ghidul HotPDF de tipărire cu TPrinter. Fiecare mutație care contează ridică de asemenea un eveniment corespunzător — OnPageChange, OnZoomChange, OnSearchChange, OnHighlightChange, OnViewRotationChange — astfel încât un abonat află ce s-a schimbat fără interogare periodică (polling)
Cum știe THPDFViewer când să se redeseneze?
THPDFViewer știe când să se redeseneze pentru că se abonează la model, în loc să ghicească. Constructorul lui THPDFViewer creează un THPDFViewerModel privat, apoi conectează fiecare dintre evenimentele sale de notificare — OnBeginUpdate, OnEndUpdate, OnHighlightChange, OnPageChange, OnSearchChange, OnViewRotationChange, OnZoomChange — la un handler privat corespunzător. Sarcina fiecărui handler este mică: apelează RefreshDocument, metoda care rasterizează efectiv pagina curentă prin același renderer de pagină cu cache descris în detaliile interne de randare pagină-la-bitmap ale HotPDF, apoi compune deasupra casetele de evidențiere și rezultatele căutării și aplică rotația de vizualizare curentă. Proprietățile publicate precum PageIndex, Zoom, ZoomMode și ViewRotation sunt simpli redirecționatori — getter-ul citește FModel.PageIndex, setter-ul scrie FModel.PageIndex — astfel încât din Object Inspector sau din cod, controlul pare că deține direct starea, deși THPDFViewerModel este singurul loc unde acea stare chiar există. Apelanții nu sunt limitați nici la subsetul redirecționat: THPDFViewer expune modelul însuși printr-o proprietate Model: THPDFViewerModel doar-citire, astfel încât codul care dorește FindFormFieldAt sau PrefetchCurrentPageSnapshots — niciuna dintre acestea nefiind reexpusă de control — poate ajunge dincolo de wrapper și apela modelul direct
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: oprirea furtunilor de redesenare
BeginUpdate și EndUpdate există pentru că o singură schimbare logică atinge adesea mai multe bucăți de stare deodată, iar redesenarea după fiecare bucată ar fi risipitoare și zgomotoasă vizual. Înlocuirea documentului încărcat este cel mai clar exemplu: atribuirea THPDFViewerModel.Document resetează rotația de vizualizare, șterge rezultatele căutării, șterge regiunile de evidențiere și sare la pagina unu, iar fiecare dintre aceste etape declanșează în mod normal propriul eveniment de schimbare. THPDFViewerModel învelește acea secvență în BeginUpdate/EndUpdate, o pereche cu numărare de referințe unde apelurile imbricate declanșează OnBeginUpdate doar la tranziția către cel mai exterior apel și OnEndUpdate doar la tranziția înapoi în afară. THPDFViewer urmărește aceeași adâncime pe partea sa și omite RefreshDocument pentru fiecare eveniment granular cât timp numărul este peste zero, apoi se redesenează exact o singură dată când lotul se închide. Evenimentele granulare tot se declanșează în timpul lotului, așa că un abonat care se interesează doar de OnSearchChange tot află despre asta; doar redesenarea proprie a controlului este cea care se colapsează la un singur apel în loc de patru
Cum mapează evidențierea marquee o tragere de mouse înapoi în coordonate PDF?
Evidențierea marquee mapează o tragere de mouse înapoi în coordonate PDF printr-o pereche de metode ale modelului construite exact pentru acest drum dus-întors: PagePointToView și ViewPointToPage. Ambele preiau un index de pagină, un DPI și un punct, iar ambele rezolvă transformarea în două etape — mai întâi propria intrare /Rotate a paginii și originea sa PDF din colțul stânga-jos, apoi ViewRotation-ul separat, non-distructiv al vizualizării și originea de dispozitiv din colțul stânga-sus al vizualizatorului — special pentru ca direcția inversă să poată anula cele două etape în ordine strict inversă și să facă drumul dus-întors corect pe toate cele șaisprezece combinații de rotație de pagină și rotație de vizualizare. THPDFViewer apelează ViewPointToPage când utilizatorul eliberează mouse-ul după ce a tras un dreptunghi în modul de interacțiune vimHighlight, transformă cele două puncte de dispozitiv într-un THPDFRectangle în spațiul paginii și îl predă lui Model.AddHighlightRegion. Un detaliu care merită cunoscut dacă construiți ceva similar: capturarea mouse-ului aparține vizualizatorului descendent din TScrollBox, nu TImage-ului copil în care este desenat bitmap-ul, pentru că TControl.MouseCapture este protejat și doar controlul părinte îl poate revendica — astfel încât o tragere care iese din limitele imaginii înainte ca butonul să fie eliberat tot se rezolvă prin propriile MouseMove/MouseUp suprascrise ale vizualizatorului, în loc să fie eliminată silențios de controlul copil
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;
Ce vă aduce în plus această separare, dincolo de o suită de teste verde
Beneficiul nu se limitează la teste care trec într-un job CI fără sesiune desktop. Pentru că THPDFViewer redirecționează către THPDFViewerModel în loc să-i dubleze logica, HotPDF a putut adăuga un al treilea consumator — THPDFViewerAction și subclase concrete precum THPDFZoomInAction și THPDFFindNextAction — care conectează navigarea, zoom-ul, căutarea și rotația la un TActionList Delphi standard, astfel încât un buton de bară de instrumente sau un element de meniu poate conduce vizualizatorul declarativ, activându-se automat în funcție de dacă un vizualizator este rezolvat în prezent ca țintă a acțiunii. Niciunul din acel strat nu a trebuit să știe nimic despre bitmap-uri sau GDI; apelează Viewer.NextPage sau Viewer.Model.FindNext, iar lanțul existent de evenimente se ocupă de redesenare. Și pentru că nimic din THPDFViewerModel nu referă TScrollBox, TImage sau un handle de fereastră, mașina de stare de dedesubt nu este nici ea sudată de acel unic control — același model ar putea sta în spatele unei suprafețe de randare diferite fără a atinge o singură linie de logică de navigare, zoom sau căutare
Unde ajută cache-ul de randare și unde nu ajută
Cache-ul de randare al THPDFViewerModel ajută în interiorul unui document încărcat, dar nu schimbă cât costă încărcarea acelui document în primul rând. CreatePageSnapshot, CreateCurrentPageSnapshot și metodele de prefetch PrefetchPageSnapshots/PrefetchCurrentPageSnapshots trec toate prin același renderer cu cache indexat după pagină și DPI, astfel încât întoarcerea la o pagină deja vizualizată la același nivel de zoom este o lovitură de cache (cache hit), nu o re-randare, iar preîncărcarea unei mici raze de pagini vecine netezește cazul comun al unui cititor care paginează înainte câte o pagină odată. Totuși, nimic din toate acestea nu atinge costul apelului inițial LoadFromFile, iar un vizualizator construit pentru a deschide orice trage un utilizator peste el întâlnește în cele din urmă un fișier suficient de mare încât să facă din acel apel adevăratul blocaj. Pentru alternativa pe niveluri, bazată pe handle, la o încărcare completă — care merită cunoscută înainte să vină acea zi — consultați articolul complementar despre Direct File API pentru PDF-uri mari
Clasele Model și View descrise aici sunt încă două piese ale aceleiași suprafețe de document încărcat folosite pe tot parcursul componentei HotPDF pentru Delphi și C++Builder, construite pentru a fi conduse dintr-un formular, dintr-un TActionList, sau din niciunul dintre acestea