Amikor egy dinamikus XFA űrlap egy Delphi nézegetőben oldalakat ad hozzá vagy elvesz, a PDFium Component v3.126.1 óta az új összeget jelenti a TPdf.PageCount-on és a TPdf.OnXfaPageCountChanged-en át, mert a natív oldal-esemény hozzáadott/elvett deltát hordoz, nem összeget. A v3.126.1-es Windows V8 libraryk az áthelyezett mezőkkel mozgatják a bemeneti találati területeket is, a v3.126.2 pedig elavult oldalhandleket tölt újra, miután a layout callback visszatért. Az ezt elindító hibajelentés egy költségelszámolási űrlap volt: kattints kétszer az Add Rowra, az űrlap két oldalra nő, és az oldalkijelző büszkén azt írja, 1 / 1. Írj egy olyan mezőbe, ami átköltözött a 2. oldalra, és a leütések láthatatlan helyre érkeznek. Egyik sem mutatkozott meg azokon a fix hosszúságú mintaűrlapokon, amikkel mindenki először tesztel, és megéri tudni, miért, ha űrlapnézegetőt ágyazol be
Mi történik, amikor egy dinamikus XFA űrlap újrapaginál?
Egy dinamikus XFA űrlapnak nincs rögzített oldallistája, tehát az oldalszáma a layout kimenete, és minden adatszerkesztésnél változhat. Az XFA 3.3 fáként írja le az űrlapot subformokból; egy ismétlődő subformot egy instanceManager vezérel, és egy _Row.addInstance() szerű script még egy sort klónoz. A layout processzor aztán újra beleárasztja a tartalmat az oldalterületekbe, ami adhat hozzá oldalt, vethet el oldalt, vagy tolhat meglévő mezőket másik oldalra. Az ISO 32000-1 §12.7.8-a csak azt definiálja, hogyan utaznak az XFA csomagok a PDF-en belül; minden, ami utána történik, az XFA enginhez tartozik, ami a PDFium Componentban a PDFium saját XFA layoutja, a host folyamatban futva. Egy Delphi nézegető tehát olyan dokumentummal dolgozik, aminek az oldalszáma, oldalméretei és widgetpozíciói mind élő állapotok. Három dolog romlik el, ha a host mást tételez fel:
- A navigációra, görgetési tartományokra és oldal-spinnerrekre cache-elt oldalszám elavul, vagy rosszabb, rossz számmal frissül
- Az átköltöző mezők a keretüket az új pozícióban mutatják, miközben a szerkesztő meg az egér találati területe a régi koordinátákon marad
- A nézegető olyan oldalhandlet tart, amit a layout kicserélt, így a kattintások és rajzolások olyan oldalra mennek, ami az adott űrlapban már nem létezik
A szerkesztett sorok mentéskor és újranyitáskor való megőrzése külön probléma a maga szabályaival; ez a cikk annál marad, ami futásidőben a nézegetőn belül történik
Melyik PDFium runtime kell a dinamikus XFA-hoz?
A dinamikus XFA a PDFium Componentban a natív library V8/XFA buildjét igényli, amit a PDFium unit globális EnableV8Engine változója választ ki az első dokumentumbetöltés előtt. A folyamat először, amikor bármely TPdf betölti a libraryt, elköteleződik egyetlen DLL-re, és egy sima PDFium build egyáltalán nem tudja futtatni az XFA engint. Dokumentum megnyitásakor a TPdf beleszimatol a fájlba XFA jelekért, és automatikusan a V8 buildre vált, de csak, ha még nem töltődött sima library abban a folyamatban. Ha az elköteleződés már rossz irányba ment, a TPdf.OnXfaRuntimeMissing egyszer elsül, hogy a host megmondja a felhasználónak, indítsa újra. A flag explicit beállítása indításkor eltünteti a találgatást. Az XFA eseményeket hordozó FPDF_FORMFILLINFO callback struktúrának is egyeznie kell a DLL-lel; a háttér a FPDF_FORMFILLINFO 2-es verzió és az XFA callback ABI cikkben van, az űrlaptípusok megkülönböztetését nézegető nyitása előtt pedig a XFA űrlapok felismerése és csomagjaik kiolvasása fedi le
uses
PDFium;
procedure TClaimForm.FormCreate(Sender: TObject);
begin
// Dönts el, mielőtt az első TPdf betölti a natív libraryt:
// a folyamat később nem válthat a pdfium.dll-ről pdfium.v8.dll-re
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;
Miért jelentett a PageCount 1-et egy kétoldalas űrlapra?
v3.126.1 előtt a PDFium Component a natív oldal-esemény page_count argumentumát tárolta dokumentumösszegként, és az az argumentum valójában az új és régi oldalszám abszolút különbsége. A PDFium FFI_PageEvent-et dob, amikor egy layout menet befejeződik hozzáadott vagy elvett oldal eseménytípussal; belül előbb frissíti a tárolt oldalszámát, aztán abs(new - old)-ot ad át. A kezdeti layoutnál a régi szám nulla, tehát a delta egyenlő az össeggel, és egy háromoldalas statikus minta a várt módon három oldalt jelent. Pontosan ezért nem leplezte le soha a hibát a fix hosszúságú tesztűrlap. Amikor egy dinamikus űrlap először nő egy oldalról kettőre, a delta 1, és a wrapper a TPdf.PageCount-ot és az OnXfaPageCountChanged NewCount paraméterét is 1-re állította. Egy sor törlése egy háromoldalas űrlapból ugyanolyan jellegű butaságot adott a másik irányban
A delta előző értékre halmozása sem biztonságos javítás. Az inicializálási és layout callbackek sorrendje miatt a wrapper nem mindig bízhat a korábbi számában alapértékként, így egy futó összeg sodródhat. v3.126.1 óta a callback az argumentumot számként ignorálja, és FPDF_GetPageCount-ot hív a dokumentumra, ami az épp befejeződött layoutból olvassa az összeget. Aztán törli a cache-elt oldaljeleneteket, azt az összeget tárolja XFA oldalszám-felülírásként a TPdf.PageCount mögött, és csak ezután dobja az OnXfaPageCountChanged-et. Mire a handlered fut, a NewCount és a FPdf.PageCount egyetért
procedure TClaimForm.PdfXfaPageCountChanged(Sender: TObject; NewCount: Integer);
begin
// v3.126.1+: a NewCount a befejezett layout összege, soha nem delta.
// Ez PDFium layout callbackjén belül fut: csak host UI állapotot frissíts,
// ne zárd be innen a dokumentumot és ne tölts újra oldalakat
UpdatePageRange(NewCount);
end;
procedure TClaimForm.PdfViewPageChange(Sender: TObject);
begin
// Minden oldal-újratöltés után elsül, a késleltetett XFA frissítést is beleértve
PageSpin.Value := PdfView1.PageNumber;
end;
procedure TClaimForm.UpdatePageRange(Count: Integer);
begin
PageSpin.MinValue := 1;
PageSpin.MaxValue := Count;
PageLabel.Caption := Format('of %d', [Count]);
end;
Az esemény csak olyan Full XFA űrlapokon sül el, amiknek a layoutja futásidőben változik. A statikus XFA és AcroForm dokumentumok soha nem dobják, tehát egy mindkettőt kezelő nézegető meghagyhatja ugyanazt a handlert hozzárendelve. Hozzárendelés nélkül is biztonságos; a TPdf.PageCount mögötti felülírás attól függetlenül él, és az esemény azért van, hogy a host frissítse, amit cache-elt
Miért marad a beviteli doboz a régi oldalon, amikor egy mező költözik?
A keret mozgott, a szerkesztő nem, mert a natív XFA értesítő egy téglalapot vetett össze önmagával. Amikor a layout egy már betöltött widget geometriáját változtatja meg, a PDFiumnak észlelnie kellene az új téglalapot, és PerformLayout-ot hívnia a widgeten, ami áthelyezi a szövegszerkesztőt meg a találati területét. Az összehasonlítás a GetWidgetRect()-et vette a RecacheWidgetRect()-cel. Mindkét függvény ugyanannak a membernek a const referenciáját adja vissza, és az újracache-elés helyben felülírja azt a membert, tehát az összehasonlítás mindig két azonos értéket látott, és a betöltött widgetek kihagyták az újra-layoutjukat
A tünet akkor bukott felszínre, amikor egy teszt megváltoztatott egy subform magasságát úgy, hogy meglévő mezők átkúsztak a következő oldalra. Mindkét V8 architektúrán a mezőkeret az új pozíciójában rajzolódott, miközben a beütött szöveg meg az egér találati területe az előző Y koordinátán maradt. Az explicit újra-layout nem javította, az oldal újratöltése sem, mert a widget továbbra is azt hitte, a geometriája aktuális. A v3.126.1-gyel szállított Windows V8 libraryk érték szerint lemásolják a régi téglalapot, mielőtt újracache-elnék, és azt a másolatot hasonlítják, így az átköltözött widgetek újra-layoutolódnak, és a szerkesztett érték pontosan ott jelenik meg, ahol a keret van. Ez natív javítás: a DLL-ekkel utazik, tehát a Pascal unitok frissítése öregebb pdfium.v8.dll megtartása mellett a rossz helyen lévő találati területeket a helyükön hagyja. A mögötte álló regressziós ellenőrzés előbb egy megmaradt sort nem-default értékre szerkeszt, aztán megköveteli azt az értéket a mező új helyén, mert egy default értékekkel újjáépített sor egyébként átmenőnek nézne ki
Hogyan tölt újra oldalakat a TPdfView anélkül, hogy kihúzná a handlet PDFium alól?
v3.126.2 óta a TPdfView elhalasztja azt az oldal-újratöltést, ami egy XFA layout váltást követ, amíg a natív hívásstack vissza nem gomolyodik. Az oldal-esemény általában akkor sül el, miközben a PDFium még a bemenetet dolgozza: a user rákattintott egy Add Row gombra, a kattintás lefuttatott egy scriptet, a script megváltoztatta az instanciaszámot, és a layout ugyanazon natív híváson belül fejeződött be. Az oldalhandle bezárása meg újranyitása ebben a pillanatban felszabadítana egy objektumot, amit a hívó még használ. v3.126.2 előtt a nézegető csak érvénytelenítette önmagát, így a megjelenített oldalhandle mutathatott layout előtti állapotra, és ha a user az utolsó oldalon volt, amikor az eltűnt, a kiválasztott oldalszám a tartományon kívülre esett
A késleltetett frissítés néhány kicsi lépésben működik, és azok megmagyarázzák, amit a hostból látsz:
- Az oldal-esemény callback pending XFA layout frissítésként jelöli a nézetet, és privát ablaküzenetet postáz; az üzenet megérkezése előtti ismétlődő események egyetlen frissítésbe olvadnak
- Egy még ablakhandle nélküli nézet megtartja a pending flaget, és az üzenetet a
CreateWnd-ből postázza, míg a dokumentumváltás, a nézet deaktiválása vagy megsemmisítése törli a flaget - Amikor az üzenet megérkezik, a nézet törli a szövegkijelölést, a keresési kiemelést meg a fókuszált mező indexét, mert mindhárom a régi layoutra utalt
- A kiválasztott oldal az új
PageCount-ra szoríttatik; megváltozott oldalszám a rendes oldalváltáson megy át, egyébként az aktuális oldal töltődik újra, és az illesztési mód újra alkalmazódik - Ha a layout egyáltalán nem hagy oldalt, a nézet kirakja a régi oldalhandlejét, ahelyett hogy nem létező oldalt rajzolna
Ugyanez a megszorítás a saját kódodra is érvényes. Az OnXfaPageCountChanged azon a natív layout callbacken belül fut, tehát kezeld értesítésként: ott frissíts címkéket, spinner tartományokat és toolbar állapotot, és minden nehezebbet, mint a dokumentum bezárása vagy másik megnyitása, sorolj be postázott üzenettel, hogy az callback visszatérése után fusson. A TPdfView.OnPageChange aztán megmondja, mikor töltötte be a nézet ténylegesen az oldalt, és ezen a ponton a PdfView1.PageNumber olvasása a szorított értéket adja. A Tab-billenés és azok a FormType ellenőrzések, amiket egy űrlapnézegető nyitáskor futtat, a PDF űrlapmező-navigáció a PDFium Componenttal cikkben vannak
Miért dob „Cannot open text page"-t egy Full XFA mezőbe kattintás?
A Full XFA oldalaknak nincs PDF szövegoldala, és v3.126.2 előtt a nézegető alapértelmezett szövegkijelölése meg linkfelismerése mindenképp meg akart tölteni egyet. A TPdfView.AllowUserTextSelection az alapértelmezett True-ján az rámutatás megkérdezte a szövegréteget karakterért az egér alatt, és egy felengedett egérgomb automatikus URL-próbát futtatott az oldalszövegen. Full XFA oldalon a szövegoldal nem nyitható meg, tehát egy rendes kattintás egy mezőbe Cannot open text page kivételbe futhatott. v3.126.2 óta mindkét belső út eredmény nélkül tér vissza, amikor a TPdf.FormType ftXfaFull, és az XFA runtime elérhető, így az alapértelmezett beállítások működnek, és a mezőbemenet elérhető marad
A AllowUserTextSelection kikapcsolása Full XFA dokumentumokra továbbra is ésszerű UI választás, mert nincs oldalszöveg, amit kijelölj, és a húzó mozdulatoknak nem kéne kijelölési módot indítaniuk. Nem helyettesíti a frissítést viszont: korábbi verziókon a kattintásra futó URL-próba nem függött attól a property-től, tehát egy nézegető kikapcsolt kijelöléssel is belefuthatott ugyanabba a kivételbe
procedure TClaimForm.ConfigureViewerForForm;
begin
// A FormType a nyitott dokumentumot olvassa, ezért FPdf.Active := True után hívd
if FPdf.XFA and (FPdf.FormType = ftXfaFull) and FPdf.XfaRuntimeAvailable then
begin
// Full XFA oldalakon nincs PDF szövegréteg; a mezők szerkeszthetők maradnak
PdfView1.AllowUserTextSelection := False;
StatusBar1.SimpleText := Format('Dynamic XFA form, %d page(s)',
[FPdf.PageCount]);
end
else
PdfView1.AllowUserTextSelection := True;
end;
A gépelésnek megvolt a maga javítása a v3.126.2-ben. A natív XFA szövegszerkesztő nem cseréli le a kijelölést, amikor karaktert kap: a FORM_OnChar a kurzornál szúr be, a Backspace egyetlen karaktert töröl, tehát egy érték kijelölése és átgépelése régi meg új szöveget tett egymás mellé. A PDFium Component mostantól megjegyzi, hogy a kattintás XFA szövegmezőre ért, és a beütött karaktereket, Backspace-t és Delete-t FORM_ReplaceSelection-en át irányítja, valahányszor kijelölés van, és a dokumentum űrlapkitöltési vagy módosítási engedélyt ad. Hogy egy csak olvasható XFA mező változhat-e, továbbra is a natív szerkesztő dönti el, tehát az űrlapban csak olvashatónak jelölt mező megtartja az értékét akkor is, ha a dokumentum amúgy engedi a kitöltést. A TPdfView.AllowFormEvents False-ra állítása szintén leállítja ezt a billentyű-útirányítást, ami egy csak olvasható nézegetőt csak olvashatóként tart
Gyorsreferencia: dinamikus XFA egy Delphi nézegetőben
| Tünet | Ok | Javítva |
|---|---|---|
| Az oldalszám 1-et mutat, miután az űrlap két oldalra nő | A natív oldal-esemény hozzáadott/elvett deltát ad át, nem összeget | v3.126.1 (wrapper) |
| A mezőkeret mozog, a beütött szöveg meg a találati terület lemarad | A betöltött widget kihagyta az újra-layoutot egy önmagával való összehasonlítás után | v3.126.1 (Windows V8 libraryk) |
| A nézegető layout előtti oldalállapotra rajzol vagy irányít bemenetet | Az oldalhandle nem töltődött újra újrapaginálás után | v3.126.2 (késleltetett frissítés) |
| Mezőbe kattintás Cannot open text page kivételt ad | Szövegkijelölés és URL-próba szövegréteg nélküli oldalakon | v3.126.2 |
| Kijelölt érték fölé gépelve hozzáfűz, cserélés helyett | A natív XFA szerkesztő a kurzornál szúr be | v3.126.2 |
- Állítsd az
EnableV8Engine-tTrue-ra, mielőtt bármely dokumentum betöltődik, és kezeld azOnXfaRuntimeMissing-et arra az esetre, ha a sima library töltődött be előbb - Az összeget a
TPdf.PageCount-ból vagy azOnXfaPageCountChangedNewCountparaméteréből olvasd; soha ne adj vagy vonj le magad oldalszámokat - Tartsd könnyűnek az
OnXfaPageCountChangedhandlert, mert az natív layout callbacken belül fut - Szinkronizáld az aktuális oldalkijelzőt a
TPdfView.OnPageChange-ben, ami azután sül el, hogy a késleltetett újratöltés szorította az oldalszámot - Deployold együtt a v3.126.1-es vagy újabb Windows V8 DLL-eket a unitokkal; a widget újra-layout javítás natív kódban él
- Tesztelj olyan űrlappal, ami ténylegesen változtatja az oldalszámát, és átköltöztet egy szerkesztett mezőt oldaltörésen át, mert a fix hosszúságú minták minden hibát elrejtenek erről a listáról
A dinamikus XFA az oldalszámot meg a mezőgeometriát élő értékekké teszi, és egy nézegető csak akkor marad helyes, ha azokat a befejezett layoutból veszi, és biztonságos pillanatban tölti újra az oldalakat. A PDFium Component mindkettőt kezeli a TPdf-ben és a TPdfView-ben, tehát a hostnak csak figyelnie kell. Részletek és letöltések a PDFium Component for Delphi termékoldalon