Två klagomål följer varje inbäddad PDF-visare: stora dokument renderas om från början vid varje start, och text blir suddig så fort fönstret dras till en skärm med hög DPI. losLab PDF Library löser båda dessa problem i sin kontroll TPDFlibViewer: egenskapen DiskCacheFolder aktiverar en diskcache för sidor som sparas mellan sessioner, identifierad med dokumentets fingeravtryck, och egenskapen ScreenDPI, som stöds av GetDpiForWindow och en WM_DPICHANGED-hanterare, ger kontrollen äkta DPI-medvetenhet per skärm
Båda problemen är värda att ta på allvar eftersom användare märker dem omedelbart. Ett skannat kontrakt på 400 sidor som tog åtta sekunder att bli rullningsbart igår tar åtta sekunder igen idag, trots att ingenting i filen har ändrats. Och på ett skrivbord med blandade DPI-inställningar ser en visare som renderas med den primära skärmens 96 DPI bra ut där, men upplevs synbart oskarp på 4K-panelen bredvid. Den här artikeln går igenom hur TPDFlibViewer löser båda problemen och, vilket är mer användbart, varför designen landade i just dessa lösningar. Om du är ny när det gäller själva kontrollen bör du börja med översikten över den interaktiva PDF-visarkontrollen och sedan komma tillbaka hit för att läsa om prestandaskiktet
Hur cachelagrar du renderade PDF-sidor mellan sessioner i Delphi?
Tilldela en mapp till TPDFlibViewer.DiskCacheFolder så sparar kontrollen renderade sidbitmappar på disken. På så sätt öppnas ett nyligen visat dokument omedelbart istället för att renderas om. Layouten på disken är <Folder>\PDFlibPas-PageCache\<document fingerprint>\p<page>_z<zoomkey>.bin: en undermapp per dokument, en fil per kombination av sida och zoom. Allt annat sker automatiskt; det finns inget cache-API to call och ingenting som behöver rensas manuellt
uses
PDFlibViewer;
procedure TMainForm.FormCreate(Sender: TObject);
begin
FViewer := TPDFlibViewer.Create(Self);
FViewer.Parent := Self;
FViewer.Align := alClient;
// One line turns on the cross-session page cache
FViewer.DiskCacheFolder := 'C:\ProgramData\MyApp\PageCache';
FViewer.LoadFromFile('C:\Contracts\master-agreement.pdf', '');
end;
Räknaren DiskCacheHits rapporterar hur många sidrenderingar diskcachen har sparat under den aktuella sessionen, vilket gör vinsten mätbar snarare än bara upplevd. För ett dokument som du stängde och öppnade igen ökar räknaren för varje sida som rullas in i vyn utan att behöva renderas
Varför cachenyckeln är ett fingeravtryck och inte ett versionsnummer
Dokumentets fingeravtryck är en 64-bitars FNV-1a-hash över strängen path|size|last write time, formaterad som sexton hex-siffror. Denna trippel utgör hela strategin för ogiltigförklaring: när filen redigeras ändras dess storlek eller skrivtid, vilket leder till att hashen ändras och visaren öppnar helt enkelt en annan cache-undermapp. Den gamla mappen konsulteras aldrig igen och tas så småningom bort genom LRU-rensning. Det finns inget versionstaggsprotokoll att underhålla, ingen tidsstämpelsjämförelselogik som kan driva iväg och inget sätt för en gammal bitmapp att serveras för ett ändrat dokument
Detta är samma idé om "innehållsidentitet som cachenyckel" som byggsystem använder, och den visar sitt värde genom de felscenarier den eliminerar. En cache som lagrar filer under dokumentets vanliga namn måste identifiera redigeringar explicit, och varje explicit kontroll är en potentiell bugg som väntar på ett gränsfall: en fil som återställts från säkerhetskopia med en gammal tidsstämpel, en sparfunktion som bevarar storleken, eller en sökväg som jämförs skiftlägeskänsligt på en maskin men inte på en annan. Att hasha alla tre signalerna till mappnamnet gör inaktualitet strukturellt omöjlig snarare än procedurmässigt kontrollerad
Varför rotation avsiktligt har utelämnats från diskenyckeln
Cachefilens namn kodar sidnummer och zoom, och inget annat — vyrotationsvinkeln saknas avsiktligt. Diskcachen lagrar alltid de oroterade renderingsbyten; när en cachelagrad sida läses in medan vyn är roterad, omorienteras pixlarna i minnet efter avkodning. Om rotation vore en del av diskenyckeln skulle en användare som växlar mellan de fyra vyvinklarna skriva samma sida till disken fyra gånger. Detta skulle fyrdubbla cachen tillväxt för noll extra information, eftersom rotation är en billig pixelomflyttning jämfört med en fullständig rendering
Bitmappscachen i minnet använder en sammansatt nyckel, Round(Zoom * 1000) * 4 + Rotation div 90, eftersom den roterade bitmappen är precis vad ritningshanteraren behöver där. Tråden för bakgrundsförhämtning följer samma princip: den renderar alltid oroterade sidor och rapporterar enkla zoomnycklar, och huvudtråden tillämpar rotationen när resultaten töms – samma regel om av "huvudtråden äger cachen" som diskuteras i artikeln om parallell sidrendering och trådsäkerhet. Att hålla rotationen utanför allt under minnesskiktet innebär en enda kanonisk rendering per sida och zoom, överallt där det är kostsamt att lagra
Hur begränsar LRU-rensning cachemappens storlek?
Cachen sparar upp till tio dokumentmappar och rensar de minst använda när nya dokument tillkommer. Användningshistoriken spåras via en fil last-used.marker i varje dokumentmapp, vars tidsstämpel uppdateras vid både skrivningar och läsningar i cachen. Det gör att ett dokument som du bara läser igen hålls kvar utan att behöva skrivas om. Mappen som tillhör det för närvarande öppna dokumentet är undantagen från radering, så rensningen kan aldrig ta bort sidor under tiden de visas aktivt
Varje diskoperation i cachen — fingeravtryck, mappskapande, uppdatering av markörer, läsning och skrivning av sidor samt rensning — döljer sina egna undantagsfel. Detta är ett medvetet val, inte slarv: cachen är en accelerator som arbetar efter bästa förmåga. En full disk, en skrivskyddad mapp eller en låsning av antivirusprogram får endast degradera visaren till att "rendera som tidigare", men aldrig till att "misslyckas med att visa sidan". Det värsta resultatet ett IO-fel i cachen kan ge upphov till är en missad cacheträff
procedure TMainForm.FormClose(Sender: TObject; var Action: TCloseAction);
begin
// How many render passes did the disk cache save this session?
Log(Format('Disk cache hits: %d', [FViewer.DiskCacheHits]));
end;
Hur gör du en PDF-visare DPI-medveten i Delphi?
TPDFlibViewer leder varje DPI-beroende beräkning genom ett enda fält, och denna design med en enda ändringspunkt är det som gör stödet för skärmar hanterbart. Layout-, förhämtnings- och renderingskod läser alla samma FScreenDPI-värde; vid 100 % zoom mappar en PDF-punkt till ScreenDPI / 72 pixlar. Det offentliga skrivskyddet för egenskapen ScreenDPI tömmer bitmappscachen, bygger om layouten, uppdaterar rullningsintervallen och ogiltigförklarar fönstret, så att en ändring av DPI blir en enda tilldelning där alla konsumenter följer med automatiskt. Samma pixelmatematik är viktig när du renderar för en skrivare istället för en bildskärm, ett ämne som behandlas i artikeln om förhandsgranskning och rendering av enhetskontext
Detekteringen sker i en överskuggning av CreateWnd snaren än i konstruktorn, eftersom GetDpiForWindow kräver ett giltigt fönsterhandtag och konstruktorn körs långt innan ett sådant finns. Innan CreateWnd utlöses har kontrollen standardvärdet 96 för att layoutmatematiken ska förbli stabil. Själva GetDpiForWindow är ett API för Windows 10 1607+, så kontrollen binder det dynamiskt med GetProcAddress och faller tillbaka på GetDeviceCaps(LOGPIXELSY) på äldre system — enheten laddas fortfarande överallt, och äldre Windows rapporterar helt enkelt systemets DPI
Vad händer när fönstret flyttas till en 4K-skärm?
Windows skickar WM_DPICHANGED när ett DPI-medvetet fönster flyttas till en bildskärm med en annan skalfaktor, och TPDFlibViewer hanterar detta direkt. Hanteraren läser av det nya DPI-värdet från det låga ordet i wParam, tilldelar det via SetScreenDPI — vilket rensar den nu inkorrekta bitmappscachen och lägger ut sidorna på nytt — och accepterar sedan den föreslagna fönsterrektangeln som Windows skickar i lParam via SetWindowPos, vilket håller den logiska klientstorleken stabil under flytten. Användaren ser dokumentet renderas skarpt i den nya skalan istället för en bitmapp som skalas upp av operativsystemet
// Normally you never touch ScreenDPI: CreateWnd detects the host monitor
// and WM_DPICHANGED tracks moves. Override it only for special targets,
// e.g. rendering the layout as if for a 150% display:
FViewer.ScreenDPI := 144; // clears the bitmap cache and rebuilds layout
Begränsningar värda att känna till innan du lanserar
Tre praktiska begränsningar förtjänar en plats i din designgranskning. För det första tas WM_DPICHANGED endast emot om processen har valt detta: din applikationsmanifest måste deklarera DPI-medvetenhet per bildskärm (per-monitor v2 i nuvarande Windows), vilket i Delphi IDE är inställningen för DPI-medvetenhet under applikationsalternativ. En process med system-DPI-medvetenhet får aldrig detta meddelande, och kontrollen renderar då vid den DPI som detekterades vid skapandet av fönstret. För det andra byter diskcachen diskutrymme mot snabbhet — tio dokuments värde av sidbitmappar över flera zoomnivåer är verkligt lagringsutrymme, så peka DiskCacheFolder mot en plats du känner dig trygg med att den växer, inte mot en mobil användarprofil. För det tredje kräver cachemappen skrivbehörighet för den aktiva användaren; utan detta fungerar visaren fortfarande men utan några träffar — kontrollera DiskCacheHits under testning om du misstänker att cachen inte används
Tillsammans ändrar dessa två funktioner den upplevda kvaliteten i en dokumenttung applikation mer än de flesta renderingsoptimeringar: dokument som dina användare återvänder till öppnas direkt, och vyn förblir skarp på vilken skärm den än hamnar. Båda levereras som vanliga egenskaper på TPDFlibViewer i losLab PDF Library, så att integrera dem är snarare en eftermiddags jobb med anslutningar än ett projekt för en renderingsmotor