Wenn ein dynamisches XFA-Formular in einem Delphi-Viewer Seiten ergänzt oder entfernt, meldet PDFium Component die neue Gesamtzahl seit v3.126.1 über TPdf.PageCount und TPdf.OnXfaPageCountChanged, denn das native Seiten-Event trägt ein Hinzugefügt/Entfernt-Delta statt einer Gesamtzahl. Die Windows-V8-Bibliotheken in v3.126.1 bewegen außerdem Eingabe-Trefferbereiche mit umgezogenen Feldern mit, und v3.126.2 lädt veraltete Seiten-Handles neu, nachdem der Layout-Callback zurückgekehrt ist. Der Bug-Report, der das ins Rollen brachte, war ein Spesenformular: Zweimal auf Add Row klicken, das Formular wächst auf zwei Seiten, und der Seitenindikator meldet stolz 1 von 1. Tippt man in ein Feld, das auf Seite 2 gewandert ist, landen die Tastenschläge irgendwo unsichtbar. Nichts davon zeigte sich an den Formularen fester Länge, mit denen jeder zuerst testet, und die Gründe dafür sind es wert zu kennen, wenn Sie einen Formular-Viewer einbetten
Was passiert, wenn ein dynamisches XFA-Formular neu paginiert?
Ein dynamisches XFA-Formular hat keine feste Seitenliste, seine Seitenzahl ist also eine Ausgabe des Layouts und kann sich bei jeder Datenänderung des Nutzers ändern. XFA 3.3 beschreibt das Formular als Baum aus Subforms; eine wiederholte Subform wird von einem instanceManager gesteuert, und ein Skript wie _Row.addInstance() klont eine weitere Zeile. Der Layout-Prozessor flutet den Inhalt dann erneut in die Seitenbereiche, was eine Seite ergänzen, eine Seite streichen oder bestehende Felder auf eine andere Seite schieben kann. ISO 32000-1 §12.7.8 definiert nur, wie die XFA-Pakete im PDF reisen; alles, was danach passiert, gehört zur XFA-Engine, die in PDFium Component PDFiums eigenes XFA-Layout im Host-Prozess ist. Ein Delphi-Viewer hat es also mit einem Dokument zu tun, dessen Seitenzahl, Seitengrößen und Widget-Positionen allesamt lebendiger Zustand sind. Drei Dinge gehen schief, wenn der Host etwas anderes annimmt:
- Die Seitenzahl, die der Host für Navigation, Scroll-Bereiche und Seiten-Spinner cacht, veraltet — oder schlimmer, sie wird mit der falschen Zahl aktualisiert
- Felder, die umziehen, zeigen ihren Rand an der neuen Position, während Editor und Maus-Trefferbereich an den alten Koordinaten bleiben
- Der Viewer hält ein Seiten-Handle, das das Layout ersetzt hat, Klicks und Paints gehen also an eine Seite, die es in diesem Formular nicht mehr gibt
Zeilenedits über Sichern und Wiederöffnen hinweg zu persistieren ist ein eigenes Problem mit eigenen Regeln; dieser Artikel bleibt bei dem, was zur Laufzeit im Viewer passiert
Welche PDFium-Runtime braucht dynamisches XFA?
Dynamisches XFA in PDFium Component verlangt den V8/XFA-Build der nativen Bibliothek, gewählt über die globale Variable EnableV8Engine in der Unit PDFium, bevor das erste Dokument lädt. Der Prozess legt sich beim ersten Laden der Bibliothek durch irgendein TPdf auf eine DLL fest, und ein schlichter PDFium-Build kann die XFA-Engine gar nicht fahren. Beim Öffnen eines Dokuments schaut TPdf zwar nach XFA-Markern in der Datei und wechselt automatisch zum V8-Build, aber nur, solange in diesem Prozess noch keine schlichte Bibliothek geladen wurde. Läuft die Festlegung bereits in die falsche Richtung, feuert TPdf.OnXfaRuntimeMissing einmal, damit der Host dem Nutzer einen Neustart nahelegen kann. Das Flag beim Start explizit zu setzen nimmt der Sache jede Rätselrei. Die FPDF_FORMFILLINFO-Callback-Struktur, die die XFA-Events trägt, muss ebenfalls zur DLL passen; der Hintergrund steht in FPDF_FORMFILLINFO Version 2 und dem XFA-Callback-ABI, und das Erkennen von XFA-Formularen und das Lesen ihrer Pakete behandelt, wie man die Formulartypen auseinanderhält, bevor man einen Viewer öffnet
uses
PDFium;
procedure TClaimForm.FormCreate(Sender: TObject);
begin
// Entscheiden, bevor das erste TPdf die native Bibliothek lädt:
// der Prozess kann nicht später von pdfium.dll auf pdfium.v8.dll wechseln
EnableV8Engine := True;
FPdf := TPdf.Create(nil);
FPdf.OnXfaRuntimeMissing := PdfXfaRuntimeMissing;
FPdf.OnXfaPageCountChanged := PdfXfaPageCountChanged;
FPdf.FileName := 'C:\Forms\expense-claim.pdf';
FPdf.Active := True;
PdfView1.Pdf := FPdf;
PdfView1.OnPageChange := PdfViewPageChange;
PdfView1.Active := True;
UpdatePageRange(FPdf.PageCount);
end;
procedure TClaimForm.PdfXfaRuntimeMissing(Sender: TObject);
begin
StatusBar1.SimpleText :=
'This XFA form needs the V8 runtime; restart the application to enable it';
end;
Warum meldete PageCount 1 für ein zweiseitiges Formular?
Vor v3.126.1 speicherte PDFium Component das page_count-Argument des nativen Seiten-Events als Dokument-Gesamtzahl, und dieses Argument ist tatsächlich die absolute Differenz zwischen neuer und alter Seitenzahl. PDFium wirft FFI_PageEvent nach einem abgeschlossenen Layout-Durchlauf mit dem Event-Typ Seite hinzugefügt oder Seite entfernt; intern aktualisiert es zuerst seine gespeicherte Seitenzahl und reicht dann abs(new - old) durch. Beim ersten Layout ist die alte Zahl null, das Delta gleicht also der Gesamtzahl, und eine dreiseitige statische Referenzdatei meldet erwartungsgemäß drei Seiten. Genau deshalb haben Formulare fester Länge den Bug nie enthüllt. Sobald ein dynamisches Formular zum ersten Mal von einer auf zwei Seiten wächst, ist das Delta 1, und der Wrapper setzte sowohl TPdf.PageCount als auch den NewCount-Parameter von OnXfaPageCountChanged auf 1. Eine Zeile aus einem dreiseitigen Formular zu entfernen produzierte dieselbe Art Unsinn in die andere Richtung
Das Delta auf den alten Wert aufzuaddieren ist auch keine sichere Reparatur. Die Reihenfolge von Initialisierungs- und Layout-Callbacks bedeutet, dass der Wrapper seine frühere Zahl nicht immer als Basis vertrauen kann, eine laufende Summe kann also driften. Seit v3.126.1 ignoriert der Callback das Argument als Zählwert und ruft FPDF_GetPageCount am Dokument auf, was die Gesamtzahl aus dem gerade abgeschlossenen Layout liest. Dann räumt er die gecachten Seiten-Szenen weg, speichert diese Gesamtzahl als XFA-Seitenzahl-Override hinter TPdf.PageCount und wirft erst danach OnXfaPageCountChanged. Läuft Ihr Handler, ziehen NewCount und FPdf.PageCount an einem Strang
procedure TClaimForm.PdfXfaPageCountChanged(Sender: TObject; NewCount: Integer);
begin
// v3.126.1+: NewCount ist die Gesamtzahl des abgeschlossenen Layouts, nie ein Delta.
// Das läuft in PDFiums Layout-Callback: nur Host-UI-Zustand aktualisieren,
// das Dokument hier weder schließen noch Seiten neu laden
UpdatePageRange(NewCount);
end;
procedure TClaimForm.PdfViewPageChange(Sender: TObject);
begin
// Feuert nach jedem Seiten-Reload, eingeschlossen das verzögerte XFA-Refresh
PageSpin.Value := PdfView1.PageNumber;
end;
procedure TClaimForm.UpdatePageRange(Count: Integer);
begin
PageSpin.MinValue := 1;
PageSpin.MaxValue := Count;
PageLabel.Caption := Format('of %d', [Count]);
end;
Das Event feuert nur für Full-XFA-Formulare, deren Layout sich zur Laufzeit ändert. Static XFA- und AcroForm-Dokumente werfen es nie, ein Viewer, der beides handhabt, kann denselben Handler also drangelassen haben. Unassigned zu lassen ist ebenfalls sicher; der Override hinter TPdf.PageCount greift ohnehin, und das Event existiert, damit der Host auffrischen kann, was er gecacht hat
Warum bleibt das Eingabefeld auf der alten Seite, wenn ein Feld umzieht?
Der Rand zog um, der Editor nicht, weil der native XFA-Notifier ein Rechteck mit sich selbst verglich. Wenn das Layout die Geometrie eines bereits geladenen Widgets ändert, soll PDFium das neue Rechteck bemerken und PerformLayout am Widget aufrufen, was den Texteditor und seinen Trefferbereich neu positioniert. Die Prüfung verglich GetWidgetRect() mit RecacheWidgetRect(). Beide Funktionen liefern eine const-Referenz auf dasselbe Member, und der Recache überschreibt dieses Member in place, der Vergleich sah also stets zwei identische Werte, und geladene Widget übersprangen ihr Relayout
Das Symptom kam zum Vorschein, als ein Test die Höhe einer Subform so änderte, dass bestehende Felder auf die nächste Seite hinüberrutschten. Auf beiden V8-Architekturen wurde der Feldrand an seiner neuen Position gezeichnet, während der getippte Text und der Maus-Trefferbereich an der alten Y-Koordinate blieben. Ein explizites Relayout fixte es nicht, ein Neuladen der Seite auch nicht, denn das Widget glaubte weiterhin, seine Geometrie sei aktuell. Die mit v3.126.1 ausgelieferten Windows-V8-Bibliotheken kopieren das alte Rechteck by value, bevor sie neu cachen, und vergleichen diese Kopie, umgezogene Widget relayouten also, und der editierte Wert erscheint exakt dort, wo der Rand ist. Das ist ein nativer Fix: Er reist mit den DLLs, die Pascal-Units zu aktualisieren, während eine ältere pdfium.v8.dll im Einsatz bleibt, lässt die verrutschten Trefferbereiche also in place. Der Regressionstest, der das eintrieb, editiert eine überlebende Zeile zuerst auf einen Nicht-Default-Wert und verlangt diesen Wert dann an der neuen Position des Felds, denn eine mit Default-Werten neu gebaute Zeile hätte sonst wie ein Erfolg ausgesehen
Wie lädt TPdfView Seiten neu, ohne PDFium ein Handle unterm Hintern wegzuziehen?
Seit v3.126.2 schiebt TPdfView den Seiten-Reload, der auf eine XFA-Layout-Änderung folgt, auf, bis der native Aufrufstapel abgebaut ist. Das Seiten-Event feuert meist, während PDFium noch Eingaben verarbeitet: Der Nutzer klickte auf einen Add-Row-Button, der Klick fuhr ein Skript, das Skript änderte die Instanzzahl, und das Layout schloss innerhalb desselben nativen Aufrufs ab. Das Seiten-Handle in diesem Moment zu schließen und neu zu öffnen würde ein Objekt freigeben, das der Aufrufer noch benutzt. Vor v3.126.2 invalidierte sich der Viewer nur selbst, das angezeigte Seiten-Handle konnte also weiter auf den Pre-Layout-Zustand zeigen, und war der Nutzer gerade auf der letzten Seite, als sie verschwand, lag die gewählte Seitenzahl außerhalb des Bereichs
Das verzögerte Refresh arbeitet in ein paar kleinen Schritten, und sie erklären das Verhalten, das Sie als Host sehen:
- Der Seiten-Event-Callback markiert die View als mit einem ausstehenden XFA-Layout-Refresh und posted eine private Fenster-Nachricht; wiederholte Events, bevor die Nachricht ankommt, verschmelzen zu einem Refresh
- Eine View ohne Fenster-Handle hält das Pending-Flag und posted die Nachricht aus
CreateWnd, während ein Dokumentwechsel, das Deaktivieren oder Zerstören der View das Flag löscht - Kommt die Nachricht an, räumt die View die Textauswahl, das Such-Highlight und den Fokussiertes-Feld-Index weg, denn alle drei bezogen sich auf das alte Layout
- Die gewählte Seite wird auf das neue
PageCountgeklemmt; eine geänderte Seitenzahl läuft durch den normalen Seitenwechsel, sonst wird die aktuelle Seite neu geladen, und der Fit-Modus wird erneut angewandt - Lässt das Layout gar keine Seiten zurück, entlädt die View ihr altes Seiten-Handle, statt eine Seite zu zeichnen, die es nicht mehr gibt
Dieselbe Beschränkung gilt für Ihren eigenen Code. OnXfaPageCountChanged läuft innerhalb dieses nativen Layout-Callbacks, behandeln Sie es also wie eine Benachrichtigung: Labels, Spinner-Bereiche und Toolbar-Zustand dort aktualisieren, alles Schwerere, etwa das Schließen des Dokuments oder das Öffnen eines anderen, über eine gepostete Nachricht in die Warteschlange stellen, damit es nach der Rückkehr des Callbacks läuft. TPdfView.OnPageChange sagt Ihnen dann, wann die View die Seite tatsächlich neu geladen hat, und PdfView1.PageNumber an diesem Punkt zu lesen liefert den geklemmten Wert. Tab-Durchlauf und die FormType-Prüfungen, die ein Formular-Viewer beim Öffnen fährt, behandelt PDF-Formularfeld-Navigation mit PDFium Component
Warum wirft ein Klick auf ein Full-XFA-Feld „Cannot open text page“?
Full-XFA-Seiten haben keine PDF-Textseite, und vor v3.126.2 versuchte die Standard-Textauswahl und die Link-Erkennung des Viewers trotzdem, eine zu laden. Mit TPdfView.AllowUserTextSelection auf seinem Default True fragte das Hovern die Text-Ebene nach einem Zeichen unter der Maus, und ein Mouse-up-Klick fuhr eine automatische URL-Sonde über den Seitentext. Auf einer Full-XFA-Seite lässt sich die Textseite nicht öffnen, ein gewöhnlicher Klick in ein Feld konnte also in einer Cannot open text page-Exception enden. Seit v3.126.2 liefern beide internen Pfade kein Ergebnis, wenn TPdf.FormType gleich ftXfaFull ist und die XFA-Runtime verfügbar ist, die Default-Einstellungen funktionieren also, und die Feldeingabe bleibt verfügbar
AllowUserTextSelection für Full-XFA-Dokumente auszuschalten bleibt trotzdem eine vernünftige UI-Entscheidung, denn es gibt keinen Seitentext zu selektieren, und Zieh-Gesten sollten keinen Auswahl-Modus starten. Ein Ersatz fürs Upgrade ist es allerdings nicht: Auf früheren Versionen hing die URL-Sonde beim Klick nicht von dieser Property ab, ein Viewer konnte dieselbe Exception also auch mit abgeschalteter Auswahl erwischen
procedure TClaimForm.ConfigureViewerForForm;
begin
// FormType liest das geöffnete Dokument, also nach FPdf.Active := True aufrufen
if FPdf.XFA and (FPdf.FormType = ftXfaFull) and FPdf.XfaRuntimeAvailable then
begin
// Auf Full-XFA-Seiten gibt es keine PDF-Text-Ebene; Felder bleiben editierbar
PdfView1.AllowUserTextSelection := False;
StatusBar1.SimpleText := Format('Dynamic XFA form, %d page(s)',
[FPdf.PageCount]);
end
else
PdfView1.AllowUserTextSelection := True;
end;
Das Tippen brauchte in v3.126.2 seine eigene Reparatur. Der native XFA-Texteditor ersetzt keine Auswahl, wenn er ein Zeichen erhält: FORM_OnChar fügt am Caret ein, und Backspace löscht ein einzelnes Zeichen, einen Wert zu selektieren und drüberzutippen produzierte also alten und neuen Text nebeneinander. PDFium Component merkt sich jetzt, dass der Klick auf einem XFA-Textfeld gelandet ist, und routet getippte Zeichen, Backspace und Delete durch FORM_ReplaceSelection, wann immer eine Auswahl existiert und das Dokument Fill-Forms- oder Modify-Berechtigung gewährt. Ob ein read-only XFA-Feld sich ändern darf, entscheidet weiterhin der native Editor, ein im Formular als read-only markiertes Feld behält also seinen Wert, selbst in einem Dokument, das sonst das Ausfüllen erlaubt. TPdfView.AllowFormEvents auf False zu setzen stoppt dieses Tastatur-Routing ebenfalls, ein read-only Viewer bleibt so read-only
Kurzreferenz: dynamisches XFA in einem Delphi-Viewer
| Symptom | Ursache | Gefixt in |
|---|---|---|
| Die Seitenzahl zeigt 1, nachdem das Formular auf zwei Seiten gewachsen ist | Das native Seiten-Event liefert ein Hinzugefügt/Entfernt-Delta, keine Gesamtzahl | v3.126.1 (Wrapper) |
| Der Feldrand zieht um, getippter Text und Trefferbereich bleiben zurück | Geladenes Widget übersprang das Relayout nach einem Selbstvergleich | v3.126.1 (Windows-V8-Bibliotheken) |
| Der Viewer zeichnet auf den Pre-Layout-Seitenzustand oder routet Eingaben dorthin | Seiten-Handle nach der Repagination nicht neu geladen | v3.126.2 (verzögertes Refresh) |
| Ein Klick in ein Feld wirft Cannot open text page | Textauswahl und URL-Sonde auf Seiten ohne Text-Ebene | v3.126.2 |
| Tippen über einen selektierten Wert hängt an statt zu ersetzen | Der native XFA-Editor fügt am Caret ein | v3.126.2 |
EnableV8EngineaufTruesetzen, bevor irgendein Dokument lädt, undOnXfaRuntimeMissingfür den Fall behandeln, dass die schlichte Bibliothek zuerst geladen wurde- Die Gesamtzahl aus
TPdf.PageCountoder demNewCount-Parameter vonOnXfaPageCountChangedlesen; niemals selbst Seitenzahlen addieren oder subtrahieren - Den
OnXfaPageCountChanged-Handler leicht halten, denn er läuft innerhalb des nativen Layout-Callbacks - Den aktuellen Seitenindikator in
TPdfView.OnPageChangesynchronisieren, der nach dem verzögerten Reload feuert, der die Seitenzahl geklemmt hat - Die Windows-V8-DLLs ab v3.126.1 zusammen mit den Units ausliefern; der Widget-Relayout-Fix wohnt im nativen Code
- Mit einem Formular testen, das seine Seitenzahl tatsächlich ändert und ein editiertes Feld über einen Seitenumbruch schiebt, denn Referenzdateien fester Länge verstecken jeden Bug auf dieser Liste
Dynamisches XFA macht aus Seitenzahl und Feldgeometrie lebendige Werte, und ein Viewer bleibt nur dann korrekt, wenn er sie aus dem abgeschlossenen Layout nimmt und Seiten zu einem sicheren Moment neu lädt. PDFium Component handhabt beides innerhalb von TPdf und TPdfView, der Host muss nur noch zuhören. Details und Downloads finden Sie auf der PDFium Component for Delphi product page