Technisch artikel

PDF-formulierveldnavigatie in Delphi (PDFium Component)

Druk op Tab in een PDF-formulier dat uw code bouwde, en de cursor landt twee velden verder dan waar hij hoorde, of slaat de tweede kolom helemaal over, of springt na het derde veld terug naar de top in plaats van naar het vierde. Degene die een factuur invult in uw viewer verwacht dat het toetsenbord het formulier doorloopt zoals het elk webformulier doorloopt dat ze ooit gebruikten. Doet hij dat niet, dan grijpen ze naar de muis, zoeken naar het volgende vakje en besluiten geruisloos dat uw gereedschap onaf is. Voorspelbare veldvolgorde is het verschil tussen een data-invoerviewer die mensen tolereren en een die ze vertrouwen, en het is vrijwel geheel een kwestie van de juiste focus-API gebruiken in plaats van toetsenbordinvoer te faken met gesimuleerde klikken

De voorbeelden hieronder gebruiken PDFium Component, een op PDFium-gebaseerde VCL/LCL-component voor Delphi, C++Builder en Lazarus. Navigatie is één van drie dingen die een formulier-viewer goed moet krijgen; de andere twee, het formulier correct openen en ingevulde waarden opslaan zodat ze werkelijk verschijnen, zijn waar de meeste verrassingen zich verbergen, dus alle drie worden hieronder behandeld

Een formulier openen: FormFill, FormType en de XFA-vraag

Veldtoegang vereist het form-fill-subsysteem, bestuurd door de eigenschap FormFill, om ingeschakeld te zijn voordat het document wordt geopend. Eenmaal actief vertelt FormType u wat voor formulier u tegenover zich hebt, en het antwoord verandert de featureset die u kunt beloven:

Diagram van FormFill-instelling en FormType-detectietakken in een Delphi PDFium Component-viewer, splitsend in ftNone-, ftAcroForm- en ftXfaFull-afhandeling
FormType vertakt zodra FormFill is ingeschakeld, en elke tak belooft een andere featureset
Pdf.FileName := FormPath;
Pdf.FormFill := True;   // aanzetten vóór Active; verplicht voor elke veldtoegang
Pdf.Active := True;

case Pdf.FormType of
  ftNone:
    DisableFormPanel('This document has no interactive form');
  ftAcroForm:
    BuildFieldList;     // volledige veldnavigatie en -bewerking beschikbaar
  ftXfaFull:
    ShowXfaNotice;      // XFA rendert vanuit zijn eigen XML-template;
                        // beschouw veldbewerking als beperkt
end;

Twee praktische opmerkingen volgen uit die switch. AcroForm is het standaard ISO 32000-formuliermodel en het is waar elke API hierop richt. XFA-documenten sluiten hun eigen XML-formulierarchitectuur in, dus een klant volledige XFA-bewerking beloven na een snelle AcroForm-demo is een toezegging waar u spijt van krijgt. De tweede opmerking gaat over bijwerkingen: FormFill op True zetten initialiseert ook document-JavaScript. In een data-invoerviewer is dat precies goed, want berekeningsscripts zijn wat een lopend totaal actueel houden terwijl iemand typt. In een voorbeeldvenster voor bestanden van onbekende herkomst is het precies verkeerd. Het artikel over veilig PDF-voorbeeld behandelt de FormFill := False-kant van die afweging

Tab-toets-volgorde die landt waar gebruikers verwachten

Terug naar het toetsenbordprobleem van boven. De verleiding is om Tab te faken door een muisklik te synthetiseren op de rechthoek van de volgende widget, wat breekt zodra een veld uit het scherm is gescrold of twee widgets overlappen. De focus-API verplaatst de eigen focus van het formulier direct, zonder giswerk met geometrie. Vijf aanroepen dekken het: FocusFormField op index, FocusNextFormField en FocusPreviousFormField voor stappen, FocusedFormFieldIndex om te lezen waar u bent, en ClearFormFieldFocus om focus helemaal te laten vallen

Diagram van Tab-toets-focustraversal in een Delphi PDFium Component-viewer waarin FocusNextFormField binnen de tabvolgorde van één pagina doorrolt en vijf focus-API's toetsenbordnavigatie dekken
Traversering loopt binnen de tabvolgorde van één pagina, dus oversteken naar de volgende pagina blijft de taak van de viewer
procedure TFormViewer.HandleTabKey(Shift: TShiftState);
begin
  if ssShift in Shift then
    PdfView.FocusPreviousFormField
  else
    PdfView.FocusNextFormField;
  UpdateFieldStatus;  // e.g. "Field 4 of 17: InvoiceDate"
end;

Het ene stuk gedrag dat mensen laat struikelen is de wrap. Volgorde werkt door de tab-volgorde van de huidige pagina en loopt daarbinnen: stap voorbij het laatste veld en u bent terug bij het eerste. Beide stapfuncties retourneren de nieuwe veldindex, of -1 wanneer de pagina helemaal geen velden bevat. Dat lussen is per pagina, niet per document, wat betekent dat oversteken naar de volgende pagina uw taak is, niet die van de bibliotheek. Vergelijk de geretourneerde index met degene waarmee u begon, merk wanneer hij gewrapt is, en schuif PageNumber zelf vooruit als het formulier als één doorlopende reeks moet lezen. Sla die controle over en een formulier van twee pagina's vangt de cursor geruisloos op pagina één, wat zijn eigen variant van de kapotte-Tab-klacht is

Volgorde wordt nuttig zodra de rest van de UI erop reageert. De OnFormFieldEnter-event vuurt wanneer focus aankomt, en op de viewer rapporteert OnFormFieldFocusChange de nieuwe veldindex, zodat een zijpaneel in de pas blijft met wat het toetsenbord zojuist selecteerde. Wanneer u de omgekeerde afbeelding nodig hebt, van een schermpositie naar een veld, doet de geïndexeerde eigenschap FormFieldAt de hit-testing voor tooltip-voorbeelden en klik-om-te-bewerken-panelen. Er zit een stil toegankelijkheidsvoordeel in dit alles: omdat focus de eigen veldvolgorde van het document volgt, is het pad dat u voor de Tab-toets bekabelt hetzelfde pad dat een schermlezer aankondigt, zonder extra werk

Veldnamen tonen in plaats van ruwe indexcijfers kost één extra eigenschap. FormFieldInfo[] retourneert een TPdfFormFieldInfo-record per index, met de veldnaam, het type, de lettergrootte, de aangevinkte staat, de exportwaarde en het groepslidmaatschap, wat een navigatielijst zou moeten tonen ("Field 4 of 17: InvoiceDate" eerder dan "4"). Radiogroepen zijn het geval dat een toegewijd testbestand verdient. Meerdere widgets kunnen één veldnaam delen, dus een lijst naïef uit widgets samengesteld toont dezelfde groep meerdere keren en verwart iedereen die hem leest

Waarom ingevulde waarden leeg uitkomen, en de aanroep die het oplost

De andere klacht die support-wachtrijen vult is verontrustender dan een misdragende Tab-toets: een formulier wordt programmatisch ingevuld, de klant opent het in Acrobat, en elk veld oogt leeg. Klik in een veld en zijn waarde klikt in beeld. De data staat de hele tijd in het bestand. Wat ontbreekt is de afbeelding van de data, en de reden is de moeite waard om één keer te begrijpen want hij verklaart een hele familie van bugs

Een AcroForm-tekstveld slaat zijn waarde op in de /V-entry van het veldwoordenboek (ISO 32000-1 §12.7.3.3). Wat een viewer werkelijk verft is iets afzonderlijks: de appearance-stream van de widget onder /AP (§12.5.5), een klein vooraf gerenderd inhoudsfragmentje. Schrijf /V en laat /AP met rust, en de twee drijven uit elkaar. De waarde is er; de gerenderde versie ervan is verouderd of afwezig. Acrobat toevallig herbouwt de appearance van een veld wanneer het focus krijgt, wat de hele verklaring is voor waarden die alleen bij klik verschijnen. De oude NeedAppearances-vlag, die viewers vroeg om appearances voor u te regenereren, werkte nooit uniform en is in PDF 2.0 afgeschaft, en print-servers en miniatuurgeneratoren negeren hem volledig. Ze verven /AP en niets anders, dus als /AP leeg is printen ze een leeg vakje

Waarden toewijzen via FormField[i] schrijft alleen /V. Daarom is een formulier vullen een reeks van drie stappen, en de stap die teams laten vallen is de middelste:

Diagram van de /V-waarde versus /AP-appearance-drift in AcroForm-velden en de driestaps-Delphi-vulreeks gebouwd rond GenerateFormAppearances
Waarden toekennen schrijft alleen /V, en de middelste stap is degene die herschildert wat print servers werkelijk renderen
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];   // writes /V only

  // Bouw de /AP appearance streams opnieuw op; zonder dit ziet het
  // formulier er in Acrobat leeg uit tot je elk veld aanklikt
  Pdf.GenerateFormAppearances;

  Pdf.SaveAs(OutputPath);
end;

GenerateFormAppearances is de hele oplossing. Hij herbouwt elke widget z'n appearance-stream vanuit de huidige waarden, fonts en quadding, zodat een viewer die nooit een focus-event draait, een print-server of een miniatuurgenerator, de ingevulde staat toch verft. Roep hem één keer na de reeks toewijzingen, niet één keer per veld. Appearance-generatie doet echt layout-werk, en aanroepen per veld vermenigvuldigen dat over een groot formulier voor niets

Appearances regenereren is ook het moment waarop fonts en uitlijning zich laten gelden, wat de bron is van een verrassing van de tweede orde. De nieuwe stream legt elke waarde uit binnen de widget-rechthoek met het font, de grootte en de quadding van het veld. Een waarde die comfortabel in uw testformulier zit kan afknippen of krimpen in een exemplaar van een klant waar hetzelfde veld smaller is. Velden met automatische grootte (lettergrootte nul) krimpen de tekst om te passen; velden met vaste grootte knippen hem gewoon af. Beide zijn legaal, en de enige eerlijke manier om te weten welke een gegeven formulier doet is kijken naar de geregenereerde uitvoer eerder dan de string die u schreef. Wanneer iemand tekst meldt die aan de rand van een vakje is afgesneden, is dit vrijwel altijd de reden

Behandel verificatie als onderdeel van het afronden van het werk, niet als een bijzaak. Open het opgeslagen bestand in Acrobat en bevestig dat de waarden zichtbaar zijn voordat u een veld aanraakt. Print het dan naar PDF of naar een afbeelding vanuit een andere viewer, een die formulierlogica helemaal negeert, en bevestig dat de waarden dat pad ook overleven. Samen vangen die twee controles elke variant van de /V-tegen-/AP-drift

Veldconfiguraties die de demo passeren en in het veld falen

Schone demo-formulieren verbergen een verzameling edge-cases die klantbestanden niet verbergen. Vier ervan verklaren de meeste "it worked on my machine"-rapporten

  • Checkbox-exportwaarden. De "aan"-staat is niet altijd Yes. Een formulier is vrij om zijn eigen exportwaarde te definiëren, en de verkeerde string schrijven laat het vakje visueel uitgeschakeld terwijl uw code ervan overtuigd is dat hij het instelde. Lees de exportwaarde uit FormFieldInfo[] in plaats van er één aan te nemen
  • Radiogroepen met gedeelde naam. Eén veld, meerdere widgets. De waarde die u toewijst beslist welke widget als geselecteerd leest, dus UI-code die aanneemt dat één naam naar één rechthoek wijst uiteindelijk de focusring op de verkeerde knop tekent
  • Berekende velden. Totalen onderhouden door document-JavaScript werken bij in reactie op veld-events. Een programmatische invulling die die events omzeilt moet óf herberekening triggeren óf de berekende velden direct overschrijven. Een formulier waar de regelitems en het totaal het oneens zijn is erger dan beide fixes
  • Verborgen verplichte velden. Conditionele formulieren verbergen velden die nog steeds als verplicht zijn gemarkeerd. Besluit vooraf of uw validatie zichtbaarheid respecteert of de ruwe verplicht-vlag, en schrijf die beslissing ergens op waar support hem kan vinden

Eén onderscheid is de moeite waard om te settelen voordat het u bijt: appearances genereren is niet flattenen. GenerateFormAppearances maakt waarden overal zichtbaar terwijl het de velden bewerkbaar laat. Flattenen bakelt de appearance in statische pagina-inhoud en stroopt de interactiviteit voor altijd, wat juist is voor een archiefexemplaar en verkeerd voor een formulier dat de volgende persoon nog moet invullen. Als FormType ftXfaFull rapporteert eerder dan ftAcroForm, is geen van het bewerkingsoppervlak hier sowieso schoon van toepassing, aangezien het document vanuit zijn eigen XML-sjabloon rendert; detecteer dat geval en vertel het de gebruiker, in plaats van hen de limiet zelf te laten vinden

Het form-fill-subsysteem, focusvolgorde en appearance-generatie die hier worden getoond zijn onderdeel van PDFium Component voor Delphi, C++Builder en Lazarus/FPC. Als uw viewer ook review-markup naast formulierdata afhandelt, behandelt het artikel over annotatie-review dat aangrenzende model