Quando un form XFA dinamico in un viewer Delphi aggiunge o toglie pagine, PDFium Component riporta il nuovo totale tramite TPdf.PageCount e TPdf.OnXfaPageCountChanged dalla v3.126.1, perché l'evento pagina nativo porta un delta aggiunte/rimozioni anziché un totale. Le librerie V8 Windows nella v3.126.1 spostano anche le aree di hit degli input insieme ai campi rilocati, e la v3.126.2 ricarica gli handle di pagina stanti dopo che il callback di layout è tornato. La segnalazione che ha fatto partire tutto era un modulo di richiesta spese: clic su Add Row due volte, il form cresce fino a due pagine, e l'indicatore di pagina legge fiero 1 di 1. Digiti in un campo che si è spostato a pagina 2 e i tasti atterrano chissà dove, invisibili. Niente di tutto questo compariva con i form campione a lunghezza fissa con cui tutti provano per primi, e i motivi valgono la pena di essere conosciuti se incorpori un viewer di form
Che cosa succede quando un form XFA dinamico si ripagina?
Un form XFA dinamico non ha una lista di pagine fissa, quindi il suo conteggio pagine è un output del layout e può cambiare ogni volta che l'utente modifica i dati. XFA 3.3 descrive il form come un albero di subform; un subform ripetuto è governato da un instanceManager, e uno script come _Row.addInstance() clona un'altra riga. Il processore di layout poi riversa di nuovo il contenuto nelle aree di pagina, il che può aggiungere una pagina, toglierne una o spingere campi esistenti su un'altra pagina. ISO 32000-1 §12.7.8 definisce solo come i pacchetti XFA viaggiano dentro il PDF; tutto ciò che succede dopo appartiene al motore XFA, che in PDFium Component è il layout XFA di PDFium stesso in esecuzione nel processo host. Un viewer Delphi quindi ha a che fare con un documento il cui conteggio pagine, le cui dimensioni di pagina e le cui posizioni dei widget sono tutti stato vivo. Tre cose vanno storte quando l'host presume il contrario:
- Il conteggio pagine che l'host tiene in cache per navigazione, intervalli di scroll e spinner di pagina diventa stantio, o peggio, viene aggiornato con il numero sbagliato
- I campi che si rilocano mostrano il bordo nella nuova posizione mentre l'editor e l'area di hit del mouse restano alle vecchie coordinate
- Il viewer conserva un handle di pagina che il layout ha sostituito, così clic e disegnate vanno a una pagina che in quel form non esiste più
Fare sopravvivere le modifiche delle righe attraverso salvataggio e riapertura è un problema a parte con le sue regole; questo articolo resta su ciò che succede a runtime dentro il viewer
Quale runtime PDFium serve a XFA dinamico?
XFA dinamico in PDFium Component richiede la build V8/XFA della libreria nativa, selezionata dalla variabile globale EnableV8Engine nell'unità PDFium prima che il primo documento si carichi. Il processo si impegna su una DLL la prima volta che un qualsiasi TPdf carica la libreria, e una build PDFium semplice non può far girare per niente il motore XFA. Quando un documento si apre, TPdf sbircia nel file in cerca di marker XFA e passa alla build V8 da solo, ma solo se nel processo non è ancora stata caricata alcuna libreria semplice. Quando l'impegno è già andato nel verso sbagliato, TPdf.OnXfaRuntimeMissing scatta una volta sola così l'host può dire all'utente di riavviare. Impostare il flag esplicitamente all'avvio toglie ogni congettura. Anche la struttura di callback FPDF_FORMFILLINFO che porta gli eventi XFA deve combaciare con la DLL; il contesto è in FPDF_FORMFILLINFO versione 2 e l'ABI dei callback XFA, e rilevare i form XFA e leggere i loro pacchetti copre come distinguere i tipi di form prima di aprire un viewer
uses
PDFium;
procedure TClaimForm.FormCreate(Sender: TObject);
begin
// Decidi prima che il primo TPdf carichi la libreria nativa:
// il processo non può passare da pdfium.dll a pdfium.v8.dll più tardi
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;
Perché PageCount riportava 1 per un form di due pagine?
Prima della v3.126.1, PDFium Component memorizzava l'argomento page_count dell'evento pagina nativo come totale del documento, e quell'argomento in realtà è la differenza assoluta tra il nuovo e il vecchio conteggio pagine. PDFium solleva FFI_PageEvent dopo che un passaggio di layout è finito, con un tipo di evento pagina aggiunta o pagina rimossa; internamente aggiorna prima il proprio conteggio pagine memorizzato e poi passa abs(new - old). Sul layout iniziale il vecchio conteggio è zero, quindi il delta eguaglia il totale, e un campione statico di tre pagine riporta tre pagine come previsto. È esattamente per questo che i form di test a lunghezza fissa non hanno mai smascherato il bug. La prima volta che un form dinamico cresce da una pagina a due, il delta è 1, e il wrapper impostava sia TPdf.PageCount sia il parametro NewCount di OnXfaPageCountChanged a 1. Togliere una riga a un form di tre pagine produceva lo stesso tipo di assurdo nell'altra direzione
Accumulare il delta sul valore precedente non è una riparazione sicura neppure quella. L'ordine dei callback di inizializzazione e layout fa sì che il wrapper non possa sempre fidarsi del proprio conteggio precedente come baseline, quindi una somma progressiva può divergere. Dalla v3.126.1, il callback ignora l'argomento come conteggio e chiama FPDF_GetPageCount sul documento, che legge il totale dal layout appena completato. Poi pulisce le scene di pagina in cache, memorizza quel totale come override del conteggio pagine XFA dietro a TPdf.PageCount, e solo dopo solleva OnXfaPageCountChanged. Per il momento in cui il tuo handler gira, NewCount e FPdf.PageCount sono d'accordo
procedure TClaimForm.PdfXfaPageCountChanged(Sender: TObject; NewCount: Integer);
begin
// v3.126.1+: NewCount è il totale del layout completato, mai un delta.
// Questo gira dentro il callback di layout di PDFium: aggiorna solo lo stato
// UI dell'host, non chiudere il documento né ricaricare pagine da qui
UpdatePageRange(NewCount);
end;
procedure TClaimForm.PdfViewPageChange(Sender: TObject);
begin
// Scatta dopo ogni ricaricamento di pagina, compreso il refresh XFA differito
PageSpin.Value := PdfView1.PageNumber;
end;
procedure TClaimForm.UpdatePageRange(Count: Integer);
begin
PageSpin.MinValue := 1;
PageSpin.MaxValue := Count;
PageLabel.Caption := Format('of %d', [Count]);
end;
L'evento scatta solo per i form Full XFA il cui layout cambia a runtime. I documenti Static XFA e AcroForm non lo sollevano mai, quindi un viewer che gestisce entrambi può lasciare assegnato lo stesso handler. Lasciarlo non assegnato è sicuro a sua volta; l'override dietro a TPdf.PageCount viene applicato comunque, e l'evento esiste perché l'host possa rinfrescare ciò che aveva in cache
Perché la casella di input resta sulla vecchia pagina quando un campo si sposta?
Il bordo si è spostato e l'editor no perché il notifier XFA nativo confrontava un rettangolo con sé stesso. Quando il layout cambia la geometria di un widget già caricato, PDFium dovrebbe accorgersene e chiamare PerformLayout sul widget, che riposiziona l'editor di testo e la sua area di hit. Il controllo confrontava GetWidgetRect() con RecacheWidgetRect(). Entrambe le funzioni restituiscono un riferimento const allo stesso membro, e il ricache sovrascrive quel membro sul posto, così il confronto vedeva sempre due valori identici e i widget caricati saltavano il loro relayout
Il sintomo è emerso quando un test cambiava l'altezza di un subform così che i campi esistenti attraversassero la pagina successiva. Su entrambe le architetture V8, il bordo del campo veniva disegnato nella nuova posizione mentre il testo digitato e l'area di hit del mouse restavano alla coordinata Y precedente. Un relayout esplicito non sistemava le cose, e nemmeno ricaricare la pagina, perché il widget continuava a credere che la sua geometria fosse aggiornata. Le librerie V8 Windows speditate con la v3.126.1 copiano il vecchio rettangolo per valore prima del ricache e confrontano quella copia, così i widget spostati fanno relayout e il valore modificato appare esattamente dove sta il bordo. Questa è una correzione nativa: viaggia con le DLL, quindi aggiornare le unità Pascal tenendo una pdfium.v8.dll più vecchia lascia le aree di hit fuori posto. Il controllo di regressione che l'ha guidata modifica prima una riga superstite a un valore non predefinito e poi pretende quel valore nella nuova posizione del campo, perché una riga ricostruita coi valori predefiniti sembrerebbe altrimenti un passaggio
Come ricarica le pagine TPdfView senza tirare via un handle dall'interno di PDFium?
Dalla v3.126.2, TPdfView differisce il ricaricamento di pagina che segue un cambio di layout XFA finché lo stack di chiamate nativo non si è scaricato. L'evento pagina di solito scatta mentre PDFium sta ancora processando l'input: l'utente ha cliccato un pulsante Add Row, il click ha fatto girare uno script, lo script ha cambiato il conteggio delle istanze, e il layout è finito dentro quella stessa chiamata nativa. Chiudere e riaprire l'handle di pagina in quel momento libererebbe un oggetto che il chiamante sta ancora usando. Prima della v3.126.2, il viewer si invalidava soltanto, così l'handle di pagina mostrato poteva continuare a puntare allo stato pre-layout, e se l'utente era stato sull'ultima pagina quando questa era sparita, il numero di pagina selezionato era fuori intervallo
Il refresh differito funziona in pochi piccoli passi, e spiegano il comportamento che vedi dall'host:
- Il callback dell'evento pagina marca la vista come in attesa di un refresh del layout XFA e posta un messaggio di finestra privato; gli eventi ripetuti prima che il messaggio arrivi si fondono in un unico refresh
- Una vista senza ancora un handle di finestra conserva il flag in attesa e posta il messaggio da
CreateWnd, mentre cambiare documento, disattivare la vista o distruggerla pulisce il flag - Quando il messaggio arriva, la vista pulisce la selezione di testo, l'evidenziazione di ricerca e l'indice del campo con focus, perché tutti e tre si riferivano al vecchio layout
- La pagina selezionata viene limitata al nuovo
PageCount; un numero di pagina cambiato passa per il normale cambio di pagina, altrimenti la pagina corrente viene ricaricata, e la modalità di adattamento viene riapplicata - Se il layout non lascia proprio nessuna pagina, la vista scarica il suo vecchio handle di pagina invece di disegnare una pagina che non esiste più
Lo stesso vincolo vale per il tuo codice. OnXfaPageCountChanged gira dentro quel callback di layout nativo, quindi trattalo come una notifica: aggiorna etichette, intervalli dello spinner e stato della toolbar lì, e metti in coda tutto ciò che è più pesante, come chiudere il documento o aprirne un altro, con un messaggio postato così gira dopo che il callback è tornato. TPdfView.OnPageChange poi ti dice quando la vista ha davvero ricaricato la pagina, e leggere PdfView1.PageNumber in quel punto ti dà il valore limitato. L'attraversamento col tasto Tab e i controlli FormType che un viewer di form esegue all'apertura sono coperti in navigazione dei campi form PDF con PDFium Component
Perché cliccare un campo Full XFA solleva "Cannot open text page"?
Le pagine Full XFA non hanno una pagina di testo PDF, e prima della v3.126.2 la selezione testuale predefinita del viewer e il rilevamento dei link provavano comunque a caricarne una. Con TPdfView.AllowUserTextSelection al suo default di True, il passaggio del mouse chiedeva al layer di testo il carattere sotto il cursore, e un click con rilascio del mouse faceva una sonda URL automatica sul testo di pagina. Su una pagina Full XFA la pagina di testo non può essere aperta, quindi un click ordinario dentro un campo poteva finire in un'eccezione Cannot open text page. Dalla v3.126.2, entrambi i percorsi interni restituiscono nessun risultato quando TPdf.FormType è ftXfaFull e il runtime XFA è disponibile, così le impostazioni predefinite funzionano e l'input nei campi resta disponibile
Spegnere AllowUserTextSelection per i documenti Full XFA resta una scelta di UI ragionevole, perché non c'è testo di pagina da selezionare e i gesti di trascinamento non dovrebbero avviare una modalità di selezione. Non è un sostituto dell'aggiornamento, però: sulle versioni precedenti la sonda URL al click non dipendeva da quella proprietà, quindi un viewer poteva beccare la stessa eccezione con la selezione disabilitata
procedure TClaimForm.ConfigureViewerForForm;
begin
// FormType legge il documento aperto, quindi chiama questo dopo FPdf.Active := True
if FPdf.XFA and (FPdf.FormType = ftXfaFull) and FPdf.XfaRuntimeAvailable then
begin
// Nessun layer di testo PDF esiste sulle pagine Full XFA; i campi restano modificabili
PdfView1.AllowUserTextSelection := False;
StatusBar1.SimpleText := Format('Dynamic XFA form, %d page(s)',
[FPdf.PageCount]);
end
else
PdfView1.AllowUserTextSelection := True;
end;
La digitazione si è meritata una sua riparazione nella v3.126.2. L'editor di testo XFA nativo non sostituisce una selezione quando riceve un carattere: FORM_OnChar inserisce al cursore, e Backspace cancella un solo carattere, quindi selezionare un valore e digitarci sopra produceva il vecchio e il nuovo testo fianco a fianco. PDFium Component ora ricorda che il click è caduto su un campo testuale XFA e instrada i caratteri digitati, Backspace e Delete attraverso FORM_ReplaceSelection ogni volta che esiste una selezione e il documento concede il permesso di compilare o modificare. Se un campo XFA a sola lettura possa cambiare lo decide comunque l'editor nativo, quindi un campo marcato a sola lettura nel form conserva il suo valore anche in un documento che altrimenti consente la compilazione. Impostare TPdfView.AllowFormEvents a False ferma anche questo instradamento della tastiera, il che mantiene un viewer a sola lettura a sola lettura
Riferimento rapido: XFA dinamico in un viewer Delphi
| Sintomo | Causa | Sistemato in |
|---|---|---|
| Il conteggio pagine mostra 1 dopo che il form è cresciuto a due pagine | L'evento pagina nativo passa un delta aggiunte/rimozioni, non un totale | v3.126.1 (wrapper) |
| Il bordo del campo si sposta, testo digitato e area di hit restano indietro | Il widget caricato saltava il relayout dopo un'auto-confrontazione | v3.126.1 (librerie Windows V8) |
| Il viewer disegna o instrada l'input allo stato di pagina pre-layout | Handle di pagina non ricaricato dopo la ripaginazione | v3.126.2 (refresh differito) |
| Il click in un campo solleva Cannot open text page | Selezione testuale e sonda URL su pagine senza layer di testo | v3.126.2 |
| Digitare sopra un valore selezionato accoda anziché sostituire | L'editor XFA nativo inserisce al cursore | v3.126.2 |
- Imposta
EnableV8EngineaTrueprima che qualsiasi documento si carichi, e gestisciOnXfaRuntimeMissingper il caso in cui la libreria semplice sia stata caricata per prima - Leggi il totale da
TPdf.PageCounto dal parametroNewCountdiOnXfaPageCountChanged; non aggiungere o sottrarre mai conteggi di pagina da te - Tieni leggero l'handler di
OnXfaPageCountChanged, perché gira dentro il callback di layout nativo - Sincronizza l'indicatore della pagina corrente in
TPdfView.OnPageChange, che scatta dopo che il ricaricamento differito ha limitato il numero di pagina - Spedisci le DLL Windows V8 v3.126.1 o successive insieme alle unità; la correzione del relayout dei widget vive nel codice nativo
- Prova con un form che cambia davvero il proprio conteggio pagine e sposta un campo modificato attraverso un'interruzione di pagina, perché i campioni a lunghezza fissa nascondono ogni bug di questa lista
XFA dinamico trasforma conteggio pagine e geometria dei campi in valori vivi, e un viewer resta corretto solo quando li prende dal layout completato e ricarica le pagine in un momento sicuro. PDFium Component gestisce entrambe le cose dentro TPdf e TPdfView, quindi all'host resta solo da ascoltare. Dettagli e download sono sulla pagina prodotto di PDFium Component for Delphi