HotPDF delar upp sin Delphi-PDF-visare i två delar: THPDFViewerModel, en vanlig klass som äger zoom-, rotations-, sök-, markerings- och navigeringstillstånd utan något beroende av ett fönsterhandtag, och THPDFViewer, en TScrollBox-baserad kontroll som omvandlar det tillståndet till pixlar. Uppdelningen är det som gör att visarlogiken kan köras, och testas, utan att någonsin skapa ett formulär
De flesta anpassade visarkontroller ser inte ut så här. Zoomnivån lever i ett privat fält på kontrollen, sidnavigering begränsar sina gränser inuti en knapps OnClick-hanterare, och det enda sättet att veta om Ctrl+scroll respekterar ett zoomtak är att köra appen, klicka, och titta. En kontroll byggd på det sättet fungerar bra tills den behöver en regressionssvit, eller en andra värd — en förhandsgranskningsdialog för utskrift, ett miniatyrfält, en batch-granskare utan något synligt fönster alls — och tillståndet du behöver visar sig vara hopsvetsat med en TWinControl som insisterar på ett riktigt handtag innan den gör någonting
Varför behöver en PDF-visarkontroll en MVC-uppdelning över huvud taget?
En PDF-visare behöver den här typen av uppdelning eftersom dess tillstånd och dess presentation förändras av olika anledningar och i olika takt. Sidindex, zoom, visningsrotation, söktreffar och markeringsregioner är verksamhetstillstånd: de kan beräknas, valideras och serialiseras utan en enda pixel på skärmen. Att rita en bitmapp, fånga musen och rita en markeringsrektangel med "myrmarsch" är presentationsangelägenheter som bara är meningsfulla när en kontroll väl finns. HotPDF håller den första gruppen i THPDFViewerModel, en klass utan något VCL-fönsterhanterande ursprung alls, och den andra gruppen i THPDFViewer, som äger en modellinstans och reagerar på den — närmare ett Model-View-par än en läroboks tre-lagers MVC, eftersom det inte finns någon separat Controller-klass och THPDFViewer själv omvandlar råa tangentbords- och mushändelser till modellanrop. Vad som spelar större roll än etiketten är beroenderiktningen: ingenting i THPDFViewerModel kräver ett Handle, en meddelandeslinga, eller ett synligt skrivbord, vilket är precis det som gör att HotPDF:s egen testsvit kan driva sidbläddring, zoombegränsning, tangentbordskommandon, och koordinat-tur-och-retur genom DUnitX utan att öppna ett fönster
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;
Vad THPDFViewerModel faktiskt äger
THPDFViewerModel äger allt en visare behöver för att svara på vad som för närvarande ska visas på skärmen utan att äga hur det ska ritas. PageIndex, PageNumber, och PageCount spårar position; Zoom och ZoomMode (vzmActualSize, vzmFitPage, vzmFitWidth, vzmCustom) spårar skala; ViewRotation spårar en icke-destruktiv skärmrotation som aldrig rör sidans egen /Rotate-post. Navigeringsmetoder — FirstPage, PriorPage, NextPage, LastPage — och zoommetoder — ZoomIn, ZoomOut, som går igenom en fast tabell med nitton förinställda nivåer från 5% till 6400% — lever här också, tillsammans med FindAll/FindNext/FindPrevious för textsökning och AddHighlightRegion/RemoveHighlightRegion/ClearHighlightRegions för beständiga sidannoteringar en anropare vill behålla mellan renderingar. Modellen äger utdata lika väl som indata: CreateCurrentPageSnapshot och CreateCurrentPageMetafile exporterar exakt den sida som för närvarande visas på skärmen, och PrintCurrentView skickar samma aktuella vy — aktuell sida, aktuell zoom-härledd DPI, aktuell rotation — till en TPrinter, ett smalare, vy-avgränsat jobb än den dokumentövergripande utskriftspipelinen som täcks i HotPDF:s genomgång av TPrinter-utskrift. Varje förändring som spelar roll utlöser också en matchande händelse — OnPageChange, OnZoomChange, OnSearchChange, OnHighlightChange, OnViewRotationChange — så en prenumerant får reda på vad som ändrats utan att behöva fråga om det
Hur vet THPDFViewer när den ska rita om?
THPDFViewer vet när den ska rita om eftersom den prenumererar på modellen i stället för att gissa. THPDFViewers konstruktor skapar en privat THPDFViewerModel, och kopplar sedan var och en av dess aviseringshändelser — OnBeginUpdate, OnEndUpdate, OnHighlightChange, OnPageChange, OnSearchChange, OnViewRotationChange, OnZoomChange — till en matchande privat hanterare. Varje hanterares uppgift är liten: anropa RefreshDocument, metoden som faktiskt rastrerar den aktuella sidan via samma cachade sidrenderare som beskrivs i HotPDF:s interna sida-till-bitmapp-rendering, och komponerar sedan markeringsrutor och söktreffar ovanpå och applicerar den aktuella visningsrotationen. Publicerade egenskaper som PageIndex, Zoom, ZoomMode, och ViewRotation är tunna vidarebefordrare — getter-metoden läser FModel.PageIndex, setter-metoden skriver FModel.PageIndex — så från Object Inspector eller från kod ser kontrollen ut som att den håller tillståndet direkt, även om THPDFViewerModel är den enda platsen där tillståndet faktiskt lever. Anropare är inte heller begränsade till den vidarebefordrade delmängden: THPDFViewer exponerar själva modellen via en skrivskyddad Model: THPDFViewerModel-egenskap, så kod som vill ha FindFormFieldAt eller PrefetchCurrentPageSnapshots — ingen av dem återexponeras av kontrollen — kan nå förbi omslaget och anropa modellen direkt
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 och EndUpdate: stoppar omritningsstormar
BeginUpdate och EndUpdate finns eftersom en enda logisk förändring ofta rör flera delar av tillståndet samtidigt, och att rita om efter varje del skulle vara slösaktigt och visuellt rörigt. Att byta ut det laddade dokumentet är det tydligaste exemplet: att tilldela THPDFViewerModel.Document återställer visningsrotation, rensar söktreffar, rensar markeringsregioner, och hoppar till sida ett, och vart och ett av de stegen utlöser normalt sin egen förändringshändelse. THPDFViewerModel omsluter den sekvensen i BeginUpdate/EndUpdate, ett referensräknat par där nästlade anrop bara utlöser OnBeginUpdate vid övergången in i det yttersta anropet och OnEndUpdate vid övergången tillbaka ut. THPDFViewer spårar samma djup på sin sida och hoppar över RefreshDocument för varje enskild händelse medan räknaren är över noll, och ritar sedan om exakt en gång när batchen stängs. De enskilda händelserna utlöses ändå under batchen, så en prenumerant som bara bryr sig om OnSearchChange hör fortfarande om det; det är bara kontrollens egen omritning som slås samman till ett anrop i stället för fyra
Hur mappar myrmarsch-markering ett musdrag tillbaka till PDF-koordinater?
Myrmarsch-markering mappar ett musdrag tillbaka till PDF-koordinater genom ett par modellmetoder byggda exakt för den tur-och-retur-resan: PagePointToView och ViewPointToPage. Båda tar ett sidindex, en DPI, och en punkt, och båda löser transformationen i två steg — först sidans egen /Rotate-post och dess nedre-vänstra PDF-ursprung, sedan vyns separata, icke-destruktiva ViewRotation och visarens övre-vänstra enhetsursprung — specifikt så att den omvända riktningen kan ångra de två stegen i strikt omvänd ordning och gå tur-och-retur korrekt över alla sexton kombinationer av sidrotation och visningsrotation. THPDFViewer anropar ViewPointToPage när användaren släpper musen efter att ha dragit en rektangel i interaktionsläget vimHighlight, omvandlar de två enhetspunkterna till en THPDFRectangle i sidrymden, och lämnar den till Model.AddHighlightRegion. En detalj värd att känna till om du bygger något liknande: musfångst tillhör den TScrollBox-ärvda visaren, inte den underliggande TImage som bitmappen ritas i, eftersom TControl.MouseCapture är skyddad och bara föräldrakontrollen kan göra anspråk på den — så ett drag som lämnar bildens gränser innan knappen släpps löses fortfarande genom visarens egen överridda MouseMove/MouseUp i stället för att tyst tappas bort av barnkontrollen
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;
Vad uppdelningen ger dig utöver en grön testsvit
Vinsten är inte begränsad till att tester går igenom i ett CI-jobb utan skrivbordssession. Eftersom THPDFViewer vidarebefordrar till THPDFViewerModel i stället för att duplicera dess logik kunde HotPDF lägga till en tredje konsument — THPDFViewerAction och konkreta subklasser som THPDFZoomInAction och THPDFFindNextAction — som kopplar in navigering, zoom, sökning och rotation i en standard-Delphi-TActionList, så att en verktygsfältsknapp eller ett menyval kan styra visaren deklarativt, och aktivera sig själv automatiskt beroende på om en visare för närvarande är löst som åtgärdens mål. Inget av det lagret behövde veta något om bitmappar eller GDI; det anropar Viewer.NextPage eller Viewer.Model.FindNext, och den befintliga händelsekedjan tar hand om omritningen. Och eftersom ingenting i THPDFViewerModel refererar till TScrollBox, TImage, eller ett fönsterhandtag, är tillståndsmaskinen därunder inte heller hopsvetsad med just den kontrollen — samma modell skulle kunna sitta bakom en annan renderingsyta utan att röra en enda rad av navigerings-, zoom- eller sökningslogik
Var rendercachen hjälper, och var den inte gör det
THPDFViewerModels rendercache hjälper inom ett laddat dokument, men den ändrar inte vad det kostar att över huvud taget ladda det dokumentet. CreatePageSnapshot, CreateCurrentPageSnapshot, och förhämtningsmetoderna PrefetchPageSnapshots/PrefetchCurrentPageSnapshots går alla via samma cachade renderare nyckad på sida och DPI, så att bläddra tillbaka till en sida du redan har visat i samma zoomnivå är en cache-träff snarare än en omrendering, och att förhämta en liten radie av intilliggande sidor jämnar ut det vanliga fallet där en läsare bläddrar framåt en sida i taget. Inget av detta rör dock kostnaden för det initiala LoadFromFile-anropet, och en visare byggd för att öppna vad en användare än drar till den möter så småningom en fil stor nog att göra det anropet till den faktiska flaskhalsen. För det stegvisa, handtagsbaserade alternativet till en fullständig inläsning — värt att känna till innan den dagen kommer — se följdartikeln om Direct File API för stora PDF:er
Model- och View-klasserna som beskrivs här är två ytterligare delar av samma laddade-dokument-yta som används genomgående i HotPDF-komponenten för Delphi och C++Builder, byggd för att styras från ett formulär, från en TActionList, eller från ingetdera alls