HotPDF rozdeľuje svoj prehliadač PDF pre Delphi na dve časti: THPDFViewerModel, obyčajnú triedu, ktorá drží stav priblíženia, otočenia, vyhľadávania, zvýraznenia a navigácie bez akejkoľvek závislosti na handli okna, a THPDFViewer, ovládací prvok založený na TScrollBox, ktorý tento stav premieňa na pixely. Práve toto rozdelenie umožňuje, aby logika prehliadača bežala — a dala sa testovať — bez toho, aby sa vôbec vytvoril formulár
Väčšina vlastných ovládacích prvkov prehliadača takto nevyzerá. Úroveň priblíženia býva v súkromnom poli ovládacieho prvku, navigácia medzi stránkami orezáva svoje hranice priamo vnútri obslužnej rutiny OnClick tlačidla a jediný spôsob, ako zistiť, či Ctrl+koliesko rešpektuje strop priblíženia, je spustiť aplikáciu, kliknúť a pozrieť sa. Ovládací prvok postavený takýmto spôsobom funguje bez problémov, kým nepotrebuje regresnú testovaciu sadu alebo druhého hostiteľa — dialóg s náhľadom tlače, panel s miniatúrami, dávkový prehliadač bez akéhokoľvek viditeľného okna — a stav, ktorý potrebujete, sa nakoniec ukáže byť pevne priletovaný k TWinControl, ktorý trvá na skutočnom handli skôr, než čokoľvek urobí
Prečo vôbec ovládací prvok prehliadača PDF potrebuje rozdelenie MVC?
Prehliadač PDF potrebuje tento typ rozdelenia preto, lebo jeho stav a jeho prezentácia sa menia z rôznych dôvodov a rôznou rýchlosťou. Index stránky, priblíženie, otočenie pohľadu, výsledky vyhľadávania a oblasti zvýraznenia sú obchodný stav: dajú sa vypočítať, validovať a serializovať bez jediného pixelu na obrazovke. Vykreslenie bitmapy, zachytenie myši a kreslenie výberového obdĺžnika sú prezentačné záležitosti, ktoré dávajú zmysel iba vtedy, keď už existuje ovládací prvok. HotPDF drží prvú skupinu v THPDFViewerModel, triede bez akéhokoľvek predka z okienkového VCL, a druhú skupinu v THPDFViewer, ktorý vlastní inštanciu modelu a reaguje na ňu — bližšie k dvojici Model-View než k učebnicovému trojvrstvovému MVC, keďže neexistuje samostatná trieda Controller a samotný THPDFViewer premieňa surové udalosti klávesnice a myši na volania modelu. Dôležitejší než samotné pomenovanie je smer závislosti: nič na THPDFViewerModel nevyžaduje Handle, slučku správ ani viditeľnú plochu — a práve to umožňuje vlastnej testovacej sade HotPDF riadiť stránkovanie, orezávanie priblíženia, klávesové príkazy a obojsmerné prevody súradníc cez DUnitX bez otvorenia okna
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;
Čo presne vlastní THPDFViewerModel
THPDFViewerModel vlastní všetko, čo prehliadač potrebuje na to, aby vedel odpovedať, čo by malo byť práve teraz na obrazovke, bez toho, aby vlastnil aj to, ako sa to má nakresliť. PageIndex, PageNumber a PageCount sledujú polohu; Zoom a ZoomMode (vzmActualSize, vzmFitPage, vzmFitWidth, vzmCustom) sledujú mierku; ViewRotation sleduje nedeštruktívne otočenie na obrazovke, ktoré sa nikdy nedotkne vlastnej položky stránky /Rotate. Navigačné metódy — FirstPage, PriorPage, NextPage, LastPage — a metódy priblíženia — ZoomIn, ZoomOut, ktoré prechádzajú pevnou tabuľkou devätnástich prednastavených úrovní od 5 % do 6400 % — žijú tiež tu, spolu s FindAll/FindNext/FindPrevious na vyhľadávanie textu a AddHighlightRegion/RemoveHighlightRegion/ClearHighlightRegions na trvalé anotácie stránky, ktoré si volajúci chce ponechať medzi jednotlivými vykresleniami. Model vlastní rovnako výstup ako vstup: CreateCurrentPageSnapshot a CreateCurrentPageMetafile exportujú presne tú stránku, ktorá je práve na obrazovke, a PrintCurrentView odosiela ten istý aktuálny pohľad — aktuálnu stránku, aktuálne DPI odvodené z priblíženia, aktuálne otočenie — do TPrinter, čo je užšia, na pohľad viazaná úloha než celodokumentová tlačová pipeline opísaná v sprievodcovi HotPDF pre tlač cez TPrinter. Každá mutácia, na ktorej záleží, vyvolá aj zodpovedajúcu udalosť — OnPageChange, OnZoomChange, OnSearchChange, OnHighlightChange, OnViewRotationChange — takže odberateľ sa dozvie, čo sa zmenilo, bez toho, aby musel stav dookola kontrolovať
Ako THPDFViewer vie, kedy má prekresliť?
THPDFViewer vie, kedy má prekresliť, pretože sa odoberá na model namiesto toho, aby hádal. Konštruktor THPDFViewer vytvorí súkromný THPDFViewerModel a potom prepojí každú z jeho notifikačných udalostí — OnBeginUpdate, OnEndUpdate, OnHighlightChange, OnPageChange, OnSearchChange, OnViewRotationChange, OnZoomChange — s príslušnou súkromnou obslužnou rutinou. Úloha každej obslužnej rutiny je malá: zavolať RefreshDocument, metódu, ktorá skutočne rasterizuje aktuálnu stránku cez rovnaký vyrovnávacou pamäťou podložený vykresľovač stránok opísaný v vnútornej implementácii vykresľovania stránky HotPDF do bitmapy, potom navrch skomponuje rámčeky zvýraznenia a výsledky vyhľadávania a aplikuje aktuálne otočenie pohľadu. Publikované vlastnosti ako PageIndex, Zoom, ZoomMode a ViewRotation sú tenké presmerovania — getter číta FModel.PageIndex, setter zapisuje do FModel.PageIndex — takže z Object Inspectora alebo z kódu to vyzerá, akoby ovládací prvok stav vlastnil priamo, hoci jediné miesto, kde tento stav skutočne žije, je THPDFViewerModel. Volajúci navyše nie sú obmedzení len na presmerovanú podmnožinu: THPDFViewer sprístupňuje samotný model cez vlastnosť Model: THPDFViewerModel len na čítanie, takže kód, ktorý chce FindFormFieldAt alebo PrefetchCurrentPageSnapshots — ani jedno z toho ovládací prvok znova nesprístupňuje — môže siahnuť za obálku a zavolať model priamo
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 a EndUpdate: zastavenie búrky prekresľovaní
BeginUpdate a EndUpdate existujú preto, lebo jedna logická zmena sa často dotkne viacerých kúskov stavu naraz, a prekresľovať po každom jednotlivom kúsku by bolo zbytočné a vizuálne rušivé. Výmena načítaného dokumentu je najjasnejší príklad: priradenie do THPDFViewerModel.Document resetuje otočenie pohľadu, vymaže výsledky vyhľadávania, vymaže oblasti zvýraznenia a skočí na prvú stránku, a každý z týchto krokov bežne vyvolá vlastnú udalosť zmeny. THPDFViewerModel obalí túto sekvenciu do BeginUpdate/EndUpdate, dvojice s počítadlom referencií, kde vnorené volania vyvolajú OnBeginUpdate iba pri prechode do najvonkajšieho volania a OnEndUpdate iba pri prechode späť von. THPDFViewer sleduje rovnakú hĺbku na svojej strane a preskočí RefreshDocument pre každú čiastkovú udalosť, kým je počítadlo nad nulou, a prekreslí presne raz, keď sa dávka uzavrie. Čiastkové udalosti sa počas dávky stále vyvolávajú, takže odberateľ, ktorému záleží iba na OnSearchChange, sa o nej stále dozvie; zredukuje sa iba vlastné prekreslenie ovládacieho prvku na jedno volanie namiesto štyroch
Ako mapovanie výberového zvýraznenia prevádza ťahanie myšou späť na súradnice PDF?
Výberové zvýraznenie mapuje ťahanie myšou späť na súradnice PDF cez dvojicu metód modelu postavených presne na tento obojsmerný prevod: PagePointToView a ViewPointToPage. Obe berú index stránky, DPI a bod a obe riešia transformáciu v dvoch krokoch — najprv vlastnú položku /Rotate stránky a jej pôvod PDF vľavo dole, potom samostatné, nedeštruktívne ViewRotation pohľadu a pôvod zariadenia prehliadača vľavo hore — konkrétne preto, aby opačný smer dokázal zrušiť oba kroky v presne opačnom poradí a správne sa obojsmerne prevádzať naprieč všetkými šestnástimi kombináciami otočenia stránky a otočenia pohľadu. THPDFViewer volá ViewPointToPage, keď používateľ uvoľní tlačidlo myši po ťahaní obdĺžnika v interakčnom režime vimHighlight, prevedie oba body zariadenia na THPDFRectangle v priestore stránky a odovzdá ho Model.AddHighlightRegion. Jeden detail sa oplatí poznať, ak staviate niečo podobné: zachytenie myši patrí prehliadaču odvodenému od TScrollBox, nie podriadenému TImage, do ktorého sa maľuje bitmapa, pretože TControl.MouseCapture je chránená a nárokovať si ju môže iba nadradený ovládací prvok — takže ťahanie, ktoré opustí hranice obrázka skôr, než sa tlačidlo pustí, sa aj tak vyrieši cez vlastné pretažené MouseMove/MouseUp prehliadača namiesto toho, aby ho ticho zahodil podriadený ovládací prvok
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;
Čo toto rozdelenie prináša navyše k zelenej testovacej sade
Prínos sa neobmedzuje iba na testy prechádzajúce v úlohe CI bez desktopovej relácie. Pretože THPDFViewer presmerováva na THPDFViewerModel namiesto toho, aby jeho logiku duplikoval, HotPDF dokázal pridať tretieho konzumenta — THPDFViewerAction a konkrétne potomky ako THPDFZoomInAction a THPDFFindNextAction — ktoré zapájajú navigáciu, priblíženie, vyhľadávanie a otočenie do štandardného TActionList v Delphi, takže tlačidlo na paneli nástrojov alebo položka menu môže prehliadač riadiť deklaratívne a sama sa automaticky povoľuje na základe toho, či je aktuálne ako cieľ akcie vyriešený nejaký prehliadač. Táto vrstva vôbec nemusela vedieť nič o bitmapách či GDI; volá Viewer.NextPage alebo Viewer.Model.FindNext a o prekreslenie sa postará existujúci reťazec udalostí. A pretože nič v THPDFViewerModel neodkazuje na TScrollBox, TImage ani handle okna, ani stavový automat pod tým nie je pevne priletovaný k tomuto jedinému ovládaciemu prvku — ten istý model by mohol stáť za odlišnou vykresľovacou plochou bez toho, aby sa dotkol jediného riadku logiky navigácie, priblíženia či vyhľadávania
Kde pomáha vyrovnávacia pamäť vykreslenia a kde nie
Vyrovnávacia pamäť vykreslenia v THPDFViewerModel pomáha vnútri už načítaného dokumentu, no nemení, čo stojí samotné načítanie tohto dokumentu na začiatku. CreatePageSnapshot, CreateCurrentPageSnapshot a metódy predbežného načítania PrefetchPageSnapshots/PrefetchCurrentPageSnapshots všetky prechádzajú cez rovnaký vykresľovač s vyrovnávacou pamäťou kľúčovaný podľa stránky a DPI, takže návrat na stránku, ktorú ste už videli pri rovnakej úrovni priblíženia, je zásah do vyrovnávacej pamäte, nie nové vykreslenie, a predbežné načítanie malého okruhu susedných stránok zahladí bežný prípad čitateľa, ktorý stránkuje dopredu po jednej stránke. Nič z toho sa však nedotýka nákladov na počiatočné volanie LoadFromFile, a prehliadač postavený tak, aby otvoril čokoľvek, čo naň používateľ pretiahne, sa nakoniec stretne so súborom dosť veľkým na to, aby sa práve toto volanie stalo skutočným úzkym hrdlom. Pre stupňovanú alternatívu založenú na handli namiesto plného načítania — oplatí sa poznať ju ešte pred týmto dňom — pozrite si sprievodný článok o Direct File API pre veľké PDF
Triedy Model a View opísané tu sú ďalšími dvoma kúskami tej istej plochy načítaného dokumentu, ktorá sa používa naprieč celým komponentom HotPDF pre Delphi a C++Builder, postaveným tak, aby sa dal ovládať z formulára, z TActionList, alebo z ničoho z toho