Technical Article

Навигация в полетата на PDF формуляри в Delphi (PDFium Component)

Натиснете Tab в PDF формуляр, изграден от вашия код, и курсорът ще се озове на две полета разстояние от мястото, където трябва, или ще пропусне изцяло втората колона, или ще се върне в началото след третото поле вместо след четвъртото. Човекът, попълващ фактура във вашия визуализатор, очаква клавиатурата да премине през формуляра по същия начин, по който работи във всеки уеб формуляр. Когато това не се случи, той посяга към мишката, търси следващото поле и тихо решава, че инструментът ви не е завършен. Предвидимото преминаване през полетата е разликата между визуализатор за въвеждане на данни, който потребителите просто толерират, и такъв, на който имат доверие, и това зависи изцяло от използването на правилния API за фокус, вместо да имитирате въвеждане от клавиатурата със симулирани щраквания.

Примерите по-долу използват PDFium Component â€?базиран на PDFium VCL/LCL компонент за Delphi, C++Builder и Lazarus. Навигацията е едно от трите неща, които визуализаторът на формуляри трябва да изпълнява правилно; другите две â€?правилното отваряне на формуляра и запазването на попълнените стойности, така че действително да се появят, са местата, където се крият повечето изненади. Затова и трите теми са разгледани по-долу.

Отваряне на формуляр: FormFill, FormType, и въпросът за XFA

Достъпът до полетата изисква подсистемата за попълване на формуляри, управлявана от свойството FormFill, да бъде активирана преди отварянето на документа. След като се активира, FormType ви показва с какъв тип формуляр си имате работа, а отговорът променя набора от функции, които можете да гарантирате:

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;

От това следват две практически бележки. AcroForm е стандартният модел на формуляр по ISO 32000 и е това, към което са насочени всички представени тук приложни програмни интерфейси (API). XFA документите вграждат собствена архитектура за XML формуляри, така че обещаването на пълно редактиране на XFA на клиент след бърза демонстрация на AcroForm е ангажимент, за който ще съжалявате. Втората бележка се отнася до страничните ефекти: задаването на FormFill на True инициализира и JavaScript кода в документа. При визуализатор за въвеждане на данни това е абсолютно вярно, тъй като скриптовете за изчисление са това, което поддържа общата сума актуална, докато потребителят пише. В прозорец за предварителен преглед на файлове с неизвестен произход това обаче е грешно. Статията за сигурен предварителен преглед на PDF разглежда другата страна на този компромис с настройката FormFill := False.

Преминаване с клавиша Tab, което отвежда потребителите там, където очакват

Да се върнем на проблема с клавиатурата от самото начало. Изкушението е да се симулира Tab чрез изкуствено кликване с мишката върху правоъгълника на следващия компонент, което се проваля в момента, в който дадено поле се скролира извън екрана или два компонента се застъпят. Вместо това API за фокус премества фокуса на самия формуляр директно, без геометрични догадки. Пет повиквания покриват тази функция: FocusFormField по индекс, FocusNextFormField и FocusPreviousFormField за преминаване, FocusedFormFieldIndex за четене на текущата позиция и ClearFormFieldFocus за пълно премахване на фокуса.

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;

Едно поведение, което може да обърка разработчиците, е зациклянето (wrap). Преминаването работи според реда на клавиша Tab на текущата страница и се върти в цикъл: преминете след последното поле и ще се върнете на първото. И двете функции за стъпка връщат новия индекс на полето, или -1, когато страницата не съдържа полета. Това зацикляне е за всяка страница отделно, а не за целия документ, което означава, че преминаването към следващата страница е ваша задача, а не на библиотеката. Сравнете върнатия индекс с този, от който сте започнали, забележете кога е направил пълен кръг и променете сами PageNumber, ако формулярът трябва да се чете като една непрекъсната последователност. Пропуснете тази проверка и формуляр от две страници тихо ще блокира курсора на първата страница.

Преминаването става наистина полезно, когато останалата част от потребителския интерфейс реагира на него. Събитието OnFormFieldEnter се задейства при пристигане на фокуса, а във визуализатора OnFormFieldFocusChange съобщава новия индекс на полето, така че страничният панел да може да се синхронизира с това, което току-жо е избрано от клавиатурата. Когато се нуждаете от обратното съпоставяне (от позиция на екрана към поле), индексираното свойство FormFieldAt извършва тест за съвпадение (hit-testing) за подсказки и панели за редактиране при щракване. Всичко това носи и скрито предимство за достъпността: тъй като фокусът следва собствения ред на полетата в документа, пътят, който конфигурирате за клавиша Tab, съвпада с пътя, който софтуерът за четене на екрана съобщава, без никакви допълнителни усилия.

Показването на имената на полетата вместо необработените им индексни номера изисква още едно свойство. FormFieldInfo[] връща запис TPdfFormFieldInfo за всеки индекс, съдържащ името на полето, типа, размера на шрифта, отметнатото състояние, стойността за експортиране и членството в групи â€?което е точно това, което трябва да се показва в списъка за навигация („ПолÐ?4 от 17: InvoiceDateâ€?вместо â€?â€?. Групите от радио бутони (Radio groups) са случай, за който си струва да имате специален тестов файл. Няколко компонента могат да споделят едно и също име на поле, така че списък, сглобен без съобразяване с това, показва една и съща група няколко пъти и обърква потребителите.

Защо попълнените стойности се показват празни и повикването, което решава проблема

Другото оплакване, което често пълни списъците с техническа поддръжка, е по-тревожно от неправилно работещия клавиш Tab: формулярът се попълва програмно, клиентът го отваря в Acrobat и всяко поле изглежда празно. Кликнете в дадено поле и стойността му веднага се появява. Данните са във файла през цялото време. Това, което липсва, е визуализацията на данните, а причината си струва да се разбере веднъж, защото обяснява цял клас от проблеми.

Текстовото поле на AcroForm съхранява стойността си в записа /V от речника на полето (ISO 32000-1 §12.7.3.3). Това, което визуализаторът действително изчертава, е нещо отделно: потокът за външен вид на компонента под /AP (§12.5.5), малък предварително рендиран откъс от съдържание. Ако запишете /V и оставите /AP непроменено, двете стойности се разминават. Стойността съществува, но рендираната версия е остаряла или липсва. Acrobat възстановява външния вид на полето, когато то получи фокус, което напълно обяснява защо стойностите се появяват само при щракване. Старият флаг NeedAppearances, който изискваше от визуализаторите да регенерират външния вид вместо вас, никога не е работил еднакво навсякъде и е премахнат в PDF 2.0, а сървърите за печат и генераторите на миниатюри го игнорират напълно. Те изчертават само /AP, така че ако /AP е празен, те отпечатват празна кутия.

Присвояването на стойност чрез FormField[i] записва само /V. Ето защо попълването на формуляр е последователност от три стъпки, а стъпката, която разработчиците често пропускат, е средната:

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 е цялото решение. Той възстановява потока за външен вид на всеки компонент въз основа на текущите стойности, шрифтове и подравняване, така че визуализатори без обработка на фокус, сървър за печат или генератори на миниатюри да изобразяват правилно попълненото състояние. Извикайте го веднъж след цялата група от промени, а не за всяко поле поотделно. Генерирането на външен вид извършва реално оформяне на оформлението и повикванията за всяко поле излишно биха натоварили системата при големи формуляри.

Регенерирането на външния вид е също така моментът, в който се прилагат шрифтовете и подравняването, което е източник на допълнителна изненада. Новият поток разполага всяка стойност в правоъгълника на компонента, използвайки шрифта, размера и подравняването на полето. Стойност, която се събира нормално във вашия тестов формуляр, може да бъде изрязана или свита в копието на клиента, където същото поле е по-тясно. Полетата с автоматичен размер (размер на шрифта нула) свиват текста, за да се събере; тези с фиксиран размер просто го изрязват. И двата случая са валидни според спецификацията и единствения сигурен начин да разберете какво прави даден формуляр е да разгледате регенерирания изход, а не само низа, който сте записали. Когато някой съобщи за отрязан текст в края на кутията, това почти винаги е причината.

Третирайте проверката като част от завършването на работата, а не като закъсняла идея. Отворете записания файл в Acrobat и потвърдете, че стойностите са видими, преди да докоснете което и да е поле. След като го отпечатате в PDF или в изображение от друг визуализатор, който игнорира логиката на формуляра изцяло, и се уверете, че стойностите се запазват и там. Тези две проверки заедно улавят всеки вариант на разминаване между /V и /AP.

Конфигурации на полета, които преминават демото, но се провалят на практика

Чистите демо формуляри крият редица гранични случаи, които не се срещат в клиентските файлове. Четири от тях са причина за повечето съобщения от типа „работеше на моя компютърâ€?

  • Стойности за експортиране на квадратчета за отметка (Checkbox). Състоянието „включеноâ€?не винаги е Yes. Даден формуляр може да дефинира собствена стойност за експортиране и записването на грешен низ оставя кутийката визуално неотметната, докато вашият код е убеден, че я е задал. Четете стойността за експортиране от FormFieldInfo[], вместо да правите предположения.
  • Групи радио бутони със споделено име. Едно поле, няколко компонента. Стойността, която присвоявате, определя кой компонент се счита за избран, така че кодът на потребителския интерфейс, который предполага, че едно име съответства на един правоъгълник, завършва с изчертаване на фокусния пръстен върху грешния бутон.
  • Изчисляеми полета. Общите суми, поддържани от JavaScript в документа, се актуализират в отговор на събития в полетата. Програмното попълване, което заобикаля тези събития, трябва или да задейства преизчисляване, или директно да презапише изчисляемите полета. Формуляр, в който отделните позиции и крайната сума не съвпадат, е по-лош от всяко друго решение.
  • Скрити задължителни полета. Динамичните формуляри скриват полета, които все още са маркирани като задължителни. Решете предварително дали вашата валидация зачита видимостта или необработения флаг за задължително поле, и след това запишете това решение някъде, където поддръжката може да го намери.

Една разлика си струва да се изясни отрано: генерирането на външен вид не е сливане (flattening). GenerateFormAppearances прави стойностите видими навсякъде, като оставя полетата редактируеми. Сливането вгражда външния вид в статичното съдържание на страницата и премахва интерактивността завинаги â€?което е подходящо за архивно копие, но грешно за формуляр, който следващият човек трябва да попълни. Ако FormType отчита ftXfaFull вместо ftAcroForm, нищо от интерфейса за редактиране тук не се прилага лесно, тъй като документът се рендира от собствен XML шаблон; разпознайте този случай и уведомете потребителя, вместо да го оставяте да открие това ограничение сам.

Подсистемата за попълване на формуляри, преминаването през фокуса и генерирането на външен вид, показани тук, са част от PDFium Component за Delphi, C++Builder, и Lazarus/FPC. Ако вашият визуализатор обработва и рецензентски маркирания заедно с данни от формуляри, статията за преглед на анотации разглежда този съседен модел.