Technical Article

Navigácia vo formulárových poliach PDF v Delphi (PDFium Component)

Stlačíte kláves Tab vo formulári PDF vytvorenom vaším kódom a kurzor skončí o dve polia ďalej, než by mal, prípadne úplne vynechá druhý stĺpec alebo skočí späť na začiatok po treťom poli namiesto štvrtého. Osoba vypĺňajúca faktúru vo vašom prehliadači očakáva, že pohyb po formulári pomocou klávesnice bude fungovať rovnako ako na každom webovom formulári, ktorý kedy použila. Ak to tak nie je, siahne po myši, začne hľadať ďalšie políčko a v duchu zhodnotí váš nástroj ako nedokončený. Predvídateľné prechádzanie polí je rozdielom medzi prehliadačom na zadávanie údajov, ktorý ľudia iba tolerujú, a takým, ktorému dôverujú. Ide takmer výlučne o používanie správneho rozhrania API pre prácu s fokusom namiesto predstierania vstupu z klávesnice pomocou simulovaných kliknutí.

Príklady nižšie používajú PDFium Component, komponent VCL/LCL založený na PDFium pre Delphi, C++Builder a Lazarus. Navigácia je jednou z troch vecí, ktoré musí prehliadač formulárov zvládnuť správne. Ďalšie dve – správne otvorenie formulára a uloženie vyplnených hodnôt tak, aby sa skutočne zobrazili – sú miesta, kde sa skrýva najviac prekvapení. Preto sa v článku venujeme všetkým trom oblastiam.

Otvorenie formulára: FormFill, FormType, a otázka XFA

Prístup k poliam vyžaduje, aby bol podsystém vypĺňania formulárov (riadený vlastnosťou FormFill) povolený ešte pred otvorením dokumentu. Po aktivácii vám vlastnosť FormType povie, s akým typom formulára pracujete, a odpoveď zmení sadu funkcií, ktoré môžete garantovať:

Pdf.FileName := FormPath;
Pdf.FormFill := True;   // enable before Active; required for any field access
Pdf.Active := True;

case Pdf.FormType of
  ftNone:
    DisableFormPanel('This document has no interactive form');
  ftAcroForm:
    BuildFieldList;     // full field navigation and editing available
  ftXfaFull:
    ShowXfaNotice;      // XFA renders from its own XML template;
                        // treat field editing as limited
end;

Z tohto prepínača vyplývajú dve praktické poznámky. AcroForm je štandardný model formulárov podľa normy ISO 32000 a práve naň sa zameriava každé tu uvedené API. Dokumenty XFA obsahujú vlastnú XML architektúru formulárov, takže sľúbiť zákazníkovi plnú podporu úprav XFA po rýchlej ukážke AcroForm je záväzok, ktorý budete neskôr ľutovať.

Druhá poznámka sa týka vedľajších účinkov: nastavenie vlastnosti FormFill na hodnotu True inicializuje aj JavaScript v dokumente. V prehliadači určenom na zadávanie údajov je to úplne správne, pretože výpočtové skripty udržiavajú priebežný súčet aktuálny počas písania. V okne náhľadu súborov neznámeho pôvodu je to však nesprávne. Článok o bezpečnom náhľade PDF sa zaoberá druhou stranou tohto kompromisu s nastavením FormFill := False.

Prechádzanie klávesom Tab, ktoré skončí tam, kde používateľ očakáva

Späť k problému s klávesnicou spomenutému v úvode. Často sa objavuje pokušenie simulovať kláves Tab umelým vytvorením kliknutia myši na obdĺžnik nasledujúceho prvku. To však zlyhá v okamihu, keď je pole posunuté mimo obrazovku alebo keď sa dva prvky prekrývajú. API pre prácu s fokusom namiesto toho presúva zameranie samotného formulára priamo, bez akýchkoľvek odhadov geometrie. Pokrýva to päť volaní: FocusFormField podľa indexu, FocusNextFormField a FocusPreviousFormField na prechádzanie, FocusedFormFieldIndex na zistenie aktuálnej pozície a ClearFormFieldFocus na úplné zrušenie zamerania.

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;

Správanie, na ktorom sa ľudia často pomýlia, je zacyklenie (wrap). Prechádzanie funguje podľa poradia tabelátorov na aktuálnej stránke a cyklí sa v ňom: prechod za posledné pole vás vráti na prvé. Obe funkcie krokovania vracajú index nového poľa alebo hodnotu -1, ak stránka neobsahuje žiadne polia. Toto zacyklenie prebieha v rámci jednej stránky, nie celého dokumentu, čo znamená, že prechod na ďalšiu stránku je vašou úlohou, nie úlohou knižnice. Porovnajte vrátený index s indexom, z ktorého ste začali, sledujte, kedy došlo k zacykleniu, a sami zmeňte PageNumber, ak má formulár fungovať ako jedna súvislá sekvencia. Ak túto kontrolu vynecháte, dvojstranový formulár potichu uväzní kurzor na prvej stránke, čo je ďalší variant sťažností na nefunkčný Tab.

Prechádzanie začne byť užitočné, keď naň reaguje zvyšok používateľského rozhrania. Udalosť OnFormFieldEnter sa spustí pri príchode fokusu a v prehliadači udalosť OnFormFieldFocusChange hlási index nového poľa, takže bočný panel môže držať krok s tým, čo klávesnica práve vybrala. Ak potrebujete spätné mapovanie z pozície na obrazovke na pole, indexovaná vlastnosť FormFieldAt vykonáva testovanie kliknutí (hit-testing) pre náhľady nástrojových tipov a panely kliknutím na úpravu. Celé to má aj skrytý prínos pre prístupnosť: keďže zameranie sleduje vlastné poradie polí v dokumente, cesta, ktorú nastavíte pre kláves Tab, je rovnaká, akú oznamuje čítačka obrazovky, a to bez akejkoľvek práce navyše.

Zobrazenie názvov polí namiesto surových indexových čísel vyžaduje ešte jednu vlastnosť. FormFieldInfo[] vracia záznam TPdfFormFieldInfo pre každý index, ktorý obsahuje názov poľa, typ, veľkosť písma, stav zaškrtnutia, exportovanú hodnotu a členstvo v skupine. To je presne to, čo by mal navigačný zoznam zobrazovať (napr. „Pole 4 zo 17: InvoiceDate“ namiesto „4“). Prepínače (Radio groups) sú prípadom, ktorý si zaslúži samostatný testovací súbor. Viaceré prvky môžu zdieľať jeden názov poľa, takže zoznam zostavený iba jednoduchým prechodom prvkov zobrazuje rovnakú skupinu viackrát a mätie používateľa.

Prečo sú vyplnené hodnoty prázdne a volanie, ktoré to opraví

Ďalšia sťažnosť, ktorá plní fronty podpory, je znepokojujúcejšia ako nesprávne fungujúci kláves Tab: formulár sa vyplní programovo, zákazník ho otvorí v Acrobate a každé pole vyzerá prázdne. Kliknutím do poľa sa jeho hodnota okamžite zobrazí. Údaje sú v súbore po celý čas. Chýba však obraz týchto údajov. Dôvor je dobré raz pochopiť, pretože vysvetľuje celú skupinu chýb.

Textové pole AcroForm ukladá svoju hodnotu do položky /V v slovníku poľa (ISO 32000-1 §12.7.3.3). Prehliadač však vykresľuje niečo iné: stream vzhľadu prvku pod /AP (§12.5.5), čo je malý predrenderovaný úryvok obsahu. Ak zapíšete hodnotu do /V a položku /AP ponecháte bez zmeny, obe hodnoty sa roídu. Hodnota existuje, ale jej vykreslená verzia je zastaraná alebo chýba. Acrobat zhodou okolností prebuduje vzhľad poľa v momente, keď získa zameranie (fokus), čo vysvetľuje, prečo sa hodnoty objavia až po kliknutí. Starý príznak NeedAppearances flag, ktorý žiadal prehliadače o regeneráciu vzhľadov, nikdy nefungoval jednotne a v PDF 2.0 je zastaraný. Tlačové servery a generátory náhľadov ho úplne ignorujú. Vykresľujú /AP a nič iné, takže ak je /AP prázdny, vytlačia prázdne pole.

Priradenie hodnoty cez FormField[i] zapíše iba hodnotu /V. Preto je vyplnenie formulára trojkrokový postup a krok, ktorý vývojové tímy najčastejšie vynechávajú, je ten prostredný:

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

  // Rebuild the /AP appearance streams; without this the form
  // looks blank in Acrobat until each field is clicked
  Pdf.GenerateFormAppearances;

  Pdf.SaveAs(OutputPath);
end;

Metóda GenerateFormAppearances predstavuje kompletné riešenie. Znova zostaví stream vzhľadu každého prvku na základe aktuálnych hodnôt, písiem a zarovnania, takže prehliadač, ktorý nikdy nespúšťa udalosť fokusu, tlačový server alebo generátor náhľadov, napriek tomu vykreslí vyplnený stav. Zavolajte ju raz po celej dávke priradení, nie pre každé pole samostatne. Generovanie vzhľadu vykonáva skutočnú prácu s rozložením (layoutom) a volania pre jednotlivé polia by zbytočne zaťažovali systém pri veľkých formulároch.

Regenerácia vzhľadu je tiež okamihom, kedy sa uplatnia písma a zarovnanie, čo je zdrojom ďalšieho prekvapenia. Nový stream usporiada každú hodnotu v obdĺžniku prvku s použitím písma, veľkosti a zarovnania daného poľa. Hodnota, ktorá sa bez problémov zmestí do vášho testovacieho formulára, sa môže v klientskej kópii orezať alebo zmenšiť, ak je rovnaké pole užšie. Polia s automatickou veľkosťou (veľkosť písma nula) zmenšia text tak, aby sa zmestil; polia s pevnou veľkosťou ho jednoducho orežú. Obe správania sú v súlade s normou a jediný spoľahlivý spôsob, ako zistiť, čo daný formulár robí, je pozrieť sa na regenerovaný výstup a nie na reťazec, ktorý ste zapísali. Ak niekto nahlási text orezaný na okraji rámčeka, dôvodom je takmer vždy toto.

Považujte overenie za súčasť dokončenia práce, nie za dodatočnú aktivitu. Otvorte uložený súbor v programe Acrobat a pred kliknutím na akékoľvek pole skontrolujte, či sú hodnoty viditeľné. Potom ho vytlačte do PDF alebo do obrázka z iného prehliadača, ktorý úplne ignoruje logiku formulárov, a overte, či sa hodnoty zachovajú aj tam. Tieto dve kontroly spoločne odhalia každú verziu rozdielu medzi /V a /AP.

Konfigurácie polí, ktoré prejdú testom, ale zlyhajú v reálnej prevádzke

Čisté predvádzacie formuláre skrývajú množstvo okrajových prípadov, ktoré sa v reálnych súboroch zákazníkov bežne vyskytujú. Štyri z nich sú zodpovedné za väčšinu hlásení typu „u mňa to funguje“.

  • Exportované hodnoty zaškrtávacích polí (Checkbox). Stav zapnutia nie je vždy iba Yes. Formulár si môže definovať vlastnú exportovanú hodnotu a zapísanie nesprávneho reťazca ponechá pole vizuálne nezaškrtnuté, hoci váš kód je presvedčený o jeho nastavení. Prečítajte si exportovanú hodnotu z FormFieldInfo[] namiesto jej predpokladania.
  • Skupiny prepínačov (Radio groups) so spoločným názvom. Jedno pole, viacero prvkov. Hodnota, ktorú priradíte, určuje, ktorý prvok sa načíta ako vybraný. UI kód, ktorý predpokladá, že jeden názov zodpovedá jednému obdĺžniku, tak skončí nakreslením zameriavacieho rámčeka na nesprávnom tlačidle.
  • Vypočítavané polia. Súčty udržiavané pomocou JavaScriptu v dokumente sa aktualizujú v reakcii na udalosti polí. Programové vyplnenie, ktoré tieto udalosti obchádza, musí buď spustiť prepočet, alebo priamo prepísať vypočítavané polia. Formulár, kde položky a celkový súčet nesúhlasia, je horší ako ktorékoľvek z týchto riešení.
  • Skryté povinné polia. Podmienené formuláre skrývajú polia, ktoré sú stále označené ako povinné. Rozhodnite sa vopred, či vaša validácia rešpektuje viditeľnosť alebo len samotný príznak povinného poľa, a toto rozhodnutie zdokumentujte pre potreby podpory.

Predtým, než na to doplatíte, je dôležité ujasniť si jeden rozdiel: generovanie vzhľadu nie je sploštenie (flattening). Metóda GenerateFormAppearances zviditeľní hodnoty všade, pričom ponechá polia upraviteľné. Sploštenie zapečie vzhľad do statického obsahu stránky a natrvalo odstráni interaktivitu, čo je správne pre archívnu kópiu, ale nesprávne pre formulár, ktorý má niekto ďalší vypĺňať. Ak vlastnosť FormType vráti ftXfaFull namiesto ftAcroForm, žiadna z tu popísaných funkcií na úpravu sa nedá použiť, pretože dokument se vykresľuje z vlastnej XML šablóny. Detegujte tento prípad a informujte používateľa namiesto toho, aby na toto obmedzenie narazil sám.

Podsystém vypĺňania formulárov, prechádzanie zamerania a generovanie vzhľadu predstavené v tomto článku sú súčasťou PDFium Component pre Delphi, C++Builder a Lazarus/FPC. Ak váš prehliadač spracováva aj recenzentské anotácie spolu s údajmi formulára, článok o revízii anotácií sa venuje tomuto príbuznému modelu.