Articol tehnic

Navigarea în câmpurile formularelor PDF în Delphi (PDFium)

Apăsați Tab într-un formular PDF construit de codul dvs., iar cursorul ajunge la două câmpuri distanță de locul unde ar trebui, sau sare complet peste a doua coloană, sau revine la început după al treilea câmp în loc de al patrulea. Persoana care completează o factură în vizualizatorul dvs. se așteaptă ca tastatura să parcurgă formularul la fel cum parcurge orice formular web pe care l-a folosit vreodată. Când nu se întâmplă așa, întinde mâna spre mouse, caută următoarea căsuță și decide în tăcere că instrumentul dvs. este neterminat. Parcurgerea previzibilă a câmpurilor face diferența dintre un vizualizator de introducere a datelor pe care oamenii îl tolerează și unul în care au încredere, iar aceasta ține aproape în întregime de utilizarea API-ului corect de focalizare, în loc de a simula intrarea de la tastatură prin clicuri simulate

Exemplele de mai jos folosesc PDFium Component, o componentă VCL/LCL bazată pe PDFium pentru Delphi, C++Builder și Lazarus. Navigarea este unul dintre cele trei lucruri pe care un vizualizator de formulare trebuie să le facă bine; celelalte două, deschiderea corectă a formularului și salvarea valorilor completate astfel încât acestea să apară efectiv, sunt locul unde se ascund majoritatea surprizelor, așa că toate trei sunt tratate mai jos

Deschiderea unui formular: FormFill, FormType și problema XFA

Accesul la câmpuri necesită ca subsistemul form-fill, controlat de proprietatea FormFill, să fie activat înainte de deschiderea documentului. Odată activ, FormType vă spune cu ce tip de formular aveți de-a face, iar răspunsul schimbă setul de funcționalități pe care le puteți promite:

Diagramă a ramurilor de configurare FormFill și detecție FormType într-un vizualizator Delphi PDFium Component, scinzând tratarea ftNone, ftAcroForm și ftXfaFull
FormType se ramifică odată ce FormFill este activat, iar fiecare ramură promite un set diferit de funcționalități
Pdf.FileName := FormPath;
Pdf.FormFill := True;   // se activează înainte de Active; necesar pentru orice acces la câmpuri
Pdf.Active := True;

case Pdf.FormType of
  ftNone:
    DisableFormPanel('This document has no interactive form');
  ftAcroForm:
    BuildFieldList;     // navigare și editare completă a câmpurilor, disponibile
  ftXfaFull:
    ShowXfaNotice;      // XFA se randează din propriul șablon XML;
                        // tratați editarea câmpurilor ca fiind limitată
end;

Din această comutare rezultă două observații practice. AcroForm este modelul standard de formular ISO 32000 și este ceea ce vizează fiecare API de aici. Documentele XFA își încorporează propria arhitectură de formular XML, așa că a promite unui client editare XFA completă după o demonstrație rapidă AcroForm este un angajament pe care îl veți regreta. A doua observație privește efectele secundare: setarea FormFill la True inițializează și JavaScript-ul documentului. Într-un vizualizator pentru introducerea datelor, acest lucru este exact corect, deoarece scripturile de calcul sunt cele care mențin actualizat un total curent pe măsură ce cineva tastează. Într-o fereastră de previzualizare pentru fișiere de origine necunoscută, acest lucru este exact greșit. Articolul despre previzualizarea PDF securizată tratează latura FormFill := False a acestui compromis

Parcurgerea cu tasta Tab care ajunge acolo unde se așteaptă utilizatorii

Revenim la problema tastaturii de la început. Tentația este să simulați Tab generând un clic de mouse pe dreptunghiul următorului widget, ceea ce se defectează imediat ce un câmp iese din ecran prin derulare sau două widget-uri se suprapun. În schimb, API-ul de focalizare mută direct focalizarea proprie a formularului, fără nicio presupunere legată de geometrie. Cinci apeluri acoperă asta: FocusFormField după index, FocusNextFormField și FocusPreviousFormField pentru pas cu pas, FocusedFormFieldIndex pentru a citi unde vă aflați și ClearFormFieldFocus pentru a elimina complet focalizarea

Diagramă a parcurgerii focalizării cu tasta Tab într-un vizualizator Delphi PDFium Component, în care FocusNextFormField se înfășoară în interiorul ordinii de tab al unei singure pagini, iar cinci API-uri de focalizare acoperă navigarea cu tastatura
Traversarea face buclă în interiorul ordinii tab a unei singure pagini, astfel încât trecerea la pagina următoare rămâne treaba viewer-ului
procedure TFormViewer.HandleTabKey(Shift: TShiftState);
begin
  if ssShift in Shift then
    PdfView.FocusPreviousFormField
  else
    PdfView.FocusNextFormField;
  UpdateFieldStatus;  // de ex. "Field 4 of 17: InvoiceDate"
end;

Singurul comportament care încurcă lumea este revenirea la capăt (wrap). Parcurgerea funcționează prin ordinea de tabulare a paginii curente și se reia în interiorul ei: treceți de ultimul câmp și reveniți la primul. Ambele funcții de pas returnează noul index al câmpului, sau -1 atunci când pagina nu conține deloc câmpuri. Această reluare este per pagină, nu per document, ceea ce înseamnă că trecerea la pagina următoare este responsabilitatea dvs., nu a bibliotecii. Comparați indexul returnat cu cel de la care ați pornit, observați când s-a produs reluarea și avansați dvs. înșivă PageNumber dacă formularul este gândit să se citească drept o singură secvență continuă. Omiteți această verificare și un formular pe două pagini blochează silențios cursorul pe prima pagină, ceea ce este propria sa variantă a reclamației despre Tab defect

Parcurgerea devine utilă odată ce restul interfeței reacționează la ea. Evenimentul OnFormFieldEnter se declanșează la sosirea focalizării, iar pe vizualizator OnFormFieldFocusChange raportează noul index de câmp, astfel încât un panou lateral poate rămâne sincronizat cu orice tocmai a selectat tastatura. Când aveți nevoie de maparea inversă, de la o poziție pe ecran către un câmp, proprietatea indexată FormFieldAt face testarea de coliziune (hit-testing) pentru previzualizările tip tooltip și panourile de tip click-to-edit. Există un beneficiu discret de accesibilitate în toate acestea: deoarece focalizarea urmează propria ordine de câmpuri a documentului, traseul pe care îl conectați pentru tasta Tab este același traseu pe care îl anunță un cititor de ecran, fără niciun efort suplimentar

Afișarea numelor câmpurilor în locul numerelor brute de index necesită încă o proprietate. FormFieldInfo[] returnează o înregistrare TPdfFormFieldInfo pentru fiecare index, purtând numele câmpului, tipul, dimensiunea fontului, starea de bifare, valoarea de export și apartenența la grup, ceea ce este exact ce ar trebui să afișeze o listă de navigare ("Field 4 of 17: InvoiceDate" mai degrabă decât "4"). Grupurile radio sunt cazul care merită un fișier de test dedicat. Mai multe widget-uri pot împărtăși un singur nume de câmp, așa că o listă asamblată naiv din widget-uri arată același grup de mai multe ori și îi derutează pe toți cei care o citesc

De ce valorile completate apar goale și apelul care rezolvă asta

Cealaltă reclamație care umple cozile de suport este mai alarmantă decât o tastă Tab care se comportă greșit: un formular este completat programatic, clientul îl deschide în Acrobat, iar toate câmpurile par goale. Faceți clic într-un câmp și valoarea lui apare instantaneu. Datele se află în fișier tot timpul. Ceea ce lipsește este imaginea datelor, iar motivul merită înțeles o dată pentru totdeauna, deoarece explică o întreagă familie de erori

Un câmp de tip text AcroForm își stochează valoarea în intrarea /V a dicționarului câmpului (ISO 32000-1 §12.7.3.3). Ceea ce desenează efectiv un vizualizator este cu totul altceva: fluxul de aspect (appearance stream) al widget-ului aflat sub /AP (§12.5.5), un mic fragment de conținut pre-randat. Scrieți /V și lăsați /AP neatins, iar cele două se îndepărtează una de alta. Valoarea este acolo; versiunea randată a ei este învechită sau absentă. Se întâmplă ca Acrobat să reconstruiască aspectul unui câmp atunci când acesta primește focalizarea, ceea ce reprezintă întreaga explicație pentru valorile care apar doar la clic. Vechiul indicator NeedAppearances, care le cerea vizualizatoarelor să regenereze aspectele pentru dvs., nu a funcționat niciodată uniform și este depreciat în PDF 2.0, iar serverele de tipărire și generatoarele de miniaturi îl ignoră complet. Ele desenează /AP și nimic altceva, așa că dacă /AP este gol, tipăresc o căsuță goală

Atribuirea unei valori prin FormField[i] scrie doar /V. De aceea, completarea unui formular este o secvență în trei pași, iar pasul pe care echipele îl omit este cel din mijloc:

Diagramă a derivării valoare /V versus aspect /AP în câmpurile AcroForm și a secvenței de umplere Delphi în trei pași, construită în jurul GenerateFormAppearances
Atribuirea valorilor scrie doar /V, iar pasul din mijloc este cel care repictează ceea ce serverele de imprimare randează efectiv
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];   // scrie doar /V

  // Reconstruiește fluxurile de aspect /AP; fără acest apel, formularul
  // apare gol în Acrobat până când fiecare câmp este accesat prin clic
  Pdf.GenerateFormAppearances;

  Pdf.SaveAs(OutputPath);
end;

GenerateFormAppearances este întreaga rezolvare. Reconstruiește fluxul de aspect al fiecărui widget pornind de la valorile, fonturile și alinierea (quadding) curente, astfel încât un vizualizator care nu rulează niciodată un eveniment de focalizare, un server de tipărire sau un generator de miniaturi să deseneze oricum starea completată. Apelați-l o singură dată după lotul de atribuiri, nu o dată pentru fiecare câmp. Generarea aspectului efectuează o muncă reală de aranjare în pagină, iar apelurile per câmp multiplică acest cost pe un formular mare, degeaba

Regenerarea aspectelor este și momentul în care fonturile și alinierea își spun cuvântul, ceea ce este sursa unei surprize de ordinul al doilea. Noul flux așază fiecare valoare în interiorul dreptunghiului widget-ului folosind fontul, dimensiunea și alinierea (quadding) câmpului. O valoare care încape confortabil în formularul dvs. de test poate fi tăiată sau micșorată în copia unui client, unde același câmp este mai îngust. Câmpurile cu dimensiune automată (dimensiune font zero) micșorează textul pentru a încăpea; câmpurile cu dimensiune fixă îl taie pur și simplu. Ambele comportamente sunt legale, iar singurul mod onest de a afla pe care îl aplică un anumit formular este să priviți rezultatul regenerat, nu șirul pe care l-ați scris. Când cineva raportează text tăiat la marginea unei căsuțe, acesta este aproape întotdeauna motivul

Tratați verificarea ca parte a finalizării lucrării, nu ca pe un gând ulterior. Deschideți fișierul salvat în Acrobat și confirmați că valorile sunt vizibile înainte de a atinge vreun câmp. Apoi tipăriți-l în PDF sau într-o imagine dintr-un alt vizualizator, unul care ignoră complet logica formularului, și confirmați că valorile supraviețuiesc și acelui traseu. Împreună, aceste două verificări prind fiecare variantă a decalajului dintre /V și /AP

Configurații de câmpuri care trec de demonstrație și eșuează pe teren

Formularele demo curate ascund un set de cazuri-limită pe care fișierele clienților nu le ascund. Patru dintre ele explică majoritatea rapoartelor de tip „la mine a mers”

  • Valorile de export ale casetelor de bifare. Starea „activat” nu este întotdeauna Yes. Un formular este liber să-și definească propria valoare de export, iar scrierea șirului greșit lasă caseta vizual nebifată, în timp ce codul dvs. este convins că a bifat-o. Citiți valoarea de export din FormFieldInfo[] în loc să presupuneți una
  • Grupuri radio cu nume comun. Un singur câmp, mai multe widget-uri. Valoarea pe care o atribuiți decide care widget apare ca selectat, astfel încât codul de interfață care presupune că un nume corespunde unui singur dreptunghi ajunge să deseneze inelul de focalizare pe butonul greșit
  • Câmpuri calculate. Totalurile menținute de JavaScript-ul documentului se actualizează ca răspuns la evenimentele câmpurilor. O completare programatică ce ocolește aceste evenimente trebuie fie să declanșeze recalcularea, fie să suprascrie direct câmpurile calculate. Un formular în care liniile de detaliu și totalul nu concordă este mai rău decât oricare dintre cele două soluții
  • Câmpuri obligatorii ascunse. Formularele condiționale ascund câmpuri care rămân totuși marcate ca obligatorii. Decideți din start dacă validarea dvs. respectă vizibilitatea sau indicatorul brut de câmp obligatoriu, apoi consemnați această decizie undeva unde echipa de suport o poate găsi

O distincție merită lămurită înainte să vă dea bătăi de cap: generarea aspectelor nu înseamnă aplatizare (flattening). GenerateFormAppearances face valorile vizibile peste tot, lăsând totodată câmpurile editabile. Aplatizarea încorporează aspectul în conținutul static al paginii și elimină definitiv interactivitatea, ceea ce este corect pentru o copie de arhivă și greșit pentru un formular pe care următoarea persoană încă trebuie să îl completeze. Dacă FormType raportează ftXfaFull în loc de ftAcroForm, oricum niciuna dintre suprafețele de editare de aici nu se aplică în mod curat, deoarece documentul se randează din propriul șablon XML; detectați acest caz și informați utilizatorul, în loc să îl lăsați să descopere singur limita

Subsistemul form-fill, parcurgerea focalizării și generarea aspectului prezentate aici fac parte din PDFium Component pentru Delphi, C++Builder și Lazarus/FPC. Dacă vizualizatorul dvs. gestionează și adnotările reviewer-ilor alături de datele de formular, articolul despre revizuirea adnotărilor tratează acel model adiacent