Când un formular dynamic XFA într-un viewer Delphi adaugă sau elimină pagini, PDFium Component raportează noul total prin TPdf.PageCount și TPdf.OnXfaPageCountChanged din v3.126.1, fiindcă evenimentul nativ de pagină poartă un delta de adăugat/eliminat, nu un total. Bibliotecile V8 Windows din v3.126.1 mută de asemenea zonele de hit ale input-ului odată cu field-urile realocate, iar v3.126.2 reîncarcă handle-urile de pagină perimate după ce callback-ul de layout se întoarce. Raportul de bug care a pornit totul a fost un formular de decont de cheltuieli: apăsați Add Row de două ori, formularul crește la două pagini, iar indicatorul de pagină afișează mândru 1 din 1. Tastați într-un field mutat pe pagina 2 și apăsările de taste aterizează undeva invizibil. Nimic din toate astea nu apărea cu formularele de probă cu lungime fixă pe care toată lumea le testează primele, iar motivele merită știute dacă embeddedați un viewer de formulare
Ce se întâmplă când un formular dynamic XFA se repaginează?
Un formular dynamic XFA nu are o listă fixă de pagini, deci numărul lui de pagini e un rezultat al layout-ului și se poate schimba de fiecare dată când utilizatorul editează date. XFA 3.3 descrie formularul ca un arbore de subforms; un subform repetabil e controlat de un instanceManager, iar un script precum _Row.addInstance() clonează încă un rând. Procesorul de layout fluxează apoi conținutul din nou în ariile de pagină, ceea ce poate adăuga o pagină, poate elimina una sau poate împinge field-uri existente pe altă pagină. ISO 32000-1 §12.7.8 definește doar cum călătoresc pachetele XFA în interiorul PDF-ului; tot ce se întâmplă după aparține motorului XFA, care în PDFium Component e propriul layout XFA al PDFium rulând în procesul host. Un viewer Delphi are deci de-a face cu un document al cărui număr de pagini, dimensiuni de pagini și poziții de widget-uri sunt toate stare vie. Trei lucruri ies prost când host-ul presupune altfel:
- Numărul de pagini pe care host-ul îl pune în cache pentru navigare, intervale de scroll și indicatoare de pagină perimează, sau mai rău, primește un update cu numărul greșit
- Field-urile realocate își arată bordura în poziția nouă în timp ce editorul și zona de hit a mouse-ului rămân la coordonatele vechi
- Viewer-ul păstrează un handle de pagină pe care layout-ul l-a înlocuit, deci click-urile și desenările merg spre o pagină care nu mai există în formularul acela
Persistarea editărilor de rânduri peste salvare și redeschidere e o problemă separată cu regulile ei proprii; articolul ăsta rămâne la ce se întâmplă la runtime în interiorul viewer-ului
Ce runtime PDFium are nevoie dynamic XFA?
Dynamic XFA în PDFium Component cere build-ul V8/XFA al bibliotecii native, selectat de variabila globală EnableV8Engine din unit-ul PDFium înainte ca primul document să se încarce. Procesul se angajează pe un singur DLL prima dată când orice TPdf încarcă biblioteca, iar un build PDFium simplu nu poate rula deloc motorul XFA. Când un document se deschide, TPdf aruncă totuși o privire peste fișier după markeri XFA și comută automat pe build-ul V8, dar doar dacă nicio bibliotecă simplă nu a fost încărcată încă în procesul acela. Când angajamentul a mers deja greșit, TPdf.OnXfaRuntimeMissing se declanșează o singură dată, ca host-ul să poată spune utilizatorului să repornească. Setarea explicită a flag-ului la pornire elimină ghicitorul. Structura de callback FPDF_FORMFILLINFO care poartă evenimentele XFA trebuie să se potrivească și ea cu DLL-ul; fundalul e în FPDF_FORMFILLINFO versiunea 2 și ABI-ul de callback XFA, iar detectarea formularelor XFA și citirea pachetelor lor acoperă deosebirea între tipurile de formulare înainte să deschideți un viewer
uses
PDFium;
procedure TClaimForm.FormCreate(Sender: TObject);
begin
// Decide înainte ca primul TPdf să încarce biblioteca nativă:
// procesul nu poate comuta de la pdfium.dll la pdfium.v8.dll mai târziu
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;
De ce raporta PageCount 1 pentru un formular cu două pagini?
Înainte de v3.126.1, PDFium Component stoca argumentul page_count al evenimentului nativ de pagină drept totalul documentului, iar argumentul acela e de fapt diferența absolută dintre noul și vechiul număr de pagini. PDFium ridică FFI_PageEvent după ce o trecere de layout se termină, cu un tip de eveniment pagină adăugată sau pagină eliminată; intern își actualizează întâi numărul de pagini stocat și apoi pasează abs(new - old). La layout-ul inițial numărul vechi e zero, deci delta egalează totalul, iar un eșantion static de trei pagini raportează trei pagini cum se așteaptă toată lumea. Exact de aceea formularele de test cu lungime fixă nu au dezvăluit niciodată bug-ul. Prima dată când un formular dinamic crește de la o pagină la două, delta e 1, iar wrapper-ul seta și TPdf.PageCount, și parametrul NewCount al lui OnXfaPageCountChanged pe 1. Eliminarea unui rând dintr-un formular cu trei pagini producea același tip de nonsens în direcția cealaltă
Adunarea delta-ului peste valoarea anterioară nu e nici ea o reparare sigură. Ordinea callback-urilor de inițializare și de layout înseamnă că wrapper-ul nu-și poate avea mereu încredere în numărul anterior ca bază, deci o sumă rulantă poate aluneca. Din v3.126.1, callback-ul ignoră argumentul ca număr și apelează FPDF_GetPageCount pe document, care citește totalul din layout-ul tocmai completat. Apoi golește scenele de pagină puse în cache, stochează totalul acela drept override de număr de pagini XFA în spatele lui TPdf.PageCount, și abia apoi ridică OnXfaPageCountChanged. Până când rulează handler-ul vostru, NewCount și FPdf.PageCount sunt de acord
procedure TClaimForm.PdfXfaPageCountChanged(Sender: TObject; NewCount: Integer);
begin
// v3.126.1+: NewCount e totalul layout-ului completat, niciodată un delta.
// Asta rulează în callback-ul de layout al PDFium: actualizați doar starea UI a host-ului,
// nu închideți documentul și nu reîncărcați pagini de aici
UpdatePageRange(NewCount);
end;
procedure TClaimForm.PdfViewPageChange(Sender: TObject);
begin
// Se declanșează după fiecare reîncărcare de pagină, inclusiv refresh-ul XFA amânat
PageSpin.Value := PdfView1.PageNumber;
end;
procedure TClaimForm.UpdatePageRange(Count: Integer);
begin
PageSpin.MinValue := 1;
PageSpin.MaxValue := Count;
PageLabel.Caption := Format('of %d', [Count]);
end;
Evenimentul se declanșează doar pentru formulare Full XFA al căror layout se schimbă la runtime. Documentele Static XFA și AcroForm nu-l ridică niciodată, deci un viewer care le gestionează pe amândouă poate lăsa același handler asignat. A-l lăsa neasignat e la fel de sigur; override-ul din spatele lui TPdf.PageCount se aplică oricum, iar evenimentul există ca host-ul să poată împrospăta ce a pus în cache
De ce rămâne caseta de input pe pagina veche când un field se mută?
Bordura s-a mutat, iar editorul nu, pentru că notifier-ul nativ XFA compara un dreptunghi cu el însuși. Când layout-ul schimbă geometria unui widget deja încărcat, PDFium ar trebui să observe dreptunghiul nou și să apeleze PerformLayout pe widget, care repoziționează editorul de text și zona lui de hit. Verificarea compara GetWidgetRect() cu RecacheWidgetRect(). Ambele funcții întorc o referință const spre același membru, iar recache-ul suprascrie acel membru pe loc, deci comparația vedea întotdeauna două valori identice, iar widget-urile încărcate își sareau peste re-layout
Simptomul a apărut când un test a schimbat înălțimea unui subform astfel încât field-uri existente au trecut pe pagina următoare. Pe ambele arhitecturi V8, bordura field-ului era desenată la poziția nouă, în timp ce textul tastat și zona de hit a mouse-ului rămâneau la coordonata Y anterioară. Un re-layout explicit nu repara nimic, iar nici reîncărcarea paginii, pentru că widget-ul credea în continuare că geometria lui era curentă. Bibliotecile V8 Windows livrate cu v3.126.1 copiază dreptunghiul vechi prin valoare înainte de recache și compară copia aceea, deci widget-urile mutate își fac re-layout, iar valoarea editată apare exact unde e bordura. E o reparare nativă: călătorește odată cu DLL-urile, deci actualizarea unit-urilor Pascal păstrând un pdfium.v8.dll mai vechi lasă zonele de hit dezalocate la locul lor. Verificarea de regresie care a condus repararea editează un rând supraviețuitor la o valoare non-implicită întâi și apoi cere acea valoare la noua locație a field-ului, fiindcă un rând reconstruit cu valori implicite ar părea altfel o trecere a testului
Cum reîncarcă TPdfView paginile fără să tragă un handle de sub PDFium?
Din v3.126.2, TPdfView amână reîncărcarea paginii care urmează unei schimbări de layout XFA până când stiva de apeluri native s-a destins. Evenimentul de pagină se declanșează de obicei în timp ce PDFium procesează încă input-ul: utilizatorul a apăsat un buton Add Row, click-ul a rulat un script, script-ul a schimbat numărul de instanțe, iar layout-ul s-a terminat în interiorul aceluiași apel nativ. Închiderea și redeschiderea handle-ului de pagină în clipa aceea ar elibera un obiect pe care apelantul îl folosește încă. Înainte de v3.126.2, viewer-ul doar se invalida pe sine, deci handle-ul de pagină afișat putea continua să pointeze spre starea de dinainte de layout, iar dacă utilizatorul fusese pe ultima pagină când ea a dispărut, numărul de pagină selectat era în afara intervalului
Refresh-ul amânat funcționează în câțiva pași mici, iar ei explică comportamentul pe care îl vedeți din host:
- Callback-ul de eveniment de pagină marchează view-ul ca având un refresh de layout XFA în așteptare și postează un mesaj de fereastră privat; evenimentele repetate înainte ca mesajul să sosească se fuzionează într-un singur refresh
- Un view fără handle de fereastră încă păstrează flag-ul de așteptare și postează mesajul din
CreateWnd, în timp ce schimbarea documentului, dezactivarea view-ului sau distrugerea lui golește flag-ul - Când mesajul sosește, view-ul golește selecția de text, highlight-ul de căutare și index-ul field-ului cu focus, fiindcă toate trei se refereau la vechiul layout
- Pagina selectată e fixată în limitele noului
PageCount; un număr de pagină schimbat trece prin comutarea normală de pagină, altfel pagina curentă e reîncărcată, iar modul de potrivire se aplică din nou - Dacă layout-ul nu lasă nicio pagină, view-ul descarcă vechiul lui handle de pagină în loc să deseneze o pagină care nu mai există
Aceeași constrângere se aplică codului vostru propriu. OnXfaPageCountChanged rulează în interiorul callback-ului nativ de layout, deci tratați-l ca o notificare: actualizați acolo etichete, intervale de spinner și starea toolbar-ului, și puneți la coadă orice mai greu, precum închiderea documentului sau deschiderea altuia, cu un mesaj postat, ca să ruleze după ce callback-ul se întoarce. TPdfView.OnPageChange vă spune apoi când view-ul a reîncărcat efectiv pagina, iar citirea PdfView1.PageNumber în clipa aceea vă dă valoarea fixată în limite. Traversarea cu tasta Tab și verificările FormType pe care un viewer de formulare le rulează la deschidere sunt acoperite în navigarea prin form field PDF cu PDFium Component
De ce click-ul pe un field Full XFA ridică „Cannot open text page”?
Paginile Full XFA nu au pagină de text PDF, iar înainte de v3.126.2 selecția de text implicită a viewer-ului și detecția de link-uri încercau oricum să încarce una. Cu TPdfView.AllowUserTextSelection la implicitul lui True, hover-ul cerea stratului de text un caracter sub mouse, iar un click de mouse-up rula o sondă URL automată peste textul paginii. Pe o pagină Full XFA pagina de text nu poate fi deschisă, deci un click obișnuit într-un field se putea termina cu o excepție Cannot open text page. Din v3.126.2, ambele căi interne întorc niciun rezultat când TPdf.FormType e ftXfaFull iar runtime-ul XFA e disponibil, deci setările implicite funcționează, iar input-ul în field rămâne disponibil
Oprirea lui AllowUserTextSelection pentru documentele Full XFA rămâne o alegere UI rezonabilă, fiindcă nu există text de pagină de selectat, iar gesturile de drag nu ar trebui să pornească un mod de selecție. Nu e însă un substitut pentru upgrade: pe versiunile mai vechi sonda URL la click nu depindea de proprietatea aceea, deci un viewer putea lovi aceeași excepție și cu selecția dezactivată
procedure TClaimForm.ConfigureViewerForForm;
begin
// FormType citește documentul deschis, deci apelați asta după FPdf.Active := True
if FPdf.XFA and (FPdf.FormType = ftXfaFull) and FPdf.XfaRuntimeAvailable then
begin
// Nu există strat de text PDF pe paginile Full XFA; field-urile rămân editabile
PdfView1.AllowUserTextSelection := False;
StatusBar1.SimpleText := Format('Dynamic XFA form, %d page(s)',
[FPdf.PageCount]);
end
else
PdfView1.AllowUserTextSelection := True;
end;
Tastatura a avut nevoie de propria reparare în v3.126.2. Editorul de text nativ XFA nu înlocuiește o selecție când primește un caracter: FORM_OnChar inserează la caret, iar Backspace șterge un singur caracter, deci selectarea unei valori și tastarea peste ea producea text vechi și nou unul lângă altul. PDFium Component își amintește acum că click-ul a aterizat pe un field de text XFA și dirijează caracterele tastate, Backspace și Delete prin FORM_ReplaceSelection de fiecare dată când există o selecție, iar documentul acordă permisiunea de completare de formulare sau de modificare. Dacă un field XFA read-only se poate schimba e tot decis de editorul nativ, deci un field marcat read-only în formular își păstrează valoarea chiar și într-un document care altfel permite completarea. Setarea TPdfView.AllowFormEvents pe False oprește și ea dirijarea asta de tastatură, ceea ce ține un viewer read-only read-only
Referință rapidă: dynamic XFA într-un viewer Delphi
| Simptom | Cauză | Reparat în |
|---|---|---|
| Numărul de pagini arată 1 după ce formularul crește la două pagini | Evenimentul nativ de pagină pasează un delta de adăugat/eliminat, nu un total | v3.126.1 (wrapper) |
| Bordura field-ului se mută, textul tastat și zona de hit rămân în urmă | Widget-ul încărcat își sărea peste re-layout după o auto-comparație | v3.126.1 (Windows V8 libraries) |
| Viewer-ul desenează sau dirijează input spre starea de pagină de dinainte de layout | Handle-ul de pagină nereîncărcat după repaginare | v3.126.2 (deferred refresh) |
| Click-ul într-un field ridică Cannot open text page | Selecție de text și sondă URL pe pagini fără strat de text | v3.126.2 |
| Tastarea peste o valoare selectată adaugă în loc să înlocuiască | Editorul nativ XFA inserează la caret | v3.126.2 |
- Setați
EnableV8EnginepeTrueînainte ca orice document să se încarce, și tratațiOnXfaRuntimeMissingpentru cazul în care biblioteca simplă a fost încărcată prima - Citiți totalul din
TPdf.PageCountsau din parametrulNewCountal luiOnXfaPageCountChanged; nu adunați și nu scădeți niciodată numere de pagini pe cont propriu - Țineți handler-ul
OnXfaPageCountChangedușor, fiindcă rulează în interiorul callback-ului nativ de layout - Sincronizați indicatorul de pagină curentă în
TPdfView.OnPageChange, care se declanșează după ce reîncărcarea amânată fixează numărul de pagină în limite - Livrați DLL-urile Windows V8 v3.126.1 sau mai noi odată cu unit-urile; repararea re-layout-ului de widget locuiește în cod nativ
- Testați cu un formular care chiar își schimbă numărul de pagini și mută un field editat peste o ruptură de pagină, fiindcă eșantioanele cu lungime fixă ascund fiecare bug de pe lista asta
Dynamic XFA transformă numărul de pagini și geometria field-urilor în valori vii, iar un viewer rămâne corect doar când le ia din layout-ul completat și reîncarcă paginile într-un moment sigur. PDFium Component le gestionează pe amândouă în interiorul lui TPdf și TPdfView, deci host-ul are doar de ascultat. Detalii și descărcări sunt pe pagina de produs PDFium Component pentru Delphi