Technical Article

PDF formos laukų navigacija Delphi (PDFium komponentas)

Paspauskite Tab klavišą jūsų kodo sukurtoje PDF formoje, ir žymeklis atsidurs per du laukus nuo ten, kur turėtų, arba visiškai praleis antrąjį stulpelį, arba po trečiojo lauko grįš į viršų, o ne pereis į ketvirtąjį. Vartotojas, pildantis sąskaitą faktūrą jūsų peržiūros programoje, tikisi, kad klaviatūra formoje veiks taip pat, kaip ir bet kurioje internetinėje formoje. Kai taip nėra, jis ima pelę, ieško kito laukelio ir tyliai nusprendžia, kad jūsų įrankis yra nebaigtas. Nuspėjamas laukų perėjimas yra skirtumas tarp duomenų įvedimo programos, kurią žmonės toleruoja, ir tos, kuria pasitiki. Tai beveik visiškai priklauso nuo tinkamos fokuso API naudojimo, užuot imitavus klaviatūros įvestį suklastotais spustelėjimais.

Žemiau pateiktuose pavyzdžiuose naudojamas „PDFium Component“ – PDFium pagrįstas VCL/LCL komponentas, skirtas Delphi, C++Builder ir Lazarus. Navigacija yra vienas iš trijų dalykų, kuriuos formų peržiūros programa turi atlikti teisingai; kiti du – teisingas formos atidarymas ir užpildytų reikšmių išsaugojimas, kad jos iš tikrųjų būtų matomos, yra vietos, kuriose slepiasi dauguma netikėtumų, todėl toliau nagrinėjami visi trys.

Formos atidarymas: FormFill, FormType, ir XFA klausimas

Laukų prieigai reikalinga formų pildymo posistemė, valdoma savybės FormFill, kuri turi būti įjungta prieš atidarant dokumentą. Kai ji aktyvi, FormType praneša, su kokio tipo forma susidūrėte, ir šis atsakymas keičia funkcijų rinkinį, kurį galite garantuoti:

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;

Iš šio perjungimo išplaukia dvi praktinės pastabos. „AcroForm“ yra standartinis ISO 32000 formos modelis, ir jį tiksliai atitinka kiekviena čia minima API. XFA dokumentai turi savo XML formų architektūrą, todėl pažadėti klientui pilną XFA redagavimą po greitos „AcroForm“ demonstracijos bus sprendimas, kurio gailėsitės. Antroji pastaba yra susijusi su šalutiniu poveikiu: nustačius FormFill reikšmę True, taip pat inicijuojamas dokumento JavaScript. Duomenų įvedimo peržiūros programoje tai yra visiškai teisinga, nes skaičiavimo scenarijai atnaujina bendrą sumą vartotojui vedant tekstą. Tačiau neaiškios kilmės failų peržiūros lange tai yra neteisinga. Saugaus PDF peržiūros straipsnyje nagrinėjama ši pusiausvyra nustatant FormFill := False.

Tab klavišo perėjimas, kuris nuveda ten, kur tikisi vartotojai

Grįžkime prie klaviatūros problemos, paminėtos pradžioje. Kyla pagunda imituoti Tab klavišą generuojant pelės spustelėjimą kito valdiklio stačiakampyje, tačiau tai sugenda, kai laukas yra pastumiamas už ekrano ribų arba kai du valdikliai persidengia. Vietoj to, fokuso API perkelia pačios formos fokusą tiesiogiai, be jokių spėliojimų apie geometriją. Tai atlieka penki iškvietimai: FocusFormField pagal indeksą, FocusNextFormField ir FocusPreviousFormField žingsniavimui, FocusedFormFieldIndex esamos padėties nuskaitymui ir ClearFormFieldFocus fokuso visiškam pašalinimui.

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;

Vienas iš dalykų, su kuriais žmonės susiduria, yra cikliškumas. Perėjimas veikia pagal dabartinio puslapio Tab tvarką ir sukasi ratu: perėjus paskutinį lauką, grįžtama prie pirmojo. Abi žingsniavimo funkcijos grąžina naujo lauko indeksą arba -1, kai puslapyje laukų nėra. Šis ciklas vyksta puslapio lygmeniu, o ne viso dokumento, o tai reiškia, kad perėjimas į kitą puslapį yra jūsų, o ne bibliotekos užduotis. Palyginkite grąžintą indeksą su pradiniu, pastebėkite, kada įvyko perėjimas į pradžią, ir patys pakeiskite PageNumber, jei forma turi būti skaitoma kaip viena ištisinė seka. Praleiskite šį patikrinimą, ir dviejų puslapių forma tyliai įstrigdys žymeklį pirmame puslapyje, o tai bus dar viena neveikiančio Tab klavišo problema.

Perėjimas tampa naudingas, kai į jį reaguoja likusi vartotojo sąsaja. Įvykis OnFormFieldEnter suveikia atvykus fokusui, o peržiūros programoje OnFormFieldFocusChange praneša apie nuolatinį lauko indeksą, kad šoninis skydelis galėtų neatsilikti nuo to, kas buvo pasirinkta klaviatūra. Kai reikia atvirkštinio susiejimo – iš ekrano pozicijos į lauką, indeksuota savybė FormFieldAt atlieka paspaudimo tikrinimą (hit-testing) įrankių patarimų peržiūroms ir spustelėjimui redaguoti skydeliams. Visame tame yra ir tyli nauda prieinamumui (accessibility): kadangi fokusas seka paties dokumento laukų tvarką, kelias, kurį sujungiate Tab klavišui, yra tas pats kelias, kurį praneša ekrano skaitytuvas, be jokio papildomo darbo.

Norint rodyti laukų pavadinimus, o ne neapdorotus indeksų numerius, reikia dar vienos savybės. FormFieldInfo[] grąžina TPdfFormFieldInfo įrašą kiekvienam indeksui, kuriame yra lauko pavadinimas, tipas, šrifto dydis, pažymėjimo būsena, eksporto reikšmė ir priklausymas grupei. Būtent tai turėtų rodyti navigacijos sąrašas („Laukas 4 iš 17: InvoiceDate“, o ne tiesiog „4“). Radijo mygtukų grupės yra tas atvejis, kuriam verta sukurti atskirą bandomąjį failą. Keli valdikliai gali dalytis tuo pačiu lauko pavadinimu, oro iš valdiklių paprastai sudarytas sąrašas tą pačią grupę parodys kelis kartus ir supainios visus jį skaitančius.

Kodėl užpildytos reikšmės atrodo tuščios ir iškvietimas, kuris tai ištaiso

Kitas skundas, kuris užpildo palaikymo eiles, yra labiau nerimą keliantis nei netinkamai veikiantis Tab klavišas: forma užpildoma programiškai, klientas ją atidaro su „Acrobat“, ir kiekvienas laukas atrodo tuščios. Spustelėjus lauką, jo reikšmė staiga atsiranda. Duomenys faile yra visą laiką. Trūksta tik pačių duomenų atvaizdavimo, o priežastį verta suprasti vieną kartą, nes ji paaiškina visą susijusių klaidų šeimą.

AcroForm tekstinis laukas saugo savo reikšmę lauko žodyno įraše /V (ISO 32000-1 §12.7.3.3). Tai, ką peržiūros programa iš tikrųjų piešia, yra atskiras dalykas: valdiklio išvaizdos srautas po /AP (§12.5.5) – nedidelis iš anksto sugeneruotas turinio fragmentas. Jei įrašysite /V ir paliksite /AP ramybėje, šie du dalykai išsiskirs. Reikšmė yra, tačiau jos atvaizduota versija yra pasenusi arba jos išvis nėra. „Acrobat“ atkuria lauko išvaizdą, kai jis įgyja fokusą, ir tai visiškai paaiškina, kodėl reikšmės atsiranda tik spustelėjus. Senoji vėliavėlė „NeedAppearances“, kuri prašė peržiūros programų sugeneruoti išvaizdą už jus, niekada neveikė vienodai ir yra pasenusi PDF 2.0 versijoje, o spausdinimo serveriai bei miniatiūrų generatoriai ją visiškai ignoruoja. Jie piešia /AP ir nieko daugiau, tad jei /AP yra tuščias, jie atspausdina tuščią laukelį.

Reikšmės priskyrimas per FormField[i] įrašo tik /V. Štai kodėl formos pildymas yra trijų žingsnių seka, o žingsnis, kurį programuotojai dažnai praleidžia, yra vidurinysis:

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;

GenerateFormAppearances yra pilnas sprendimas. Ji atkuria kiekvieno valdiklio išvaizdos srautą pagal esamas reikšmes, šriftus ir lygiavimą (quadding), todėl peržiūros programa, kuri niekada nevykdo fokuso įvykio, spausdinimo serveris ar miniatiūrų generatorius vis tiek nupieš užpildytą būseną. Iškvieskite ją vieną kartą po priskyrimų partijos, o ne kiekvienam laukui atskirai. Išvaizdos generavimas atlieka realų maketavimo darbą, o iškvietimai atskiriems laukams tik be reikalo apkrauna didelę formą.

Išvaizdos atkūrimas taip pat yra momentas, kai pritaikomi šriftai ir lygiavimas, o tai sukelia antrinį netikėtumą. Naujasis srautas išdėsto kiekvieną reikšmę valdiklio stačiakampio viduje, naudodamas lauko šriftą, dydį ir lygiavimą. Reikšmė, kuri patogiai telpa jūsų bandomojoje formoje, kliento kopijoje, kur tas pats laukas yra siauresnis, gali būti apkarpyta arba sumažinta. Automatinio dydžio laukai (šrifto dydis nulis) sumažina tekstą, kad jis tilptų; fiksuoto dydžio laukai jį tiesiog apkerpa. Abu variantai yra teisėti, ir vienintelis patikimas būdas sužinoti, kaip elgiasi konkreti forma, yra peržiūrėti sugeneruotą rezultatą, o ne pasikliauti įrašyta eilute. Kai kas nors praneša apie tekstą, nukirptą laukelio pakraštyje, tai kai kada yra to priežastis.

Vertinkite patikrinimą kaip darbo pabaigimo dalį, o ne kaip vėlesnį apmąstymą. Atidarykite išsaugotą failą „Acrobat“ ir prieš liesdami bet kurį lauką įsitikinkite, kad reikšmės yra matomos. Tada atspausdinkite jį į PDF arba į paveikslėlį iš kitos peržiūros programos, kuri visiškai ignoruoja formos logiką, ir įsitikinkite, kad reikšmės išlieka ir ten. Šie du patikrinimai padeda sugauti bet kokį nukrypimą tarp /V ir /AP.

Laukų konfigūracijos, kurios sėkmingai praeina demonstraciją, bet sugenda realiame darbe

Tvarkingos demonstracinės formos slepia kraštutinius atvejus, kurių nebūna klientų failuose. Keturi iš jų sudaro didžiąją dalį pranešimų apie klaidas, kurios „veikė mano kompiuteryje“.

  • Žymimųjų langelių (checkbox) eksporto reikšmės. Įjungta būsena ne visada yra Yes. Forma gali laisvai apibrėžti savo eksporto reikšmę, ir įrašius netinkamą eilutę langelis liks vizualiai nepažymėtas, nors jūsų kodas bus įsitikinęs, kad jį nustatė. Perskaitykite eksporto reikšmę iš FormFieldInfo[], užuot ją spėlioję.
  • Bendro pavadinimo radijo mygtukų grupės. Vienas laukas, keli valdikliai. Reikšmė, kurią priskiriate, nustato, kuris valdiklis laikomas pasirinktu, todėl UI kodas, darantis prielaidą, kad vienas pavadinimas atitinka vieną stačiakampį, galiausiai nubrėžia fokuso rėmelį ant netinkamo mygtuko.
  • Skaičiuojamieji laukai. Bendros sumos, kurias palaiko dokumento JavaScript, atsinaujina reaguodamos į laukų įvykius. Programinis užpildymas, kuris apeina šiuos įvykius, turi arba paleisti perskaičiavimą, arba tiesiogiai perrašyti skaičiuojamuosius laukus. Forma, kurioje eilučių elementai ir bendra suma nesutampa, yra blogiau už bet kurį iš šių sprendimų.
  • Paslėpti privalomi laukai. Sąlyginės formos paslepia laukus, kurie vis tiek yra pažymėti kaip privalomi. Iš anksto nuspręskite, ar jūsų patvirtinimas atsižvelgs į matomumą, ar į neapdorotą privalomumo vėliavėlę, ir užrašykite šį sprendimą kur nors, kur palaikymo komanda galėtų jį rasti.

Vieną skirtumą verta išsiaiškinti prieš susiduriant su problemomis: išvaizdos generavimas nėra suplokštinimas (flattening). GenerateFormAppearances padaro reikšmes matomas visur, bet palieka laukus redaguojamus. Suplokštinimas įrašo išvaizdą į statinį puslapio turinį ir visam laikui pašalina interaktyvumą, o tai tinka archyvinei kopijai, bet netinka formai, kurią kitas asmuo dar turi pildyti. If FormType praneša apie ftXfaFull, o ne ftAcroForm, jokia čia minima redagavimo sąsaja negalės būti tinkamai pritaikyta, nes dokumentas atvaizduojamas pagal savo XML šabloną; aptikite šį atvejį ir praneškite vartotojui, užuot leidę jam pačiam atrasti šiuos apribojimus.

Čia parodyta formų pildymo posistemė, fokuso perėjimas ir išvaizdos generavimas yra dalis „PDFium Component“, skirto Delphi, C++Builder ir Lazarus/FPC. Jei jūsų peržiūros programa taip pat tvarko peržiūros žymėjimą kartu su formos duomenimis, anotacijų peržiūros straipsnis nagrinėja šį susijusį modelį.