Technický článek

Navigace ve formulářových polích PDF v Delphi (komponenta PDFium)

Stiskněte Tab ve formuláři PDF, který sestavil váš kód, a kurzor skočí o dvě pole dál, než by měl, nebo úplně přeskočí druhý sloupec, nebo se po třetím poli vrátí zpátky nahoru místo na čtvrté. Člověk, který ve vaší prohlížečce vyplňuje fakturu, čeká, že klávesnice bude procházet formulář stejně, jako to dělá u každého webového formuláře, se kterým se kdy setkal. Když se to nestane, sáhne po myši, hledá další políčko a potichu si usoudí, že váš nástroj je nedodělaný. Předvídatelné procházení polí je rozdíl mezi prohlížečkou pro zadávání dat, kterou lidé jen snáší, a takovou, které důvěřují, a je to téměř výhradně otázka použití správného API pro fokus místo předstírání klávesnicového vstupu simulovanými kliknutími

Příklady níže používají PDFium Component, komponentu VCL/LCL postavenou na PDFiu pro Delphi, C++Builder a Lazarus. Navigace je jedna ze tří věcí, které musí prohlížečka formulářů zvládnout správně; ve zbylých dvou, správném otevření formuláře a uložení vyplněných hodnot tak, aby se skutečně zobrazily, se skrývá většina překvapení, takže všechny tři jsou pokryté níže

Otevření formuláře: FormFill, FormType a otázka XFA

Přístup k polím vyžaduje, aby byl před otevřením dokumentu povolen subsystém pro vyplňování formulářů, řízený vlastností FormFill. Jakmile je aktivní, FormType vám řekne, jaký typ formuláře máte před sebou, a odpověď mění sadu funkcí, kterou můžete slíbit:

Diagram větví nastavení FormFill a detekce FormType v prohlížeči PDFium Component v Delphi, štěpících zacházení ftNone, ftAcroForm a ftXfaFull
FormType se větví, jakmile se povolí FormFill, a každá větev slibuje jinou sadu funkcí
Pdf.FileName := FormPath;
Pdf.FormFill := True;   // povolit před Active; vyžadováno pro jakýkoli přístup k polím
Pdf.Active := True;

case Pdf.FormType of
  ftNone:
    DisableFormPanel('This document has no interactive form');
  ftAcroForm:
    BuildFieldList;     // plná navigace i editace polí je k dispozici
  ftXfaFull:
    ShowXfaNotice;      // XFA se vykresluje z vlastní šablony XML;
                        // editaci polí považujte za omezenou
end;

Z tohoto přepínače plynou dvě praktické poznámky. AcroForm je standardní formulářový model ISO 32000, a je to to, na co míří každé API zde. Dokumenty XFA vkládají vlastní XML architekturu formuláře, takže slíbit zákazníkovi plnou editaci XFA po rychlém demu AcroForm je závazek, kterého budete litovat. Druhá poznámka se týká vedlejších účinků: nastavení FormFill na True zároveň inicializuje JavaScript dokumentu. V prohlížečce pro zadávání dat je to přesně správně, protože výpočetní skripty jsou to, co udržuje průběžný součet aktuální, zatímco někdo píše. V náhledovém okně pro soubory neznámého původu je to naopak přesně špatně. Článek o bezpečném náhledu PDF pokrývá stranu FormFill := False tohoto kompromisu

Procházení klávesou Tab, které skončí tam, kde to uživatelé čekají

Zpátky k problému s klávesnicí z úvodu. Pokušením je předstírat Tab syntetizováním kliknutí myší na obdélník dalšího widgetu, což se rozbije ve chvíli, kdy je pole odscrollované mimo obrazovku nebo se dva widgety překrývají. API pro fokus místo toho posouvá přímo vlastní fokus formuláře, bez jakéhokoli hádání geometrie. Pokrývá to pět volání: FocusFormField podle indexu, FocusNextFormField a FocusPreviousFormField pro krokování, FocusedFormFieldIndex pro přečtení, kde se nacházíte, a ClearFormFieldFocus pro úplné zrušení fokusu

Diagram průchodu fokusem klávesou Tab v prohlížeči PDFium Component v Delphi, kde FocusNextFormField se zalamuje v tab pořadí jedné strany a pět focus API pokrývá navigaci klávesnicí
Procházení se smyčkuje uvnitř tab pořadí jedné stránky, takže přechod na další stránku zůstává úkolem prohlížeče
procedure TFormViewer.HandleTabKey(Shift: TShiftState);
begin
  if ssShift in Shift then
    PdfView.FocusPreviousFormField
  else
    PdfView.FocusNextFormField;
  UpdateFieldStatus;  // např. „Pole 4 z 17: InvoiceDate"
end;

Jeden kus chování, o který lidé zakopávají, je zalamování. Procházení funguje přes pořadí tabulátoru aktuální stránky a v jeho rámci se smyčkuje: přejdete-li poslední pole, jste zpátky na prvním. Obě krokovací funkce vrací nový index pole, nebo -1, když stránka neobsahuje žádná pole. Toto zalamování je na úrovni stránky, ne dokumentu, což znamená, že přechod na další stránku je vaše práce, ne práce knihovny. Porovnejte vrácený index s tím, od kterého jste začali, všimněte si, kdy se zalomil, a sami posuňte PageNumber, pokud má formulář fungovat jako jedna souvislá sekvence. Vynechte tuto kontrolu a dvoustránkový formulář potichu uvězní kurzor na první stránce, což je svébytná varianta stížnosti na rozbitý Tab

Procházení se stane užitečným ve chvíli, kdy na něj zareaguje zbytek UI. Událost OnFormFieldEnter se vyvolá při příchodu fokusu, a na prohlížečce OnFormFieldFocusChange hlásí nový index pole, takže postranní panel může držet krok s tím, co právě vybrala klávesnice. Když potřebujete opačné mapování, z pozice na obrazovce na pole, indexovaná vlastnost FormFieldAt provádí hit-testing pro náhledy tooltipů a panely typu klikni-a-uprav. Je v tom i tichý přínos pro přístupnost: protože fokus následuje vlastní pořadí polí dokumentu, cesta, kterou zapojíte pro klávesu Tab, je stejná cesta, kterou ohlásí čtečka obrazovky, bez jakékoli práce navíc

Zobrazení názvů polí místo syrových indexů vyžaduje ještě jednu vlastnost. FormFieldInfo[] vrací záznam TPdfFormFieldInfo pro každý index, nesoucí název pole, typ, velikost fontu, stav zaškrtnutí, exportní hodnotu a členství ve skupině, což je to, co by měl navigační seznam zobrazovat („Pole 4 z 17: InvoiceDate" místo „4"). Radio skupiny jsou případ, který si zaslouží vyhrazený testovací soubor. Několik widgetů může sdílet jediný název pole, takže seznam sestavený naivně z widgetů zobrazí stejnou skupinu vícekrát a zmate každého, kdo ho čte

Proč vyplněné hodnoty vycházejí prázdné a volání, které to opraví

Druhá stížnost, která plní fronty supportu, je znepokojivější než zlobivá klávesa Tab: formulář se vyplní programově, zákazník ho otevře v Acrobatu, a každé pole vypadá prázdné. Klikněte do pole a jeho hodnota se okamžitě zobrazí. Data jsou v souboru celou dobu. Chybí obraz těch dat, a důvod stojí za to pochopit jednou, protože vysvětluje celou rodinu chyb

Textové pole AcroForm ukládá svou hodnotu do položky /V slovníku pole (ISO 32000-1 §12.7.3.3). To, co prohlížečka skutečně vykreslí, je něco odděleného: appearance stream widgetu pod /AP (§12.5.5), malý předrenderovaný útržek obsahu. Zapište /V a nechte /AP beze změny, a ty dva se od sebe rozejdou. Hodnota tam je; vykreslená verze je zastaralá nebo chybí. Acrobat náhodou přestaví vzhled pole ve chvíli, kdy získá fokus, což je celé vysvětlení pro hodnoty, které se objeví jen po kliknutí. Starý příznak NeedAppearances, který žádal prohlížečky, aby za vás vzhledy přegenerovaly, nikdy nefungoval jednotně a v PDF 2.0 je zavržený (deprecated), a tiskové servery i generátory náhledů ho zcela ignorují. Vykreslují /AP a nic jiného, takže pokud je /AP prázdné, vytisknou prázdné políčko

Přiřazení hodnoty přes FormField[i] zapíše jen /V. Proto je vyplnění formuláře třístupňová sekvence, a krok, který týmy vynechávají, je ten prostřední:

Diagram rozchodu hodnoty /V proti vzhledu /AP v polích AcroForm a třístupňové fill posloupnosti Delphi stavěné kolem GenerateFormAppearances
Přiřazování hodnot zapisuje jen /V a prostřední krok je tím, co překreslí to, co tiskové servery skutečně renderují
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];   // zapisuje jen /V

  // Znovu sestavit appearance streamy /AP; bez toho formulář
  // v Acrobatu vypadá prázdně, dokud se na každé pole neklikne
  Pdf.GenerateFormAppearances;

  Pdf.SaveAs(OutputPath);
end;

GenerateFormAppearances je celá oprava. Přestaví appearance stream každého widgetu z aktuálních hodnot, fontů a quaddingu, takže prohlížečka, která nikdy nespustí událost fokusu, tiskový server nebo generátor náhledů, přesto vykreslí vyplněný stav. Zavolejte ho jednou po dávce přiřazení, ne jednou na pole. Generování vzhledu odvádí skutečnou práci s rozvržením, a volání pro každé pole zvlášť to zbytečně znásobí napříč velkým formulářem

Regenerace vzhledů je také chvíle, kdy se prosadí fonty a zarovnání, což je zdroj překvapení druhého řádu. Nový stream rozloží každou hodnotu uvnitř obdélníku widgetu s použitím fontu, velikosti a quaddingu daného pole. Hodnota, která se pohodlně vejde do vašeho testovacího formuláře, se může v zákazníkově kopii, kde je stejné pole užší, oříznout nebo zmenšit. Pole s automatickou velikostí (velikost fontu nula) text zmenší, aby se vešel; pole s pevnou velikostí ho prostě ořežou. Obojí je legální, a jediný poctivý způsob, jak zjistit, co daný formulář dělá, je podívat se na regenerovaný výstup, ne na řetězec, který jste zapsali. Když někdo nahlásí text useknutý na okraji políčka, tohle je téměř vždy důvod

Berte ověření jako součást dokončení práce, ne jako dodatečný nápad. Otevřete uložený soubor v Acrobatu a potvrďte, že hodnoty jsou viditelné, ještě než se dotknete jakéhokoli pole. Pak ho vytiskněte do PDF nebo do obrázku z jiné prohlížečky, takové, která formulářovou logiku zcela ignoruje, a potvrďte, že hodnoty přežijí i tuto cestu. Dohromady tyto dvě kontroly zachytí každou variantu rozjetí /V vůči /AP

Konfigurace polí, které projdou demem a selžou v praxi

Čisté demo formuláře skrývají sadu krajních případů, které zákaznické soubory neskrývají. Čtyři z nich stojí za většinou hlášení typu „na mém počítači to fungovalo"

  • Exportní hodnoty zaškrtávacích polí. Stav „zapnuto" není vždy Yes. Formulář si může definovat vlastní exportní hodnotu, a zapsání špatného řetězce nechá políčko vizuálně nezaškrtnuté, zatímco váš kód je přesvědčen, že ho nastavil. Přečtěte exportní hodnotu z FormFieldInfo[], místo abyste ji předpokládali
  • Radio skupiny se sdíleným názvem. Jedno pole, několik widgetů. Hodnota, kterou přiřadíte, rozhoduje, který widget se čte jako vybraný, takže kód UI, který předpokládá, že jeden název odpovídá jednomu obdélníku, nakonec nakreslí prstenec fokusu na špatné tlačítko
  • Vypočítávaná pole. Součty udržované JavaScriptem dokumentu se aktualizují v reakci na události polí. Programové vyplnění, které tyto události obchází, musí buď vyvolat přepočet, nebo přímo přepsat vypočítávaná pole. Formulář, ve kterém se položky a celkový součet neshodují, je horší než kterékoli z těchto řešení
  • Skrytá povinná pole. Podmíněné formuláře skrývají pole, která jsou pořád označená jako povinná. Rozhodněte předem, jestli vaše validace respektuje viditelnost, nebo syrový příznak required, a pak si toto rozhodnutí někam zapište, kde ho support najde

Jedno rozlišení stojí za ujasnění dřív, než vás kousne: generování vzhledů není zplošťování (flattening). GenerateFormAppearances zpřístupní hodnoty všude a zároveň nechá pole editovatelná. Zploštění zapeče vzhled do statického obsahu stránky a natrvalo odstraní interaktivitu, což je správné pro archivní kopii a špatné pro formulář, který ještě musí vyplnit další člověk. Pokud FormType hlásí ftXfaFull místo ftAcroForm, žádná z popsaných editačních možností se stejně nedá čistě použít, protože se dokument vykresluje z vlastní šablony XML; tento případ detekujte a řekněte to uživateli, místo abyste ho nechali narazit na tento limit sám

Subsystém pro vyplňování formulářů, procházení fokusu a generování vzhledů ukázané zde jsou součástí PDFium Component pro Delphi, C++Builder a Lazarus/FPC. Pokud vaše prohlížečka vedle dat formuláře řeší i recenzentské značky, článek o recenzi anotací pokrývá tento sousední model