Premete Tab in un modulo PDF costruito dal vostro codice e il cursore atterra due campi più in là di dove dovrebbe, oppure salta del tutto la seconda colonna, oppure torna in cima dopo il terzo campo invece che al quarto. La persona che compila una fattura nel vostro viewer si aspetta che la tastiera percorra il modulo come percorre ogni modulo web che abbia mai usato. Quando non succede, prende il mouse, va a caccia della casella successiva e decide in silenzio che il vostro strumento è incompiuto. Un attraversamento prevedibile dei campi è la differenza fra un viewer per inserimento dati che la gente tollera e uno di cui si fida, ed è quasi interamente questione di usare la giusta API di focus invece di simulare l'input da tastiera con clic fittizi
Gli esempi qui sotto usano PDFium Component, un componente VCL/LCL basato su PDFium per Delphi, C++Builder e Lazarus. La navigazione è una delle tre cose che un viewer di moduli deve azzeccare; le altre due, aprire il modulo correttamente e salvare i valori compilati in modo che compaiano davvero, sono là dove si nasconde la maggior parte delle sorprese, quindi tutte e tre sono trattate qui sotto
Aprire un modulo: FormFill, FormType e la questione XFA
L'accesso ai campi richiede che il sottosistema di compilazione moduli, controllato dalla proprietà FormFill, sia abilitato prima che il documento venga aperto. Una volta attivo, FormType vi dice che tipo di modulo avete davanti, e la risposta cambia l'insieme di funzioni che potete promettere:
Pdf.FileName := FormPath;
Pdf.FormFill := True; // abilitare prima di Active; serve per i campi
Pdf.Active := True;
case Pdf.FormType of
ftNone:
DisableFormPanel('This document has no interactive form');
ftAcroForm:
BuildFieldList; // navigazione e modifica dei campi disponibili
ftXfaFull:
ShowXfaNotice; // XFA si disegna dal proprio template XML;
// considerate limitata la modifica dei campi
end;
Da quello switch discendono due note pratiche. AcroForm è il modello di modulo standard ISO 32000, ed è ciò a cui punta ogni API qui presente. I documenti XFA incorporano una propria architettura di modulo XML, quindi promettere a un cliente la modifica XFA completa dopo una rapida dimostrazione su AcroForm è un impegno di cui vi pentirete. La seconda nota riguarda gli effetti collaterali: impostare FormFill a True inizializza anche il JavaScript del documento. In un viewer per inserimento dati questo è esattamente giusto, perché sono gli script di calcolo a tenere aggiornato un totale progressivo mentre qualcuno digita. In una finestra di anteprima per file di origine ignota è esattamente sbagliato. L'articolo sull'anteprima PDF sicura tratta il lato FormFill := False di quel compromesso
Attraversamento con Tab che atterra dove gli utenti si aspettano
Torniamo al problema di tastiera dell'inizio. La tentazione è simulare il Tab sintetizzando un clic del mouse sul rettangolo del widget successivo, cosa che si rompe nell'istante in cui un campo esce dallo schermo o due widget si sovrappongono. La API di focus sposta invece direttamente il focus del modulo stesso, senza indovinelli sulla geometria. Cinque chiamate coprono tutto: FocusFormField per indice, FocusNextFormField e FocusPreviousFormField per avanzare passo passo, FocusedFormFieldIndex per leggere dove siete e ClearFormFieldFocus per rilasciare del tutto il focus
procedure TFormViewer.HandleTabKey(Shift: TShiftState);
begin
if ssShift in Shift then
PdfView.FocusPreviousFormField
else
PdfView.FocusNextFormField;
UpdateFieldStatus; // per es. "Field 4 of 17: InvoiceDate"
end;
L'unico comportamento su cui le persone inciampano è il giro completo. L'attraversamento lavora sull'ordine di tabulazione della pagina corrente e gira in tondo al suo interno: superate l'ultimo campo e siete di nuovo sul primo. Entrambe le funzioni di avanzamento restituiscono il nuovo indice di campo, oppure -1 quando la pagina non contiene alcun campo. Quel giro è per pagina, non per documento, il che significa che passare alla pagina successiva è compito vostro, non della libreria. Confrontate l'indice restituito con quello da cui siete partiti, accorgetevi di quando ha girato in tondo e fate avanzare voi PageNumber se il modulo deve leggersi come una sequenza continua. Saltate quel controllo e un modulo di due pagine intrappola in silenzio il cursore sulla pagina uno, che è la sua versione del reclamo sul Tab rotto
L'attraversamento diventa utile quando il resto della UI reagisce. L'evento OnFormFieldEnter scatta quando il focus arriva, e sul viewer OnFormFieldFocusChange riferisce il nuovo indice di campo, così un pannello laterale può restare al passo con qualunque cosa la tastiera abbia appena selezionato. Quando vi serve la mappatura inversa, da una posizione sullo schermo a un campo, la proprietà indicizzata FormFieldAt fa il rilevamento per anteprime a comparsa e pannelli con clic per modificare. In tutto questo c'è un tornaconto silenzioso in fatto di accessibilità: poiché il focus segue l'ordine dei campi del documento stesso, il percorso che cablate per il tasto Tab è lo stesso che uno screen reader annuncia, senza lavoro aggiuntivo
Mostrare i nomi dei campi anziché i numeri di indice grezzi richiede una proprietà in più. FormFieldInfo[] restituisce un record TPdfFormFieldInfo per indice, che porta nome del campo, tipo, corpo del carattere, stato di spunta, valore di esportazione e appartenenza al gruppo, cioè quello che una lista di navigazione dovrebbe mostrare ("Campo 4 di 17: InvoiceDate" anziché "4"). I gruppi di pulsanti di opzione sono il caso che merita un file di prova dedicato. Più widget possono condividere un unico nome di campo, quindi un elenco messo insieme ingenuamente a partire dai widget mostra lo stesso gruppo più volte e confonde chiunque lo legga
Perché i valori compilati escono vuoti, e la chiamata che lo risolve
L'altro reclamo che riempie le code dell'assistenza è più allarmante di un tasto Tab che si comporta male: un modulo viene compilato via codice, il cliente lo apre in Acrobat e ogni campo sembra vuoto. Fate clic dentro un campo e il suo valore compare di scatto. I dati sono nel file per tutto il tempo. Ciò che manca è l'immagine dei dati, e vale la pena capirne il motivo una volta per tutte perché spiega un'intera famiglia di bug
Un campo di testo AcroForm memorizza il proprio valore nella voce /V del dizionario di campo (ISO 32000-1 §12.7.3.3). Ciò che un viewer dipinge davvero è un'altra cosa: lo stream di aspetto del widget sotto /AP (§12.5.5), un piccolo frammento di contenuto già disegnato. Scrivete /V e lasciate stare /AP, e i due divergono. Il valore c'è; la sua versione disegnata è vecchia o assente. Acrobat, guarda caso, ricostruisce l'aspetto di un campo quando questo riceve il focus, il che è tutta la spiegazione dei valori che compaiono solo al clic. Il vecchio flag NeedAppearances, che chiedeva ai viewer di rigenerare gli aspetti al posto vostro, non ha mai funzionato in modo uniforme ed è deprecato in PDF 2.0, e i server di stampa e i generatori di miniature lo ignorano completamente. Dipingono /AP e nient'altro, quindi se /AP è vuoto stampano una casella vuota
Assegnare un valore tramite FormField[i] scrive soltanto /V. Ecco perché compilare un modulo è una sequenza di tre passi, e il passo che i team saltano è quello di mezzo:
procedure TFormViewer.FillAndSave(const Values: array of WString;
const OutputPath: string);
var
i: Integer;
begin
for i := 0 to Pdf.FormFieldCount - 1 do
Pdf.FormField[i] := Values[i]; // scrive soltanto /V
// Ricostruisce gli stream di aspetto /AP; senza questo il modulo
// sembra vuoto in Acrobat finché ogni campo non viene cliccato
Pdf.GenerateFormAppearances;
Pdf.SaveAs(OutputPath);
end;
GenerateFormAppearances è tutta la correzione. Ricostruisce lo stream di aspetto di ogni widget a partire dai valori, dai font e dall'allineamento correnti, così che un viewer che non esegue mai un evento di focus, un server di stampa o un generatore di miniature, dipinga comunque lo stato compilato. Chiamatela una volta dopo il gruppo di assegnazioni, non una volta per campo. La generazione degli aspetti fa vero lavoro di impaginazione, e le chiamate per singolo campo lo moltiplicano su un modulo grande senza alcun motivo
Rigenerare gli aspetti è anche il momento in cui font e allineamento si fanno valere, ed è la fonte di una sorpresa di secondo ordine. Il nuovo stream impagina ogni valore dentro il rettangolo del widget usando font, corpo e allineamento del campo. Un valore che sta comodo nel vostro modulo di prova può venire tagliato o rimpicciolito nella copia di un cliente dove lo stesso campo è più stretto. I campi a dimensione automatica (corpo del carattere zero) rimpiccioliscono il testo perché entri; i campi a dimensione fissa lo tagliano e basta. Entrambi i comportamenti sono legittimi, e l'unico modo onesto di sapere quale adotti un dato modulo è guardare l'output rigenerato anziché la stringa che avete scritto. Quando qualcuno segnala testo tagliato al bordo di una casella, il motivo è quasi sempre questo
Trattate la verifica come parte del completamento del lavoro, non come un ripensamento. Aprite il file salvato in Acrobat e confermate che i valori siano visibili prima di toccare qualsiasi campo. Poi stampatelo su PDF o su immagine da un viewer diverso, uno che ignori del tutto la logica dei moduli, e confermate che i valori sopravvivano anche a quel percorso. Insieme, quei due controlli intercettano ogni variante della divergenza fra /V e /AP
Configurazioni di campo che passano la demo e falliscono sul campo
I moduli puliti da dimostrazione nascondono una serie di casi limite che i file dei clienti no. Quattro di essi spiegano la maggior parte delle segnalazioni del tipo "sulla mia macchina funzionava"
- Valori di esportazione delle caselle di spunta. Lo stato attivo non è sempre
Yes. Un modulo è libero di definire il proprio valore di esportazione, e scrivere la stringa sbagliata lascia la casella visivamente non spuntata mentre il vostro codice è convinto di averla impostata. Leggete il valore di esportazione daFormFieldInfo[]anziché darne uno per scontato - Gruppi di opzioni a nome condiviso. Un campo, più widget. Il valore che assegnate decide quale widget risulti selezionato, quindi il codice di UI che presume che un nome corrisponda a un rettangolo finisce per disegnare l'anello di focus sul pulsante sbagliato
- Campi calcolati. I totali mantenuti dal JavaScript del documento si aggiornano in risposta agli eventi dei campi. Una compilazione via codice che aggira quegli eventi deve o innescare il ricalcolo o sovrascrivere direttamente i campi calcolati. Un modulo in cui le righe e il totale non concordano è peggio di entrambe le correzioni
- Campi obbligatori nascosti. I moduli condizionali nascondono campi che restano marcati come obbligatori. Decidete in anticipo se la vostra convalida rispetta la visibilità o il flag grezzo di obbligatorietà, poi mettete quella decisione per iscritto in un posto che l'assistenza possa trovare
Una distinzione vale la pena chiarirla prima che vi morda: generare gli aspetti non è appiattire. GenerateFormAppearances rende i valori visibili ovunque lasciando i campi modificabili. L'appiattimento cuoce l'aspetto dentro il contenuto statico della pagina e toglie l'interattività per sempre, il che è giusto per una copia di archivio e sbagliato per un modulo che la persona successiva deve ancora compilare. Se FormType riferisce ftXfaFull anziché ftAcroForm, nulla della superficie di modifica qui descritta si applica comunque in modo pulito, dato che il documento si disegna dal proprio template XML; rilevate quel caso e ditelo all'utente, invece di lasciare che scopra il limite da solo
Il sottosistema di compilazione moduli, l'attraversamento del focus e la generazione degli aspetti mostrati qui fanno parte di PDFium Component per Delphi, C++Builder e Lazarus/FPC. Se il vostro viewer gestisce anche i marcatori dei revisori accanto ai dati dei moduli, l'articolo sulla revisione delle annotazioni tratta quel modello adiacente