När ett dynamiskt XFA-formulär i en Delphi-visare lägger till eller tar bort sidor rapporterar PDFium Component den nya summan via TPdf.PageCount och TPdf.OnXfaPageCountChanged sedan v3.126.1, för det nativa sidoevenemanget bär en tillagd/borttagen delta i stället för en summa. Windows V8-biblioteken i v3.126.1 flyttar också input-träffytor med omlokaliserade fält, och v3.126.2 laddar om inaktuella sidohandtag efter att layoutåteranropet returnerat. Buggrapporten som startade detta var ett utläggsformulär: klicka Lägg till rad två gånger, formuläret växer till två sidor, och sidindikatorn stoltserar med 1 av 1. Skriver du i ett fält som flyttat till sida 2 landar tangenttryckningarna någonstans osynligt. Inget av det visade sig i formulärexemplen med fast längd som alla testar med först, och skälen är värda att känna till om du bäddar in en formulärvisare
Vad händer när ett dynamiskt XFA-formulär gör om sidläggningen?
Ett dynamiskt XFA-formulär har ingen fast sidolista, så dess sidantal är en utdata från layouten och kan ändras varje gång användaren redigerar data. XFA 3.3 beskriver formuläret som ett träd av subformulär; ett upprepande subformulär styrs av en instanceManager, och ett skript som _Row.addInstance() klonar ytterligare en rad. Layoutprocessorn flyter sedan innehållet i sidytorna igen, vilket kan lägga till en sida, ta bort en sida eller skjuta befintliga fält till en annan sida. ISO 32000-1 §12.7.8 definierar bara hur XFA-paketen rider inuti PDF:en; allt som händer därefter tillhör XFA-motorn, som i PDFium Component är PDFiums egen XFA-layout som kör i värdprocessen. En Delphi-visare har alltså att göra med ett dokument vars sidantal, sidstorlekar och widgetpositioner alla är levande tillstånd. Tre saker går fel när värden antar något annat:
- Sidantalet som värden cachar för navigering, rullningsintervall och sidospinnare blir inaktuellt, eller värre, uppdateras med fel nummer
- Fält som omlokaliserar sig visar sin ram i den nya positionen medan redigeraren och musens träffyta stannar på de gamla koordinaterna
- Visaren behåller ett sidohandtag som layouten har ersatt, så klick och ritningar går till en sida som inte längre finns i det formuläret
Att bevara radredigeringar över sparande och återöppning är ett separat problem med egna regler; den här artikeln stannar vid vad som händer vid körning inuti visaren
Vilken PDFium-körning behöver dynamisk XFA?
Dynamisk XFA i PDFium Component kräver V8/XFA-bygget av det nativa biblioteket, valt av den globala variabeln EnableV8Engine i PDFium-enheten innan det första dokumentet läses in. Processen binder sig till en DLL första gången någon TPdf laddar biblioteket, och ett vanligt PDFium-bygge kan inte köra XFA-motorn alls. När ett dokument öppnas tittar TPdf visserligen i filen efter XFA-markörer och växlar automatiskt till V8-bygget, men bara om inget vanligt bibliotek har laddats ännu i processen. Har bindningen redan gått åt fel håll utlöses TPdf.OnXfaRuntimeMissing en gång så att värden kan be användaren starta om. Att sätta flaggan explicit vid uppstart tar bort gissningsleken. Återanropsstrukturen FPDF_FORMFILLINFO som bär XFA-evenemangen måste också matcha DLL:en; bakgrunden finns i FPDF_FORMFILLINFO version 2 och XFA-återanropets ABI, och att upptäcka XFA-formulär och läsa deras paket täcker hur man skiljer formulärtyperna åt innan man öppnar en visare
uses
PDFium;
procedure TClaimForm.FormCreate(Sender: TObject);
begin
// Bestäm före den första TPdf laddar det nativa biblioteket:
// processen kan inte växla från pdfium.dll till pdfium.v8.dll senare
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;
Varför rapporterade PageCount 1 för ett tvåsidigt formulär?
Före v3.126.1 lagrade PDFium Component argumentet page_count från det nativa sidoevenemanget som dokumenttotalen, och det argumentet är egentligen den absoluta skillnaden mellan det nya och det gamla sidantalet. PDFium utlöser FFI_PageEvent efter att ett layoutpass avslutats med en evenemangstyp av sida tillagd eller sida borttagen; internt uppdaterar det sitt lagrade sidantal först och skickar sedan abs(new - old). Vid den första layouten är det gamla talet noll, så deltan är lika med summan, och ett tresidigt statiskt exempel rapporterar tre sidor som förväntat. Det är exakt därför testformulär med fast längd aldrig avslöjade buggen. Första gången ett dynamiskt formulär växer från en sida till två är deltan 1, och wrappern satte både TPdf.PageCount och parametern NewCount hos OnXfaPageCountChanged till 1. Att ta bort en rad från ett tresidigt formulär gav samma sorts nonsens i den andra riktningen
Att ackumulera deltan på det tidigare värdet är ingen säker reparation heller. Ordningen av initialisering och layoutåteranrop betyder att wrappern inte alltid kan lita på sitt tidigare tal som baslinje, så en löpande summa kan driva. Sedan v3.126.1 ignorerar återanropet argumentet som ett tal och anropar FPDF_GetPageCount på dokumentet, som läser summan från layouten som just fullbordats. Det rensar sedan de cachade sidoscenerna, lagrar den summan som XFA-sidantalöverstyrningen bakom TPdf.PageCount, och först därefter utlöses OnXfaPageCountChanged. När din hanterare kör håller NewCount och FPdf.PageCount med varandra
procedure TClaimForm.PdfXfaPageCountChanged(Sender: TObject; NewCount: Integer);
begin
// v3.126.1+: NewCount är den fullbordade layoutens total, aldrig en delta.
// Detta körs inuti PDFiums layoutåteranrop: uppdatera bara värd-UI-tillstånd,
// stäng inte dokumentet eller ladda om sidor härifrån
UpdatePageRange(NewCount);
end;
procedure TClaimForm.PdfViewPageChange(Sender: TObject);
begin
// Utlöses efter varje sidladdning om, inklusive den uppskjutna XFA-uppdateringen
PageSpin.Value := PdfView1.PageNumber;
end;
procedure TClaimForm.UpdatePageRange(Count: Integer);
begin
PageSpin.MinValue := 1;
PageSpin.MaxValue := Count;
PageLabel.Caption := Format('of %d', [Count]);
end;
Evenemanget utlöses bara för Full XFA-formulär vars layout ändras under körning. Statiska XFA- och AcroForm-dokument utlöser det aldrig, så en visare som hanterar båda kan låta samma hanterare vara tilldelad. Att lämna den otilldelad är också säkert; överstyrningen bakom TPdf.PageCount tillämpas ändå, och evenemanget finns för att värden ska kunna uppdatera vad det än cachat
Varför stannar input-rutan på gamla sidan när ett fält flyttar?
Ramen flyttade och redigeraren gjorde inte det för att den nativa XFA-notifieraren jämförde en rektangel med sig själv. När layouten ändrar geometrin hos en widget som redan är laddad ska PDFium märka den nya rektangeln och anropa PerformLayout på widgeten, vilken omplacerar textredigeraren och dess träffyta. Kontrollen jämförde GetWidgetRect() med RecacheWidgetRect(). Båda funktionerna returnerar en const-referens till samma medlem, och recachen skriver över den medlemmen på plats, så jämförelsen såg alltid två identiska värden och laddade widgetar hoppade över sin omläggning
Symptomet kom fram när ett test ändrade ett subformulärs höjd så att befintliga fält korsade över till nästa sida. På båda V8-arkitekturerna ritades fältramarna i sin nya position medan den inskrivna texten och musens träffyta stannade på föregående Y-koordinat. En explicit omläggning fixade det inte, och inte heller att ladda om sidan, för widgeten trodde fortfarande att sin geometri var aktuell. Windows V8-biblioteken som levereras med v3.126.1 kopierar den gamla rektangeln per värde innan recachen och jämför den kopian, så flyttade widgetar lägger om sig och det redigerade värdet dyker upp exakt där ramen är. Det är en nativ fix: den färdas med DLL:erna, så att uppdatera Pascal-enheterna medan man behåller en äldre pdfium.v8.dll lämnar de felplacerade träffytorna kvar. Regressionskontrollen som drev den redigerar en överlevande rad till ett icke-standardvärde först och kräver sedan det värdet på fältets nya plats, för en rad återbyggd med standardvärden skulle annars se ut som ett godkänt
Hur laddar TPdfView om sidor utan att rycka ett handtag undan PDFium?
Sedan v3.126.2 skjuter TPdfView upp sidladdningen om som följer en XFA-layoutändring tills den nativa anropsstacken har vecklat ut sig. Sidoevenemanget utlöses oftast medan PDFium fortfarande bearbetar input: användaren klickade på en Lägg till rad-knapp, klicket körde ett skript, skriptet ändrade instansantalet, och layouten fullbordades inuti samma nativa anrop. Att stänga och öppna sidohandtaget i det ögonblicket skulle frigöra ett objekt som anroparen fortfarande använder. Före v3.126.2 ogiltigförklarade visaren bara sig själv, så det visade sidohandtaget kunde fortsätta peka på tillstånd före layouten, och om användaren hade varit på sista sidan när den försvann var det valda sidonumret utanför intervallet
Den uppskjutna uppdateringen fungerar i några små steg, och de förklarar beteendet du ser från värden:
- Sidoevenemangsåteranropet märker vyn som med en väntande XFA-layoutuppdatering och postar ett privat fönstermeddelande; upprepade evenemang innan meddelandet anländer slås ihop till en uppdatering
- En vy utan fönsterhandtag ännu behåller den väntande flaggan och postar meddelandet från
CreateWnd, medan att byta dokument, avaktivera vyn eller förstöra den rensar flaggan - När meddelandet anländer rensar vyn textmarkeringen, sökmarkeringen och indexet för fokuserat fält, för alla tre syftade på den gamla layouten
- Den valda sidan begränsas till det nya
PageCount; ett ändrat sidonummer går genom det normala sidbytet, i annat fall laddas aktuella sidan om, och anpassningsläget tillämpas igen - Lämnar layouten inga sidor alls lossar vyn sitt gamla sidohandtag i stället för att rita en sida som inte längre finns
Samma begränsning gäller din egen kod. OnXfaPageCountChanged körs inuti det nativa layoutåteranropet, så behandla det som en notis: uppdatera etiketter, spinnarintervall och verktygsradstillstånd där, och kölägg allt tyngre, som att stänga dokumentet eller öppna ett annat, med ett postat meddelande så att det körs efter att återanropet returnerat. TPdfView.OnPageChange berättar sedan när vyn faktiskt har laddat om sidan, och att läsa PdfView1.PageNumber vid det laget ger dig det begränsade värdet. Tab-tangentsgenomgång och de FormType-kontroller en formulärvisare kör vid öppning tas upp i PDF-formulärfältsnavigering med PDFium Component
Varför ger ett klick på ett Full XFA-fält "Cannot open text page"?
Full XFA-sidor har ingen PDF-textsida, och före v3.126.2 försökte visarens standardtextmarkering och länkdetektering ändå ladda en. Med TPdfView.AllowUserTextSelection på sitt standardvärde True frågade hovring textlagret efter ett tecken under musen, och ett musuppklick körde en automatisk URL-sondering över sidtexten. På en Full XFA-sida går textsidan inte att öppna, så ett vanligt klick i ett fält kunde sluta i ett undantag Cannot open text page. Sedan v3.126.2 returnerar båda interna vägarna inget resultat när TPdf.FormType är ftXfaFull och XFA-körningen är tillgänglig, så standardinställningarna fungerar och fältinput förblir tillgänglig
Att stänga av AllowUserTextSelection för Full XFA-dokument är fortfarande ett rimligt UI-val, för det finns ingen sidtext att markera och draggester ska inte starta ett markeringsläge. Det är ingen ersättning för att uppgradera dock: på tidigare versioner berodde URL-sonderingen vid klick inte på den egenskapen, så en visare kunde träffa samma undantag med markering avstängd
procedure TClaimForm.ConfigureViewerForForm;
begin
// FormType läser det öppna dokumentet, så anropa detta efter FPdf.Active := True
if FPdf.XFA and (FPdf.FormType = ftXfaFull) and FPdf.XfaRuntimeAvailable then
begin
// Ingen PDF-textlager finns på Full XFA-sidor; fält förblir redigerbara
PdfView1.AllowUserTextSelection := False;
StatusBar1.SimpleText := Format('Dynamic XFA form, %d page(s)',
[FPdf.PageCount]);
end
else
PdfView1.AllowUserTextSelection := True;
end;
Inskrivning behövde sin egen reparation i v3.126.2. Den nativa XFA-textredigeraren ersätter inte en markering när den tar emot ett tecken: FORM_OnChar infogar vid textmarkören, och Backspace raderar ett enskilt tecken, så att markera ett värde och skriva över det gav gammal och ny text sida vid sida. PDFium Component kommer nu ihåg att klicket landade på ett XFA-textfält och dirigerar inskrivna tecken, Backspace och Delete genom FORM_ReplaceSelection närhelst en markering finns och dokumentet beviljar ifyllning eller ändringstillstånd. Huruvida ett skrivskyddat XFA-fält får ändras avgörs fortfarande av den nativa redigeraren, så ett fält märkt skrivskyddat i formuläret behåller sitt värde även i ett dokument som i övrigt tillåter ifyllning. Att sätta TPdfView.AllowFormEvents till False stoppar också denna tangentbordsdirigering, vilket håller en skrivskyddad visare skrivskyddad
Snabbreferens: dynamisk XFA i en Delphi-visare
| Symptom | Orsak | Fixat i |
|---|---|---|
| Sidantalet visar 1 efter att formuläret växt till två sidor | Det nativa sidoevenemanget skickar en tillagd/borttagen delta, inte en summa | v3.126.1 (wrapper) |
| Fältramen flyttar, inskriven text och träffyta stannar kvar | Laddad widget hoppade över omläggning efter en självjämförelse | v3.126.1 (Windows V8-bibliotek) |
| Visaren ritar eller dirigerar input till sidtillstånd före layout | Sidohandtag inte omladdat efter omsidläggning | v3.126.2 (uppskjuten uppdatering) |
| Klick i ett fält ger Cannot open text page | Textmarkering och URL-sondering på sidor utan textlager | v3.126.2 |
| Att skriva över ett markerat värde lägger till i stället för att ersätta | Den nativa XFA-redigeraren infogar vid textmarkören | v3.126.2 |
- Sätt
EnableV8EnginetillTrueinnan något dokument läses in, och hanteraOnXfaRuntimeMissingför fallet att det vanliga biblioteket laddades först - Läs summan från
TPdf.PageCounteller parameternNewCounthosOnXfaPageCountChanged; lägg aldrig till eller subtrahera sidantal själv - Håll
OnXfaPageCountChanged-hanteraren lätt, för den körs inuti det nativa layoutåteranropet - Synka aktuella sidindikatorn i
TPdfView.OnPageChange, som utlöses efter att den uppskjutna omladdningen begränsat sidnumret - Distribuera Windows V8-DLL:erna v3.126.1 eller senare tillsammans med enheterna; widgetomläggningsfixen bor i nativ kod
- Testa med ett formulär som faktiskt ändrar sitt sidantal och flyttar ett redigerat fält över en sidbrytning, för exempel med fast längd gömmer varje bugg på listan
Dynamisk XFA gör sidantal och fältgeometri till levande värden, och en visare förblir korrekt bara när den hämtar dem från den fullbordade layouten och laddar om sidor vid en säker stund. PDFium Component hanterar båda inuti TPdf och TPdfView, så värden behöver bara lyssna. Detaljer och nedladdningar finns på produktsidan för PDFium Component for Delphi