Teknisk artikel

Navigering i PDF-formularfelter i Delphi (PDFium-komponent)

Tryk Tab i en PDF-formular, din kode har bygget, og markøren lander to felter fra, hvor den burde, eller springer den anden kolonne helt over, eller hopper tilbage til toppen efter det tredje felt i stedet for det fjerde. Personen, der udfylder en faktura i din fremviser, forventer, at tastaturet vandrer gennem formularen på samme måde, som det vandrer gennem enhver webformular, de nogensinde har brugt. Når det ikke gør det, griber de fat i musen, jager efter den næste boks og beslutter stiltiende, at dit værktøj er ufærdigt. Forudsigelig felttraversering er forskellen mellem en dataindtastningsfremviser, folk udholder, og én, de stoler på, og det er næsten udelukkende et spørgsmål om at bruge det rigtige fokus-API i stedet for at forfalske tastaturinput med simulerede klik

Eksemplerne nedenfor bruger PDFium Component, en PDFium-baseret VCL/LCL-komponent til Delphi, C++Builder og Lazarus. Navigation er én af tre ting, en formularfremviser skal få rigtig; de to andre, at åbne formularen korrekt og gemme udfyldte værdier, så de rent faktisk viser sig, er der, hvor de fleste overraskelser gemmer sig, så alle tre dækkes nedenfor

At åbne en formular: FormFill, FormType og XFA-spørgsmålet

Feltadgang kræver, at form-fill-undersystemet, styret af egenskaben FormFill, er slået til, før dokumentet åbnes. Når det er aktivt, fortæller FormType dig, hvilken slags formular du står over for, og svaret ændrer det funktionssæt, du kan love:

Diagram over FormFill-opsætnings- og FormType-detektionsgrene i en Delphi PDFium Component-viewer, der splitter ftNone-, ftAcroForm- og ftXfaFull-håndtering
FormType forgrener sig, så snart FormFill er aktiveret, og hver gren lover et andet funktionsæt
Pdf.FileName := FormPath;
Pdf.FormFill := True;   // aktivér før Active; påkrævet for al feltadgang
Pdf.Active := True;

case Pdf.FormType of
  ftNone:
    DisableFormPanel('This document has no interactive form');
  ftAcroForm:
    BuildFieldList;     // fuld feltnavigation og -redigering tilgængelig
  ftXfaFull:
    ShowXfaNotice;      // XFA renderer fra sin egen XML-skabelon;
                        // behandl feltredigering som begrænset
end;

To praktiske bemærkninger følger af den kontakt. AcroForm er standard-ISO 32000-formularmodellen, og det er, hvad hvert API her sigter mod. XFA-dokumenter indlejrer deres egen XML-formulararkitektur, så at love en kunde fuld XFA-redigering efter en hurtig AcroForm-demo er en forpligtelse, du vil fortryde. Den anden bemærkning handler om sideeffekter: at sætte FormFill til True initialiserer også dokument-JavaScript. I en dataindtastningsfremviser er det præcis rigtigt, fordi beregningsscripts er det, der holder en løbende total ajour, mens nogen skriver. I et forhåndsvisningsvindue til filer af ukendt oprindelse er det præcis forkert. Artiklen om sikker PDF-forhåndsvisning dækker FormFill := False-siden af den afvejning

Tab-tast-traversering, der lander, hvor brugere forventer

Tilbage til tastaturproblemet fra toppen. Fristelsen er at forfalske Tab ved at syntetisere et museklik på det næste widgets rektangel, hvilket går i stykker i det øjeblik et felt er scrollet ud af skærmen, eller to widgets overlapper. Fokus-API'et flytter i stedet formularens eget fokus direkte, uden nogen geometri-gætteri. Fem kald dækker det: FocusFormField efter indeks, FocusNextFormField og FocusPreviousFormField til trinvis bevægelse, FocusedFormFieldIndex til at læse, hvor du er, og ClearFormFieldFocus til at droppe fokus helt

Diagram over Tabtast fokusgennemløb i en Delphi PDFium Component-viewer, hvor FocusNextFormField wrapper inden for én sides tabrækkefølge, og fem focus-API'er dækker tastaturnavigation
Gennemløb løkker inden for én sides tabulatorrækkefølge, så krydsning til næste side forbliver fremviserens opgave
procedure TFormViewer.HandleTabKey(Shift: TShiftState);
begin
  if ssShift in Shift then
    PdfView.FocusPreviousFormField
  else
    PdfView.FocusNextFormField;
  UpdateFieldStatus;  // f.eks. "Field 4 of 17: InvoiceDate"
end;

Den ene del af adfærden, der snubler folk, er wrap'et. Traversering arbejder gennem den aktuelle sides tab-rækkefølge og løkker inden i den: træd forbi det sidste felt, og du er tilbage ved det første. Begge trin-funktioner returnerer det nye feltindeks, eller -1, når siden slet ingen felter har. Den løkning er pr. side, ikke pr. dokument, hvilket betyder, at det er dit job, ikke bibliotekets, at krydse til den næste side. Sammenlign det returnerede indeks med det, du startede fra, læg mærke til, når det har wrappet, og fremryk selv PageNumber, hvis formularen er tænkt til at læses som én sammenhængende sekvens. Spring det tjek over, og en to-siders formular fanger stille markøren på side ét, hvilket er sin egen variant af klagen om ødelagt Tab

Traversering bliver nyttig, når resten af UI'et reagerer på den. OnFormFieldEnter-hændelsen affyres, når fokus ankommer, og på fremviseren rapporterer OnFormFieldFocusChange det nye feltindeks, så et sidepanel kan holde trit med, hvad tastaturet lige har valgt. Når du har brug for den omvendte mapning, fra en skærmposition til et felt, klarer den indekserede egenskab FormFieldAt hit-testingen til tooltip-forhåndsvisninger og klik-og-rediger-paneler. Der er en stille tilgængelighedsgevinst i alt dette: fordi fokus følger dokumentets egen feltrækkefølge, er den sti, du forbinder til Tab-tasten, den samme sti, en skærmlæser annoncerer, uden ekstra arbejde

At vise feltnavne i stedet for rå indekstal kræver én egenskab mere. FormFieldInfo[] returnerer en TPdfFormFieldInfo-record pr. indeks, der bærer feltnavnet, typen, skriftstørrelsen, checked-tilstanden, eksportværdien og gruppemedlemskabet, hvilket er, hvad en navigationsliste bør vise ("Field 4 of 17: InvoiceDate" frem for "4"). Radioknap-grupper er tilfældet, der er en dedikeret testfil værd. Flere widgets kan dele ét enkelt feltnavn, så en liste samlet naivt fra widgets viser den samme gruppe flere gange og forvirrer alle, der læser den

Hvorfor udfyldte værdier kommer ud tomme, og kaldet der retter det

Den anden klage, der fylder supportkøerne, er mere alarmerende end en Tab-tast, der opfører sig dårligt: en formular bliver udfyldt programmatisk, kunden åbner den i Acrobat, og hvert felt ser tomt ud. Klik ind i et felt, og dets værdi springer i syne. Dataene har været i filen hele tiden. Det, der mangler, er billedet af dataene, og grunden er værd at forstå én gang, fordi den forklarer en hel familie af bugs

Et AcroForm-tekstfelt gemmer sin værdi i /V-posten i feltets dictionary (ISO 32000-1 §12.7.3.3). Det, en fremviser rent faktisk maler, er noget separat: widgetens appearance stream under /AP (§12.5.5), et lille forudrenderet uddrag af indhold. Skriv /V, og lad /AP være i fred, og de to driver fra hinanden. Værdien er der; den renderede version af den er forældet eller fraværende. Acrobat genopbygger tilfældigvis et felts appearance, når det får fokus, hvilket er hele forklaringen på værdier, der kun viser sig ved klik. Det gamle NeedAppearances-flag, som bad fremvisere om at regenerere appearances for dig, virkede aldrig ensartet og er forældet i PDF 2.0, og print-servere og thumbnail-generatorer ignorerer det fuldstændig. De maler /AP og intet andet, så hvis /AP er tomt, printer de en tom boks

At tildele en værdi gennem FormField[i] skriver kun /V. Det er derfor, udfyldning af en formular er en tre-trins sekvens, og trinnet, teams dropper, er det midterste:

Diagram over /V-værdien versus /AP appearance-driften i AcroForm-felter og den tretrins Delphi udfyldelsessekvens bygget rundt om GenerateFormAppearances
At tildele værdier skriver kun /V, og det midterste trin er det, der genskriver det, print-servere faktisk renderer
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];   // skriver kun /V

  // Genopbyg /AP appearance streams; uden dette ser formularen
  // tom ud i Acrobat, indtil hvert felt bliver klikket
  Pdf.GenerateFormAppearances;

  Pdf.SaveAs(OutputPath);
end;

GenerateFormAppearances er hele løsningen. Den genopbygger hver widgets appearance stream ud fra de aktuelle værdier, skrifttyper og quadding, så en fremviser, der aldrig kører en fokus-hændelse, en print-server eller en thumbnailer, maler den udfyldte tilstand alligevel. Kald den én gang efter batchen af tildelinger, ikke én gang pr. felt. Appearance-generering udfører reelt layoutarbejde, og pr.-felt-kald ganger det op over en stor formular til ingen nytte

At regenerere appearances er også det øjeblik, skrifttyper og justering gør sig gældende, hvilket er kilden til en overraskelse af anden orden. Den nye stream lægger hver værdi ud inden i widget-rektanglet ved brug af feltets skrifttype, størrelse og quadding. En værdi, der sidder behageligt i din testformular, kan klippes eller krympe i en kundes kopi, hvor det samme felt er smallere. Auto-størrelsesfelter (skriftstørrelse nul) krymper teksten, så den passer; faste størrelser klipper den bare. Begge er lovlige, og den eneste ærlige måde at vide, hvilken en given formular gør, er at kigge på det regenererede output frem for den streng, du skrev. Når nogen rapporterer tekst klippet af ved kanten af en boks, er dette næsten altid grunden

Behandl verifikation som en del af at færdiggøre arbejdet, ikke en eftertanke. Åbn den gemte fil i Acrobat, og bekræft, at værdierne er synlige, før du rører noget felt. Print den derefter til PDF eller til et billede fra en anden fremviser, én der helt ignorerer formularlogik, og bekræft, at værdierne overlever den sti også. Tilsammen fanger de to tjek hver variant af /V-versus-/AP-driften

Feltkonfigurationer, der består demoen og fejler i marken

Rene demo-formularer skjuler et sæt kantcases, som kundefiler ikke gør. Fire af dem tegner sig for de fleste af "it worked on my machine"-rapporterne

  • Afkrydsningsfelt-eksportværdier. "on"-tilstanden er ikke altid Yes. En formular kan frit definere sin egen eksportværdi, og at skrive den forkerte streng efterlader boksen visuelt umarkeret, mens din kode er overbevist om, at den satte den. Læs eksportværdien fra FormFieldInfo[] frem for at antage én
  • Radioknap-grupper med delt navn. Ét felt, flere widgets. Den værdi, du tildeler, afgør, hvilket widget læses som valgt, så UI-kode, der antager, at ét navn mapper til ét rektangel, ender med at tegne fokusringen på den forkerte knap
  • Beregnede felter. Totaler vedligeholdt af dokument-JavaScript opdaterer som svar på felt-hændelser. En programmatisk udfyldning, der springer de hændelser over, må enten udløse genberegning eller overskrive de beregnede felter direkte. En formular, hvor linjeposterne og totalen er uenige, er værre end begge løsninger
  • Skjulte påkrævede felter. Betingede formularer skjuler felter, der stadig er markeret som påkrævet. Beslut på forhånd, om din validering respekterer synlighed eller det rå required-flag, og skriv så den beslutning ned et sted, support kan finde den

Én distinktion er værd at få på plads, før den bider dig: at generere appearances er ikke det samme som flattening. GenerateFormAppearances gør værdier synlige overalt, mens felterne forbliver redigerbare. Flattening bager appearance ind i statisk sideindhold og fjerner interaktiviteten for godt, hvilket er rigtigt for en arkivkopi og forkert for en formular, den næste person stadig skal udfylde. Hvis FormType rapporterer ftXfaFull frem for ftAcroForm, gælder ingen af redigeringsfladerne her rent alligevel, da dokumentet renderer fra sin egen XML-skabelon; opdag det tilfælde, og fortæl brugeren det, frem for at lade dem finde grænsen på egen hånd

Form-fill-undersystemet, fokustraversering og appearance-generering vist her er en del af PDFium Component til Delphi, C++Builder og Lazarus/FPC. Hvis din fremviser også håndterer reviewer-markup ved siden af formulardata, dækker artiklen om annotations-review den tilstødende model