Keď dynamický XFA formulár v Delphi prehliadači pridá alebo odstráni strany, reportuje PDFium Component nový celok cez TPdf.PageCount a TPdf.OnXfaPageCountChanged od v3.126.1, pretože natívna straničná udalosť nesie deltu pridané/odstránené, nie celok. Windows V8 knižnice vo v3.126.1 presúvajú vstupné oblasti aj s premiestnenými poliami a v3.126.2 znovu načíta zorozené handle strán po návrate layout callbacku. Bug report, ktorý to odštartoval, bol formulár výdavkového priznania: kliknite dvakrát na Add Row, formulár narastie na dve strany a indikátor strán hrdo hlási 1 of 1. Napíšte do poľa, ktoré sa presunulo na stranu 2, a klávesy dopadnú niekam neviditeľne. Nič z toho sa neukázalo na vzorových formulároch pevnej dĺžky, ktorými sa začína u každého testovanie, a dôvody toho stoja za poznanie, ak vkladáte form viewer
Čo sa deje, keď sa dynamický XFA formulár repaginuje?
Dynamický XFA formulár nemá pevný zoznam strán, takže jeho počet strán je výstupom layoutu a môže sa meniť pri každej úprave dát používateľom. XFA 3.3 popisuje formulár ako strom subformulárov; opakujúci sa subformulár riadi instanceManager a skript ako _Row.addInstance() naklonuje ďalší riadok. Layout procesor potom znova prevalí obsah do oblastí strán, čo môže pridať stranu, ubrať stranu alebo pretlačiť existujúce polia na inú stranu. ISO 32000-1 §12.7.8 definuje len to, ako XFA balíky jazdia vo vnútri PDF; všetko, čo príde potom, patrí XFA engine, ktorým je v PDFium Component vlastné XFA layout PDFia bežiace v procese hostiteľa. Delphi prehliadač má preto činenie s dokumentom, ktorého počet strán, rozmery strán a pozície widgetov sú všetko živý stav. Keď hostiteľ predpokladá opak, pokazia sa tri veci:
- Počet strán, ktorý si hostiteľ ukladá do cache pre navigáciu, rozsahy rolovania a spinne strán, zorozie, alebo horšie, sa aktualizuje nesprávnym číslom
- Polia, ktoré sa premiestnia, ukazujú okraj na novej pozícii, kým editor a oblasť zásahu myši ostanú na starých súradniciach
- Prehliadač drží handle strany, ktorý layout vymenil, takže kliky a kreslenie idú na stranu, ktorá už v tom formulári neexistuje
Trvanie úprav riadkov cez uloženie a znovuotvorenie je osobitný problém s vlastnými pravidlami; tento článok ostáva pri tom, čo sa deje za behu vo vnútri prehliadača
Ktorý PDFium runtime potrebuje dynamické XFA?
Dynamické XFA v PDFium Component vyžaduje V8/XFA zostavenie natívnej knižnice, volené globálnou premennou EnableV8Engine v jednotke PDFium pred načítaním prvého dokumentu. Proces sa prvý raz, keď ktorýkoľvek TPdf načíta knižnicu, zaviaže k jedinému DLL a holé zostavenie PDFium nedokáže XFA engine rozbehnúť vôbec. Keď sa dokument otvorí, TPdf sa síce pozrie do súboru po XFA značkách a prepne na V8 zostavenie automaticky, ale len ak ešte žiadna holá knižnica v tom procese nebola načítaná. Keď sa záväzok už vydal zlou stranou, TPdf.OnXfaRuntimeMissing sa spustí raz, aby mohol hostiteľ povedať používateľovi, nech reštartuje. Explicitné nastavenie príznaku pri štarte odstráni hádanie. Štruktúra callbackov FPDF_FORMFILLINFO nesúca XFA udalosti sa musí tiež zhodovať s DLL; pozadie popisuje FPDF_FORMFILLINFO verzia 2 a XFA callback ABI a rozpoznávanie XFA formulárov a čítanie ich balíkov pokrýva odlíšenie typov formulárov, skôr než otvoríte prehliadač
uses
PDFium;
procedure TClaimForm.FormCreate(Sender: TObject);
begin
// Rozhodnite skôr, než prvý TPdf načíta natívnu knižnicu:
// proces nemôže neskôr prejsť z pdfium.dll na pdfium.v8.dll
EnableV8Engine := True;
FPdf := TPdf.Create(nil);
FPdf.OnXfaRuntimeMissing := PdfXfaRuntimeMissing;
FPdf.OnXfaPageCountChanged := PdfXfaPageCountChanged;
FPdf.FileName := 'C:\Forms\expense-claim.pdf';
FPdf.Active := True;
PdfView1.Pdf := FPdf;
PdfView1.OnPageChange := PdfViewPageChange;
PdfView1.Active := True;
UpdatePageRange(FPdf.PageCount);
end;
procedure TClaimForm.PdfXfaRuntimeMissing(Sender: TObject);
begin
StatusBar1.SimpleText :=
'This XFA form needs the V8 runtime; restart the application to enable it';
end;
Prečo PageCount hlásilo 1 pri dvojstranovom formulári?
Pred v3.126.1 si PDFium Component ukladal argument page_count natívnej straničnej udalosti ako celok dokumentu a ten argument je v skutočnosti absolútny rozdiel medzi novým a starým počtom strán. PDFium spúšťa FFI_PageEvent po dokončení layout priechodu s typom udalosti strana pridaná alebo strana odstránená; interne si najprv aktualizuje uložený počet strán a potom podá abs(new - old). Pri úvodnom layoute je starý počet nula, takže delta sa rovná celku a trojstranný statický vzor hlási tri strany, ako sa čaká. Presne preto nikdy testovacie formuláre pevnej dĺžky neodhalili bug. Prvý raz, keď dynamický formulár narastie z jednej strany na dve, delta je 1 a wrapper nastavil TPdf.PageCount aj parameter NewCount OnXfaPageCountChanged na 1. Odobratie riadka z trojstranového formulára vyrobilo tú istú triedu nesmyslov opačným smerom
Sčítavanie delty na predchádzajúcu hodnotu tiež nie je bezpečná oprava. Poradie inicializačných a layout callbackov znamená, že wrapper si nemôže vždy siať k skoršiemu počtu ako k základni, takže bežný súčet môže drifovať. Od v3.126.1 ignoruje callback argument ako počet a volá FPDF_GetPageCount nad dokumentom, ktoré číta celok z layoutu, ktorý sa práve dokončil. Potom vyčistí cached scény strán, uloží ten celok ako XFA override počtu strán za TPdf.PageCount a až potom spustí OnXfaPageCountChanged. Kým sa váš handler spustí, NewCount a FPdf.PageCount sedia
procedure TClaimForm.PdfXfaPageCountChanged(Sender: TObject; NewCount: Integer);
begin
// v3.126.1+: NewCount je celok dokončeného layoutu, nikdy delta.
// Toto beží vo vnútri layout callbacku PDFium: aktualizujte len UI stav hostiteľa,
// nezatvárajte odtiaľ dokument ani nenačítavajte strany znova
UpdatePageRange(NewCount);
end;
procedure TClaimForm.PdfViewPageChange(Sender: TObject);
begin
// Spustí sa po každom znovunačítaní strany, vrátane odkladanej XFA obnovy
PageSpin.Value := PdfView1.PageNumber;
end;
procedure TClaimForm.UpdatePageRange(Count: Integer);
begin
PageSpin.MinValue := 1;
PageSpin.MaxValue := Count;
PageLabel.Caption := Format('of %d', [Count]);
end;
Udalosť sa spúšťa len pri Full XFA formulároch, ktorých layout sa mení za behu. Statické XFA a AcroForm dokumenty ju nikdy nespustia, takže prehliadač obsluhujúci oboje môže nechať ten istý handler priradený. Ponechanie nepriradeného je tiež bezpečné; override za TPdf.PageCount sa aplikuje tak či onak a udalosť existuje preto, aby si hostiteľ mohol obnoviť, čo si cacheoval
Prečo ostané vstupný box na starej strane, keď sa pole presunie?
Okraj sa presunul a editor nie, pretože natívny XFA notifier porovnával obdĺžnik sám so sebou. Keď layout zmení geometriu už načítaného widgetu, PDFium má všimnúť si nový obdĺžnik a zavolať na widget PerformLayout, ktorý presunie textový editor aj jeho oblasť zásahu. Kontrola porovnávala GetWidgetRect() s RecacheWidgetRect(). Obidve funkcie vraciajú const referenciu na ten istý člen a recache prepíše tento člen na mieste, takže porovnanie vždy videlo dve identické hodnoty a načítané widgety preskočili svoj relayout
Príznak vyšiel na povrch, keď test zmenil výšku subformulára tak, že existujúce polia prekročili na ďalšiu stranu. Na oboch V8 architektúrach sa okraj poľa kreslil na novej pozícii, kým napísaný text aj oblasť zásahu myši ostali pri predchádzajúcej súradnici Y. Explicitný relayout to neopravil a znovunačítanie strany tiež nie, lebo widget stále veril, že jeho geometria je aktuálna. Windows V8 knižnice dodané s v3.126.1 skopírujú starý obdĺžnik hodnotou pred recache a porovnávajú tú kópiu, takže presunuté widgety prebehnú relayout a upravená hodnota sa objaví presne tam, kde je okraj. Toto je natívna oprava: cestuje s DLL, takže aktualizácia Pascal jednotiek pri zachovaní staršieho pdfium.v8.dll nechá premiestnené oblasti zásahu na mieste. Regresná kontrola, ktorá ju vynútila, najprv upraví preživší riadok na hodnotu odlišnú od predvolenej a potom žiada tú hodnotu na novej lokalite poľa, lebo riadok prestavený s predvolenými hodnotami by inak vyzeral ako úspech
Ako znovu načíta TPdfView strany, než aby vytrhol handle PDFiu spod nôh?
Od v3.126.2 odkladá TPdfView znovunačítanie strany, ktoré nasleduje po zmene XFA layoutu, kým sa natívny zásobník volaní nevyprázdni. Straničná udalosť sa zvyčajne spustí, kým PDFium ešte spracováva vstup: používateľ klikol na tlačidlo Add Row, klik spustil skript, skript zmenil počet inštancií a layout sa dokončil vnútri toho istého natívneho volania. Zavrieť a znovu otvoriť handle strany v ten moment by uvoľnilo objekt, ktorý volajúci stále používa. Pred v3.126.2 sa prehliadač len invalidoval, takže zobrazovaný handle strany mohol naďalej ukazovať na stav pred layoutom a ak bol používateľ na poslednej strane, keď zmizla, vybrané číslo strany bolo mimo rozsahu
Odkladaná obnova funguje v pár malých krokoch a tie vysvetľujú správanie, ktoré vidíte z hostiteľa:
- Callback straničnej udalosti označí view, že má nevybavenú XFA layout obnovu, a pošle súkromnú window správu; opakované udalosti pred príchodom správy sa zlúčia do jednej obnovy
- View bez window handle si zatiaľ drží príznak čakajúcej obnovy a pošle správu z
CreateWnd, zatiaľ čo zmena dokumentu, deaktivácia view alebo jeho zničenie príznak zmaže - Keď správa dorazí, view vyčistí výber textu, hľadacie zvýraznenie a index zaostreného poľa, lebo všetky tri sa viazali na starý layout
- Vybraná strana sa svorkuje na nový
PageCount; zmenené číslo strany ide cez normálnu výmenu strany, inak sa aktuálna strana znovu načíta a režim prispôsobenia sa aplikuje znova - Ak layout nezanechá žiadne strany, view vyloží svoj starý handle strany namiesto kreslenia strany, ktorá už neexistuje
To isté obmedzenie platí pre váš vlastný kód. OnXfaPageCountChanged beží vo vnútri toho natívneho layout callbacku, takže sa k nemu stavajte ako k notifikácii: tam aktualizujte návestia, rozsahy spinnerov a stav panela nástrojov a čokoľvek ťažšie, ako zatvorenie dokumentu alebo otvorenie iného, radte do fronty cez poslanú správu, aby bežalo po návrate callbacku. TPdfView.OnPageChange vám potom povie, kedy view skutočne znovu načítal stranu a čítanie PdfView1.PageNumber v ten okamih dáva svorkovanú hodnotu. Prechádzanie Tabom a kontroly FormType, ktoré form viewer robí pri otvorení, pokrýva navigácia formulárových polí PDF s PDFium Component
Prečo kliknutie na pole Full XFA vyhodí „Cannot open text page“?
Strany Full XFA nemajú PDF textovú stranu a pred v3.126.2 sa predvolený výber textu a rozpoznávanie odkazov v prehliadači aj tak snažili jednu načítať. S TPdfView.AllowUserTextSelection na predvolenom True sa preletenie myšou pýtalo textovej vrstvy na znak pod kurzorom a klik pri pustení tlačidla pustil automatickú URL sondu nad textom strany. Na strane Full XFA sa textová strana otvoriť nedá, takže obyčajný klik do poľa mohol skončiť výnimkou Cannot open text page. Od v3.126.2 vracajú obidve interné cesty žiadny výsledok, keď je TPdf.FormType ftXfaFull a XFA runtime je dostupný, takže predvolené nastavenia fungujú a vstup do polí ostáva dostupný
Vypnúť AllowUserTextSelection pre Full XFA dokumenty je stále rozumná UI voľba, lebo nie je žiadny text strany na výber a ťahové gestá nemajú spúšťať režim výberu. Nie je to však náhrada za upgrade: na skorších verziách URL sonda pri kliknutí na tejto vlastnosti nezávisela, takže prehliadač mohol naraziť na tú istú výnimku aj s vypnutým výberom
procedure TClaimForm.ConfigureViewerForForm;
begin
// FormType číta otvorený dokument, takže to volajte po FPdf.Active := True
if FPdf.XFA and (FPdf.FormType = ftXfaFull) and FPdf.XfaRuntimeAvailable then
begin
// Na stranách Full XFA neexistuje PDF textová vrstva; polia ostávajú editovateľné
PdfView1.AllowUserTextSelection := False;
StatusBar1.SimpleText := Format('Dynamic XFA form, %d page(s)',
[FPdf.PageCount]);
end
else
PdfView1.AllowUserTextSelection := True;
end;
Písanie si vo v3.126.2 vyžiadalo vlastnú opravu. Natívny XFA textový editor nenahrádza výber, keď dostane znak: FORM_OnChar vkladá na pozíciu kurzora a Backspace maže jediný znak, takže vybratie hodnoty a písanie cez ňu vyrábalo starý a nový text vedľa seba. PDFium Component si teraz pamätá, že klik pristál na XFA textovom poli a smeruje písané znaky, Backspace a Delete cez FORM_ReplaceSelection, kedykoľvek existuje výber a dokument udeľuje povolenie vyplňovať formuláre alebo upravovať. To, či sa smie zmeniť pole XFA len na čítanie, stále rozhoduje natívny editor, takže pole označené vo formulári ako read-only si drží hodnotu aj v dokumente, ktorý inak vyplňovanie dovoľuje. Nastavenie TPdfView.AllowFormEvents na False tiež zastaví toto smerovanie klávesnice, čím zostane read-only prehliadač read-only
Rýchly prehľad: dynamické XFA v Delphi prehliadači
| Príznak | Príčina | Opravené vo |
|---|---|---|
| Počet strán ukazuje 1, keď formulár narastie na dve strany | Natívna straničná udalosť podáva deltu pridané/odstránené, nie celok | v3.126.1 (wrapper) |
| Okraj poľa sa presunie, napísaný text a oblasť zásahu ostanú pozadu | Načítaný widget preskočil relayout po porovnaní samého so sebou | v3.126.1 (Windows V8 knižnice) |
| Prehliadač kreslí alebo smeruje vstup na stav strany pred layoutom | Handle strany sa po repaginácii znovu nenačítal | v3.126.2 (odložená obnova) |
| Klik do poľa vyhodí Cannot open text page | Výber textu a URL sonda na stranách bez textovej vrstvy | v3.126.2 |
| Písanie cez vybranú hodnotu prilepí namiesto nahradenia | Natívny XFA editor vkladá na pozíciu kurzora | v3.126.2 |
- Nastavte
EnableV8EnginenaTruepred načítaním akéhokoľvek dokumentu a obslúžteOnXfaRuntimeMissingpre prípad, že holá knižnica sa načítala ako prvá - Čítajte celok z
TPdf.PageCountalebo parametraNewCountOnXfaPageCountChanged; nikdy si počty strán nesčítavajte ani neodpočítavajte sami - Držte handler
OnXfaPageCountChangedľahký, lebo beží vo vnútri natívneho layout callbacku - Synchronizujte indikátor aktuálnej strany v
TPdfView.OnPageChange, ktorý zasahuje po tom, ako odkladané znovunačítanie svorkuje číslo strany - Nasádzajte Windows V8 DLL vo v3.126.1 a novšie spolu s jednotkami; oprava relayoutu widgetov býva v natívnom kóde
- Testujte s formulárom, ktorý skutočne mení počet strán a presúva upravené pole cez zlom strany, lebo vzorky pevnej dĺžky skryjú každý bug z tohto zoznamu
Dynamické XFA mení počet strán a geometriu polí na živé hodnoty a prehliadač ostáva správny len vtedy, keď ich berie z dokončeného layoutu a znovu načíta strany v bezpečnom momente. PDFium Component obslúzhi oboje vo vnútri TPdf a TPdfView, takže hostiteľovi ostáva len počúvať. Detaily a stiahnutie sú na stránke produktu PDFium Component pre Delphi