Technischer Artikel

PDF-Formularfeld-Navigation in Delphi (PDFium Component)

Der Fehlerbericht kommt mit einem Screenshot: „Ihr Tool hat das Formular ausgefüllt, aber in Acrobat ist jedes Feld leer. Wenn ich in ein Feld klicke, erscheint der Wert plötzlich.“ Die Daten stehen in der Datei — der Screenshot beweist es sogar — und trotzdem wirkt das Formular für alle Empfänger leer. Das ist der häufigste Defekt bei programmatischer PDF-Formulararbeit, und er ist kein Fehler irgendeiner Bibliothek: Er entsteht, wenn Feldwerte geschrieben werden, ohne die Feld-Appearances neu zu erzeugen. Warum das passiert, erklärt ein Abschnitt der PDF-Spezifikation; die Behebung braucht einen Methodenaufruf. Die Beispiele unten verwenden PDFium Component, eine PDFium-basierte VCL/LCL-Komponente für Delphi, C++Builder und Lazarus, doch die zugrunde liegende Dateiformatmechanik gilt für jedes AcroForm-Werkzeug.

Ein Feld, zwei Darstellungen: /V und /AP

Ein AcroForm-Textfeld speichert seinen Wert im /V-Eintrag des Field Dictionary (ISO 32000-1 §12.7.3.3). Was Viewer tatsächlich malen, ist jedoch der Appearance-Stream des Widgets — ein kleiner, vorgerenderter Inhaltsstream unter /AP (§12.5.5). Schreiben Sie /V, ohne /AP neu aufzubauen, laufen beide auseinander: Die Daten existieren, das Bild der Daten nicht. Acrobat malt die Appearance eines Feldes neu, wenn das Feld den Fokus erhält, und genau deshalb „enthüllt“ ein Klick in das Feld den Wert aus dem Fehlerbericht.

Der historische Ausweg, das NeedAppearances-Flag, das Viewer zur eigenen Appearance-Erzeugung auffordert, wurde nie konsistent beachtet und ist in PDF 2.0 (ISO 32000-2) veraltet. Druckpipelines und Thumbnail-Generatoren haben es nie beachtet — sie malen, was /AP enthält, also nichts. Der zuverlässige Vertrag lautet daher: Wer den Wert schreibt, baut auch die Appearance neu auf.

Die Appearance-Erzeugung ist zugleich die Stelle, an der Schriften und Ausrichtung sichtbar werden. Ein neu erzeugter Stream legt den Wert innerhalb des Widget-Rechtecks mit der Schrift, Größe und Quadding-Einstellung des Feldes aus. Deshalb kann ein Wert, der in Ihrem Testformular passt, in der schmaleren Kopie desselben Feldes beim Kunden abgeschnitten oder verkleinert werden. Felder mit automatischer Größe (Schriftgröße null) verkleinern Text passend; Felder mit fester Größe schneiden ab. Beides ist legal, und die einzige Möglichkeit, das Ergebnis eines konkreten Formulars zu kennen, ist die regenerierte Ausgabe zu prüfen, nicht den geschriebenen Wert — wenn ein Kunde abgeschnittenen Text meldet, ist dieser Absatz meist die ganze Erklärung.

Ein Formular öffnen: FormFill, FormType und die XFA-Frage

Feldzugriff setzt voraus, dass das Form-Fill-Subsystem, gesteuert durch die Eigenschaft FormFill, vor dem Öffnen des Dokuments aktiviert wird. Sobald es aktiv ist, sagt FormType, welcher Formulartyp vorliegt, und diese Antwort bestimmt die Funktionszusagen, die Sie machen können:

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;

Zwei praktische Hinweise. AcroForm ist das standardisierte ISO-32000-Formularmodell und das Ziel jeder API in diesem Artikel; XFA-Dokumente betten eine eigene XML-Formulararchitektur ein, und Kunden volle XFA-Bearbeitung auf Basis einer schnellen AcroForm-Demo zu versprechen, ist eine Zusage, die Sie bereuen werden. Zweitens initialisiert FormFill auch Dokument-JavaScript — genau das wollen Sie in einem Dateneingabe-Viewer, in dem Berechnungsskripte Summen aktuell halten, und genau das wollen Sie in einer Vorschau für nicht vertrauenswürdige Dateien nicht. Der Artikel zur sicheren PDF-Vorschau behandelt die Seite mit FormFill := False dieses Kompromisses.

Tastaturnavigation, die Benutzer vorhersagen können

Benutzer in der Dateneingabe leben auf der Tab-Taste, also muss die Feldnavigation sich wie jedes andere Formular verhalten. Die Fokus-API-Familie — FocusFormField, FocusNextFormField, FocusPreviousFormField, FocusedFormFieldIndex und ClearFormFieldFocus — bewegt den Formularfokus, ohne Mauseingaben zu simulieren:

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;

Kennen Sie das Verhalten an den Grenzen: Die Navigationsaufrufe laufen durch die Tab-Reihenfolge der aktuellen Seite und springen dort um — nach dem letzten Feld geht es zum ersten zurück, und beide Funktionen liefern den neuen Feldindex (oder -1, wenn die Seite keine Felder besitzt). Ob zur nächsten Seite gewechselt wird, ist Ihre Entscheidung: Erkennen Sie den Umsprung durch Vergleich der Indizes und erhöhen Sie PageNumber selbst, wenn dokumentweite Navigation das Ziel ist. Koppeln Sie die Navigation mit dem Ereignis OnFormFieldEnter (und im Viewer mit OnFormFieldFocusChange), um ein Seitenpanel mit dem Dokument synchron zu halten, und verwenden Sie die indizierte Eigenschaft FormFieldAt, wenn Sie Hit-Testing brauchen — etwa um eine Mausposition einem Feldwert für Tooltips oder Click-to-edit-Panels zuzuordnen. Screenreader-Benutzer profitieren kostenlos von derselben Navigation: Der Fokus folgt der Feldreihenfolge des Dokuments, also ist der Pfad, den Sie für Tab bauen, auch der Pfad der assistiven Technik.

Für metadatengetriebene Oberflächen liefert die Eigenschaft FormFieldInfo[] pro Index einen Datensatz TPdfFormFieldInfo; damit beschriften Sie Felder in einer Navigationsliste, statt nur Indexnummern zu zeigen. Radiogruppen verdienen hier eine eigene Regressionsdatei: Mehrere Widgets teilen sich einen Feldnamen, sodass eine naiv aus Widgets gebaute Liste scheinbare Duplikate zeigt, die Benutzer verwirren.

Die Fill-and-save-Sequenz, die Acrobat übersteht

Alles oben läuft auf eine dreistufige Sequenz hinaus, deren mittleren Schritt Teams auslassen:

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 ist die komplette Behebung für den Eingangsfehlerbericht. Die Methode baut die Widget-Appearance-Streams aus aktuellen Werten, Schriften und Quadding neu auf, sodass jeder Viewer — auch solche, die nie Fokusereignisse ausführen, wie Druckserver und Thumbnailer — den gefüllten Zustand malt. Rufen Sie sie einmal nach dem Batch der Zuweisungen auf, nicht pro Feld; Appearance-Erzeugung berührt Schriften und Layout, und Aufrufe pro Feld vervielfachen diese Kosten bei großen Formularen ohne Nutzen.

Verifikation gehört in die Definition of done: Öffnen Sie die gespeicherte Datei in Acrobat und bestätigen Sie, dass Werte ohne Klick in ein Feld sichtbar sind, dann drucken Sie aus einem zweiten Viewer nach PDF oder Bild und bestätigen, dass die Werte eine Pipeline überstehen, die Formularlogik vollständig ignoriert. Zusammen fangen diese zwei Prüfungen jede Variante der /V-gegen-/AP-Abweichung.

Produktionsformulare, die saubere Implementierungen brechen

Eine kurze Liste von Feldkonfigurationen, die Demotests bestehen und mit Kundendateien scheitern:

  • Exportwerte von Checkboxen. Der aktivierte Zustand ist nicht überall Yes — Formulare definieren beliebige Exportwerte, und ein falscher Wert lässt die Box optisch unmarkiert, während Ihr Code Erfolg glaubt.
  • Radiogruppen mit gemeinsamem Namen. Ein Feld, viele Widgets. Die Wertzuweisung wählt, welches Widget angehakt erscheint, und UI-Code pro Widget, der one-name-one-rectangle annimmt, zeichnet den falschen Fokusring.
  • Berechnete Felder. Summen, die Dokument-JavaScript berechnet, aktualisieren sich über Feldereignisse. Ein programmatisches Füllen, das Ereignisse umgeht, sollte entweder neu berechnen oder die berechneten Felder explizit überschreiben — ein Formular auszuliefern, in dem Positionen und Summe widersprechen, ist schlechter als beide Optionen.
  • Versteckte Pflichtfelder. Bedingte Formulare verstecken Felder, die weiterhin als erforderlich markiert sind. Entscheiden Sie, ob Ihre Validierung Sichtbarkeit oder Rohflag beachtet, und dokumentieren Sie die Entscheidung dort, wo Support sie findet.

FAQ

Warum erscheinen meine ausgefüllten Werte erst, wenn ein Feld angeklickt wird?

Die Werte wurden nach /V geschrieben, aber die /AP-Appearance-Streams wurden nie neu erzeugt; Viewer malen deshalb die veraltete (leere) Appearance, bis ein Fokusereignis einen Neuaufbau erzwingt. Rufen Sie GenerateFormAppearances nach der Wertzuweisung und vor SaveAs auf.

Funktioniert Feldnavigation bei XFA-Formularen?

Prüfen Sie zuerst FormType. ftAcroForm bietet die volle hier beschriebene Navigations- und Bearbeitungsfläche; ftXfaFull bedeutet, dass das Dokument aus einer eigenen XML-Vorlage rendert und Interaktion auf Feldebene begrenzt ist. Erkennen und erklären Sie das, statt Benutzer es entdecken zu lassen.

Ist Flattening dasselbe wie Appearance-Erzeugung?

Nein. GenerateFormAppearances hält Felder interaktiv und macht ihre Werte überall sichtbar. Flattening wandelt die Appearance in statischen Seiteninhalt um und entfernt Interaktivität dauerhaft — richtig für Archivoutput, falsch für ein Formular, das die nächste Person bearbeiten muss.

Das Form-Fill-Subsystem, die Fokusnavigation und die Appearance-Erzeugung aus diesem Artikel gehören zu PDFium Component für Delphi, C++Builder und Lazarus/FPC. Wenn Ihr Viewer neben Formulardaten auch Review-Markup verarbeitet, behandelt der Artikel zur Anmerkungsprüfung dieses benachbarte Modell.