HotPDF rozděluje svůj prohlížeč PDF pro Delphi na dvě části: THPDFViewerModel, obyčejnou třídu, která drží stav zoomu, otočení, hledání, zvýraznění a navigace bez jakékoli závislosti na handle okna, a THPDFViewer, ovládací prvek založený na TScrollBox, který tento stav proměňuje v pixely. Právě toto rozdělení umožňuje, aby logika prohlížeče běžela — a dala se testovat — aniž by se kdy vytvořil formulář
Většina vlastních ovládacích prvků prohlížeče takhle nevypadá. Úroveň zoomu žije v soukromém poli ovládacího prvku, navigace stránek si ořezává meze uvnitř obsluhy OnClick tlačítka, a jediný způsob, jak zjistit, zda Ctrl+kolečko respektuje strop zoomu, je aplikaci spustit, kliknout a podívat se. Ovládací prvek postavený tímto způsobem funguje dobře, dokud nepotřebuje regresní sadu testů nebo druhého hostitele — dialog náhledu tisku, pás miniatur, dávkový prohlížeč bez jakéhokoli viditelného okna — a stav, který potřebujete, se ukáže být přivařený k TWinControl, který trvá na skutečném handle dřív, než udělá cokoli
Proč vůbec potřebuje ovládací prvek prohlížeče PDF rozdělení MVC?
Prohlížeč PDF potřebuje tento druh rozdělení, protože se jeho stav a jeho prezentace mění z různých důvodů a různou rychlostí. Index stránky, zoom, otočení pohledu, zásahy hledání a oblasti zvýraznění jsou stav businessu: dají se spočítat, ověřit a serializovat bez jediného pixelu na obrazovce. Vykreslení bitmapy, zachycení myši a kreslení výběrového obdélníku typu marquee jsou naopak prezentační záležitosti, které dávají smysl teprve tehdy, jakmile existuje ovládací prvek. HotPDF drží první skupinu v THPDFViewerModel, třídě zcela bez předka z oblasti oken VCL, a druhou skupinu v THPDFViewer, který vlastní instanci modelu a na ni reaguje — blíž dvojici Model-View než učebnicovému třívrstvému MVC, protože zde neexistuje samostatná třída Controller a THPDFViewer sám převádí surové události klávesnice a myši na volání modelu. Důležitější než nálepka je směr závislosti: nic v THPDFViewerModel nevyžaduje Handle, smyčku zpráv ani viditelnou plochu — což je přesně to, co umožňuje vlastní testovací sadě HotPDF řídit stránkování, ořezávání zoomu, klávesové příkazy a obousměrné převody souřadnic přes DUnitX bez otevření 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;
Co přesně vlastní THPDFViewerModel
THPDFViewerModel vlastní vše, co prohlížeč potřebuje k odpovědi na otázku, co by mělo být právě teď na obrazovce, aniž by vlastnil to, jak se to má nakreslit. PageIndex, PageNumber a PageCount sledují pozici; Zoom a ZoomMode (vzmActualSize, vzmFitPage, vzmFitWidth, vzmCustom) sledují měřítko; ViewRotation sleduje nedestruktivní otočení na obrazovce, které se nikdy nedotkne vlastního záznamu stránky /Rotate. Navigační metody — FirstPage, PriorPage, NextPage, LastPage — a metody zoomu — ZoomIn, ZoomOut, procházející pevnou tabulku devatenácti přednastavených úrovní od 5 % do 6400 % — žijí také zde, spolu s FindAll/FindNext/FindPrevious pro hledání textu a AddHighlightRegion/RemoveHighlightRegion/ClearHighlightRegions pro trvalé anotace stránky, které si volající chce podržet mezi vykresleními. Model vlastní stejně tak výstup jako vstup: CreateCurrentPageSnapshot a CreateCurrentPageMetafile exportují přesně tu stránku, která je právě na obrazovce, a PrintCurrentView posílá tentýž aktuální pohled — aktuální stránku, aktuální DPI odvozené ze zoomu, aktuální otočení — do TPrinter, což je užší úloha vázaná na pohled než celková pipeline tisku dokumentu popsaná v průvodci HotPDF pro tisk přes TPrinter. Každá mutace, na které záleží, navíc vyvolá odpovídající událost — OnPageChange, OnZoomChange, OnSearchChange, OnHighlightChange, OnViewRotationChange — takže se odběratel dozví, co se změnilo, bez pollingu
Jak THPDFViewer pozná, kdy má překreslit?
THPDFViewer pozná, kdy má překreslit, protože se přihlásí k odběru u modelu místo toho, aby hádal. Konstruktor THPDFViewer vytvoří soukromý THPDFViewerModel a pak napojí úplně každou z jeho notifikačních událostí — OnBeginUpdate, OnEndUpdate, OnHighlightChange, OnPageChange, OnSearchChange, OnViewRotationChange, OnZoomChange — na odpovídající soukromou obsluhu. Úkol každé obsluhy je malý: zavolat RefreshDocument, metodu, která skutečně rasterizuje aktuální stránku přes stejný cachovaný vykreslovač stránek popsaný v interních detailech vykreslování stránky do bitmapy v HotPDF, a pak navrch složí zvýrazňovací rámečky a zásahy hledání a aplikuje aktuální otočení pohledu. Publikované vlastnosti jako PageIndex, Zoom, ZoomMode a ViewRotation jsou tenké přeposílače — getter čte FModel.PageIndex, setter zapisuje FModel.PageIndex — takže z Object Inspectoru nebo z kódu to vypadá, že ovládací prvek drží stav přímo, přestože THPDFViewerModel je jediné místo, kde ten stav skutečně žije. Volající navíc nejsou omezeni jen na přeposílanou podmnožinu: THPDFViewer vystavuje samotný model přes vlastnost Model: THPDFViewerModel jen pro čtení, takže kód, který chce FindFormFieldAt nebo PrefetchCurrentPageSnapshots — ani jedno z toho ovládací prvek znovu nevystavuje — může sáhnout za obal a zavolat model přímo
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: jak zastavit bouři překreslení
BeginUpdate a EndUpdate existují proto, že jedna logická změna se často dotýká několika kusů stavu najednou, a překreslovat po každém z nich by bylo plýtvání a vizuálně rušivé. Nejjasnějším příkladem je výměna načteného dokumentu: přiřazení THPDFViewerModel.Document resetuje otočení pohledu, vyčistí zásahy hledání, vyčistí oblasti zvýraznění a skočí na první stránku, a každý z těchto kroků by normálně vyvolal vlastní událost změny. THPDFViewerModel tuto sekvenci obalí do BeginUpdate/EndUpdate, dvojice s počítáním referencí, kde vnořená volání vyvolají OnBeginUpdate jen při přechodu do nejvzdálenějšího volání a OnEndUpdate jen při přechodu zpět ven z něj. THPDFViewer sleduje na své straně stejnou hloubku a přeskočí RefreshDocument pro každou dílčí událost, dokud je počítadlo nad nulou, a pak překreslí přesně jednou, jakmile se dávka uzavře. Dílčí události se během dávky přesto vyvolávají, takže odběratel, kterému záleží jen na OnSearchChange, se o ní přesto dozví; sloučí se jen vlastní překreslení ovládacího prvku do jednoho volání místo čtyř
Jak zvýraznění typu marquee mapuje tažení myší zpět na souřadnice PDF?
Zvýraznění typu marquee mapuje tažení myší zpět na souřadnice PDF přes dvojici metod modelu postavených přesně pro tento obousměrný převod: PagePointToView a ViewPointToPage. Obě berou index stránky, DPI a bod a obě řeší transformaci ve dvou fázích — nejdřív vlastní záznam stránky /Rotate a její počátek PDF vlevo dole, pak samostatné, nedestruktivní ViewRotation pohledu a počátek prohlížeče vlevo nahoře — přesně proto, aby opačný směr dokázal obě fáze zrušit ve striktně obráceném pořadí a obousměrný převod fungoval správně napříč všemi šestnácti kombinacemi otočení stránky a otočení pohledu. THPDFViewer volá ViewPointToPage ve chvíli, kdy uživatel po tažení obdélníku v interakčním režimu vimHighlight pustí tlačítko myši, promění oba body zařízení na THPDFRectangle v prostoru stránky a předá jej Model.AddHighlightRegion. Jeden detail, který stojí za znalost, pokud stavíte něco podobného: zachycení myši patří prohlížeči odvozenému od TScrollBox, ne podřízenému TImage, do kterého se maluje bitmapa, protože TControl.MouseCapture je protected a nárokovat si jej může jen nadřazený ovládací prvek — takže tažení, které opustí hranice obrázku dřív, než se tlačítko pustí, se stále vyřeší přes vlastní přetížené MouseMove/MouseUp prohlížeče, místo aby jej podřízený ovládací prvek tiše zahodil
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;
Co vám toto rozdělení přinese kromě zelené testovací sady
Přínos se neomezuje jen na testy procházející v CI úloze bez desktopové relace. Protože THPDFViewer přeposílá do THPDFViewerModel místo toho, aby jeho logiku duplikoval, mohl HotPDF přidat třetího konzumenta — THPDFViewerAction a konkrétní potomky jako THPDFZoomInAction a THPDFFindNextAction — které zapojují navigaci, zoom, hledání a otočení do standardního Delphi TActionList, takže tlačítko na panelu nástrojů nebo položka nabídky může prohlížeč řídit deklarativně a automaticky se aktivovat podle toho, zda se prohlížeč právě rozpozná jako cíl akce. Žádná z těchto vrstev nemusela vědět nic o bitmapách nebo GDI; volá Viewer.NextPage nebo Viewer.Model.FindNext a o překreslení se postará stávající řetěz událostí. A protože se nic v THPDFViewerModel neodkazuje na TScrollBox, TImage ani handle okna, ani stavový automat pod tím vším není přivařený jen k tomuto jednomu ovládacímu prvku — stejný model by mohl stát za jinou vykreslovací plochou, aniž by se dotkl jediného řádku logiky navigace, zoomu nebo hledání
Kde pomáhá cache vykreslování, a kde ne
Cache vykreslování v THPDFViewerModel pomáhá uvnitř už načteného dokumentu, ale nemění, kolik stojí samotné načtení toho dokumentu. CreatePageSnapshot, CreateCurrentPageSnapshot a metody prefetch PrefetchPageSnapshots/PrefetchCurrentPageSnapshots všechny prochází přes stejný cachovaný vykreslovač klíčovaný podle stránky a DPI, takže návrat na stránku, kterou jste už viděli při stejné úrovni zoomu, je zásah do cache, ne nové vykreslení, a prefetch malého okruhu sousedních stránek vyhlazuje běžný případ čtenáře, který listuje dopředu po jedné stránce. Nic z toho se ale nedotýká nákladu na počáteční volání LoadFromFile, a prohlížeč postavený tak, aby otevřel cokoli, co na něj uživatel přetáhne, dřív nebo později narazí na soubor tak velký, že se právě toto volání stane skutečným úzkým hrdlem. O vrstvené alternativě k plnému načtení založené na handle — vyplatí se ji znát dřív, než tento den nastane — pojednává doprovodný článek o Direct File API pro velké soubory PDF
Třídy Model a View popsané zde jsou další dva kusy téže plochy načteného dokumentu, používané napříč celou komponentou HotPDF pro Delphi a C++Builder, postavené tak, aby je bylo možné řídit z formuláře, z TActionList, nebo z ničeho z toho