HotPDF splitst zijn Delphi-PDF-viewer op in twee delen: THPDFViewerModel, een gewone klasse die zoom-, rotatie-, zoek-, markerings- en navigatiestatus beheert zonder afhankelijkheid van een vensterhandle, en THPDFViewer, een op TScrollBox gebaseerd besturingselement dat die status omzet in pixels. Deze splitsing is wat viewerlogica laat draaien, en laat testen, zonder ooit een formulier aan te maken
De meeste aangepaste viewerbesturingselementen zien er niet zo uit. Het zoomniveau bevindt zich in een private veld op het besturingselement, paginanavigatie begrenst zijn grenzen binnen de OnClick-handler van een knop, en de enige manier om te weten of Ctrl+scroll een zoomplafond respecteert, is de app draaien, klikken, en kijken. Een op die manier gebouwd besturingselement werkt prima totdat het een regressietestsuite nodig heeft, of een tweede host — een afdrukvoorbeelddialoog, een miniatuurbalk, een batchbeoordelaar zonder enig zichtbaar venster — en dan blijkt de status die u nodig heeft vastgelast te zitten aan een TWinControl die op een echte handle staat voordat deze iets wil doen
Waarom heeft een PDF-viewerbesturingselement überhaupt een MVC-splitsing nodig?
Een PDF-viewer heeft dit soort splitsing nodig omdat de status en de presentatie ervan om verschillende redenen en met verschillende snelheden veranderen. Pagina-index, zoom, weergaverotatie, zoektreffers en markeringsregio's zijn bedrijfsstatus: ze kunnen worden berekend, gevalideerd en geserialiseerd zonder één pixel op het scherm. Een bitmap tekenen, de muis vastleggen, en een markeringsrechthoek voor selectie tekenen zijn presentatiekwesties die alleen zinvol zijn zodra er een besturingselement bestaat. HotPDF houdt de eerste groep in THPDFViewerModel, een klasse zonder enige VCL-vensterafstammeling, en de tweede groep in THPDFViewer, dat een modelinstantie bezit en erop reageert — dichter bij een Model-View-paar dan een klassieke driedelige MVC uit een leerboek, aangezien er geen aparte Controller-klasse is en THPDFViewer zelf ruwe toetsenbord- en muisgebeurtenissen omzet in modelaanroepen. Belangrijker dan het label is de afhankelijkheidsrichting: niets in THPDFViewerModel vereist een Handle, een berichtenlus of een zichtbaar bureaublad, en dat is precies wat HotPDF's eigen testsuite in staat stelt om paginering, zoombegrenzing, toetsenbordcommando's en coördinaat-heen-en-terugreizen via DUnitX aan te sturen zonder een venster te openen
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;
Wat THPDFViewerModel daadwerkelijk beheert
THPDFViewerModel beheert alles wat een viewer nodig heeft om te bepalen wat er momenteel op het scherm zou moeten staan, zonder te bezitten hoe het getekend moet worden. PageIndex, PageNumber, en PageCount volgen de positie; Zoom en ZoomMode (vzmActualSize, vzmFitPage, vzmFitWidth, vzmCustom) volgen de schaal; ViewRotation volgt een niet-destructieve rotatie op het scherm die nooit de eigen /Rotate-vermelding van de pagina aanraakt. Navigatiemethoden — FirstPage, PriorPage, NextPage, LastPage — en zoommethoden — ZoomIn, ZoomOut, die door een vaste tabel van negentien vooraf ingestelde niveaus lopen van 5% tot 6400% — bevinden zich hier ook, naast FindAll/FindNext/FindPrevious voor tekstzoeken en AddHighlightRegion/RemoveHighlightRegion/ClearHighlightRegions voor permanente paginaannotaties die een aanroeper tussen renders wil behouden. Het model beheert zowel uitvoer als invoer: CreateCurrentPageSnapshot en CreateCurrentPageMetafile exporteren precies de pagina die momenteel op het scherm staat, en PrintCurrentView stuurt diezelfde huidige weergave — huidige pagina, huidige uit de zoom afgeleide DPI, huidige rotatie — naar een TPrinter, een smallere, viewgebonden taak dan de documentbrede afdrukpijplijn die wordt behandeld in HotPDF's TPrinter-afdrukhandleiding. Elke mutatie die ertoe doet, activeert ook een bijbehorende gebeurtenis — OnPageChange, OnZoomChange, OnSearchChange, OnHighlightChange, OnViewRotationChange — zodat een abonnee te weten komt wat er is veranderd zonder te hoeven pollen
Hoe weet THPDFViewer wanneer het opnieuw moet tekenen?
THPDFViewer weet wanneer het opnieuw moet tekenen omdat het zich abonneert op het model in plaats van te gokken. De constructor van THPDFViewer maakt een private THPDFViewerModel aan, en koppelt vervolgens elk van de meldingsgebeurtenissen — OnBeginUpdate, OnEndUpdate, OnHighlightChange, OnPageChange, OnSearchChange, OnViewRotationChange, OnZoomChange — aan een bijbehorende private handler. De taak van elke handler is klein: roep RefreshDocument aan, de methode die de huidige pagina daadwerkelijk rasteriseert via dezelfde gecachte paginarenderer die wordt beschreven in HotPDF's interne werking van pagina-naar-bitmap-rendering, en composeert vervolgens markeringsvakken en zoektreffers erbovenop en past de huidige weergaverotatie toe. Gepubliceerde eigenschappen zoals PageIndex, Zoom, ZoomMode, en ViewRotation zijn dunne doorverwijzers — de getter leest FModel.PageIndex, de setter schrijft FModel.PageIndex — dus vanuit de Object Inspector of vanuit code lijkt het besturingselement de status rechtstreeks te bezitten, ook al is THPDFViewerModel de enige plek waar die status daadwerkelijk leeft. Aanroepers zijn ook niet beperkt tot de doorverwezen subset: THPDFViewer ontsluit het model zelf via een alleen-lezen Model: THPDFViewerModel-eigenschap, zodat code die FindFormFieldAt of PrefetchCurrentPageSnapshots wil — geen van beide wordt door het besturingselement opnieuw ontsloten — voorbij de wrapper kan reiken en het model rechtstreeks kan aanroepen
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 en EndUpdate: hertekenstormen stoppen
BeginUpdate en EndUpdate bestaan omdat één logische wijziging vaak meerdere delen van de status tegelijk raakt, en opnieuw tekenen na elk deel zou verspillend en visueel storend zijn. Het wisselen van het geladen document is het duidelijkste voorbeeld: het toewijzen van THPDFViewerModel.Document reset de weergaverotatie, wist zoektreffers, wist markeringsregio's, en springt naar pagina één, en elk van die stappen activeert normaal gesproken zijn eigen wijzigingsgebeurtenis. THPDFViewerModel wikkelt die reeks in BeginUpdate/EndUpdate, een paar met referentietelling waarbij geneste aanroepen alleen OnBeginUpdate activeren bij de overgang naar de buitenste aanroep en OnEndUpdate bij de overgang terug daaruit. THPDFViewer volgt diezelfde diepte aan zijn kant en slaat RefreshDocument over voor elke granulaire gebeurtenis zolang de teller boven nul staat, en tekent dan precies één keer opnieuw wanneer de batch wordt afgesloten. De granulaire gebeurtenissen worden nog steeds tijdens de batch geactiveerd, dus een abonnee die alleen om OnSearchChange geeft, hoort er nog steeds van; alleen het eigen hertekenen van het besturingselement wordt teruggebracht tot één aanroep in plaats van vier
Hoe koppelt marquee-markering een muissleep terug naar PDF-coördinaten?
Marquee-markering koppelt een muissleep terug naar PDF-coördinaten via een paar modelmethoden die precies voor die heen-en-terugreis zijn gebouwd: PagePointToView en ViewPointToPage. Beide nemen een pagina-index, een DPI, en een punt, en beide lossen de transformatie in twee fasen op — eerst de eigen /Rotate-vermelding van de pagina en zijn linksonder-PDF-oorsprong, dan de aparte, niet-destructieve ViewRotation van de weergave en de linksboven-apparaatoorsprong van de viewer — specifiek zodat de omgekeerde richting de twee fasen in strikt omgekeerde volgorde ongedaan kan maken en correct heen en terug kan reizen over alle zestien combinaties van paginarotatie en weergaverotatie. THPDFViewer roept ViewPointToPage aan wanneer de gebruiker de muis loslaat na het slepen van een rechthoek in de vimHighlight-interactiemodus, zet de twee apparaatpunten om in een THPDFRectangle in paginaruimte, en geeft dit door aan Model.AddHighlightRegion. Eén detail dat de moeite waard is om te kennen als u iets vergelijkbaars bouwt: muisvastlegging behoort toe aan de van TScrollBox afstammende viewer, niet aan de onderliggende TImage waarin de bitmap wordt getekend, omdat TControl.MouseCapture beschermd is en alleen het bovenliggende besturingselement er aanspraak op kan maken — dus een sleepbeweging die de grenzen van de afbeelding verlaat voordat de knop wordt losgelaten, wordt nog steeds opgelost via de eigen overschreven MouseMove/MouseUp van de viewer in plaats van stilzwijgend te worden genegeerd door het onderliggende besturingselement
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;
Wat de splitsing oplevert naast een groene testsuite
De opbrengst is niet beperkt tot slagende tests in een CI-job zonder desktopsessie. Omdat THPDFViewer doorverwijst naar THPDFViewerModel in plaats van de logica ervan te dupliceren, kon HotPDF een derde consument toevoegen — THPDFViewerAction en concrete subklassen zoals THPDFZoomInAction en THPDFFindNextAction — die navigatie, zoom, zoeken en rotatie inpluggen op een standaard Delphi-TActionList, zodat een werkbalkknop of een menu-item de viewer declaratief kan aansturen, waarbij hijzelf automatisch wordt in- of uitgeschakeld op basis van of een viewer op dat moment als doel van de actie is opgelost. Niets van die laag hoefde iets te weten over bitmaps of GDI; het roept Viewer.NextPage of Viewer.Model.FindNext aan, en de bestaande gebeurtenisketen zorgt voor het hertekenen. En omdat niets in THPDFViewerModel verwijst naar TScrollBox, TImage, of een vensterhandle, zit de onderliggende toestandsmachine ook niet vastgelast aan dat ene besturingselement — hetzelfde model zou achter een ander renderingoppervlak kunnen zitten zonder een regel navigatie-, zoom- of zoeklogica aan te raken
Waar de rendercache helpt, en waar niet
De rendercache van THPDFViewerModel helpt binnen een geladen document, maar verandert niet wat het laden van dat document in de eerste plaats kost. CreatePageSnapshot, CreateCurrentPageSnapshot, en de prefetch-methoden PrefetchPageSnapshots/PrefetchCurrentPageSnapshots lopen allemaal via dezelfde gecachte renderer die is gesleuteld op pagina en DPI, dus terugbladeren naar een pagina die u al op hetzelfde zoomniveau heeft bekeken, is een cachetreffer in plaats van een nieuwe render, en het vooraf ophalen van een kleine straal aangrenzende pagina's verzacht het gangbare geval van een lezer die één pagina tegelijk vooruit bladert. Niets daarvan raakt echter de kosten van de initiële LoadFromFile-aanroep, en een viewer die is gebouwd om te openen wat een gebruiker er ook op sleept, komt uiteindelijk een bestand tegen dat groot genoeg is om die aanroep de daadwerkelijke bottleneck te maken. Voor het gelaagde, handle-gebaseerde alternatief voor een volledig laden — de moeite waard om te kennen voordat die dag aanbreekt — zie het bijbehorende artikel over de Direct File API voor grote PDF's
De hier beschreven Model- en View-klassen zijn twee verdere onderdelen van hetzelfde oppervlak voor geladen documenten dat overal in de HotPDF-component voor Delphi en C++Builder wordt gebruikt, gebouwd om aangestuurd te worden vanuit een formulier, vanuit een TActionList, of vanuit geen van beide