Gdy dynamiczny formularz XFA w viewerze Delphi dodaje albo usuwa strony, PDFium Component zgłasza nową sumę przez TPdf.PageCount i TPdf.OnXfaPageCountChanged od v3.126.1, bo natywne zdarzenie strony niesie deltę dodane/usunięte, a nie sumę. Biblioteki Windows V8 w v3.126.1 przesuwają też obszary trafień wejścia razem z przeniesionymi polami, a v3.126.2 wczytuje ponownie nieaktualne uchwyty stron po powrocie z callbacka layoutu. Bugreport, który to zaczął, dotyczył formularza wniosku o rozliczenie: kliknij Add Row dwa razy, formularz rośnie do dwóch stron, a wskaźnik stron dumnie pokazuje 1 z 1. Wpisz coś do pola, które przeniosło się na stronę 2, a klawisze lądują gdzieś niewidzialnie. Nic z tego nie wychodziło na sztywnych formularzach przykładowych, od których każdy zaczyna testy, a powody warto znać, jeśli osadzasz viewer formularzy
Co się dzieje, gdy dynamiczny formularz XFA się repaginuje?
Dynamiczny formularz XFA nie ma sztywnej listy stron, więc jego liczba stron jest wynikiem layoutu i może się zmieniać przy każdej edycji danych przez użytkownika. XFA 3.3 opisuje formularz jako drzewo subformów; powtarzalnym subformem steruje instanceManager, a skrypt typu _Row.addInstance() klonuje kolejny wiersz. Procesor layoutu przelewa potem treść do obszarów stron od nowa, co może dodać stronę, zabrać stronę albo wepchnąć istniejące pola na inną stronę. ISO 32000-1 §12.7.8 definiuje wyłącznie to, jak pakiety XFA jadą wewnątrz PDF; wszystko, co dzieje się potem, należy do silnika XFA, którym w PDFium Component jest własny layout XFA PDFiuma działający w procesie gospodarza. Viewer w Delphi ma więc do czynienia z dokumentem, którego liczba stron, rozmiary stron i pozycje widgetów to żywy stan. Trzy rzeczy idą źle, gdy gospodarz zakłada inaczej:
- liczba stron cache'owana przez gospodarza do nawigacji, zakresów przewijania i spinerów stron robi się nieaktualna albo, gorzej, zostaje zaktualizowana złą liczbą
- pola, które się przeprowadzają, pokazują obramowanie na nowym miejscu, podczas gdy edytor i obszar trafień myszy siedzą na starych współrzędnych
- viewer trzyma uchwyt strony, który layout podmienił, więc kliki i rysowanie idą do strony, która w tym formularzu już nie istnieje
Utrwalanie edycji wierszy przez zapis i ponowne otwarcie to osobny problem z własnymi regułami; ten artykuł zostaje przy tym, co dzieje się w trakcie działania wewnątrz viewera
Jakiego runtime'u PDFium potrzebuje dynamiczne XFA?
Dynamiczne XFA w PDFium Component wymaga builda V8/XFA biblioteki natywnej, wybieranego globalną zmienną EnableV8Engine z modułu PDFium przed wczytaniem pierwszego dokumentu. Proces wiąże się z jedną biblioteką DLL przy pierwszym wczytaniu biblioteki przez dowolny TPdf, a zwykły build PDFium nie uruchomi silnika XFA w ogóle. Przy otwarciu dokumentu TPdf zajrzyje do pliku pod kątem znaczników XFA i przełączy się na build V8 automatycznie, ale tylko jeśli żaden zwykły nie został jeszcze wczytany w tym procesie. Gdy związanie poszło już złą stroną, TPdf.OnXfaRuntimeMissing odpala się raz, żeby gospodarz mógł kazać użytkownikowi zrestartować aplikację. Jawne ustawienie flagi na starcie usuwa zgadywanie. Struktura callbacków FPDF_FORMFILLINFO niosąca zdarzenia XFA też musi pasować do biblioteki DLL; tło opisuje FPDF_FORMFILLINFO wersja 2 i ABI callbacków XFA, a wykrywanie formularzy XFA i czytanie ich pakietów pokazuje, jak odróżnić typy formularzy, zanim otworzysz viewer
uses
PDFium;
procedure TClaimForm.FormCreate(Sender: TObject);
begin
// Zdecyduj, zanim pierwszy TPdf wczyta bibliotekę natywną:
// proces nie może później przejść z pdfium.dll na pdfium.v8.dll
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;
Dlaczego PageCount raportował 1 dla formularza o dwóch stronach?
Przed v3.126.1 PDFium Component zapisywał argument page_count natywnego zdarzenia strony jako sumę dokumentu, a ten argument to w rzeczywistości wartość bezwzględna różnicy między nową a starą liczbą stron. PDFium podnosi FFI_PageEvent po zakończeniu przebiegu layoutu z typem zdarzenia strona dodana albo strona usunięta; wewnątrz najpierw aktualizuje swoją przechowywaną liczbę stron, a potem podaje abs(new - old). Przy początkowym layoucie stara liczba to zero, więc delta równa się sumie i trójstronicowa statyczna próbka raportuje trzy strony, jak trzeba. Właśnie dlatego sztywne formularze testowe nigdy nie wystawiły buga. Gdy dynamiczny formularz rośnie pierwszy raz z jednej strony do dwóch, delta wynosi 1, a wrapper ustawiał i TPdf.PageCount, i parametr NewCount OnXfaPageCountChanged na 1. Usunięcie wiersza z trójstronicowego formularza produkowało ten sam rodzaj bzdur w drugą stronę
Sumowanie delty na poprzednią wartość to też niebezpieczna naprawa. Kolejność callbacków inicjalizacji i layoutu sprawia, że wrapper nie zawsze może ufać swojej wcześniejszej liczbie jako punktowi odniesienia, więc biegnąca suma może dryfować. Od v3.126.1 callback ignoruje argument jako liczbę i woła FPDF_GetPageCount na dokumencie, który czyta sumę z layoutu właśnie ukończonego. Potem czyści cache'owane sceny stron, zapisuje tę sumę jako nadpisanie liczby stron XFA za TPdf.PageCount i dopiero potem podnosi OnXfaPageCountChanged. Do czasu, gdy twój handler się wykonuje, NewCount i FPdf.PageCount się zgadzają
procedure TClaimForm.PdfXfaPageCountChanged(Sender: TObject; NewCount: Integer);
begin
// v3.126.1+: NewCount to suma ukończonego layoutu, nigdy delta.
// To wykonuje się wewnątrz callbacka layoutu PDFium: aktualizuj tylko stan UI gospodarza,
// nie zamykaj dokumentu ani nie wczytuj stron stąd
UpdatePageRange(NewCount);
end;
procedure TClaimForm.PdfViewPageChange(Sender: TObject);
begin
// Odpala się po każdym ponownym wczytaniu strony, łącznie z odroczonym odświeżeniem XFA
PageSpin.Value := PdfView1.PageNumber;
end;
procedure TClaimForm.UpdatePageRange(Count: Integer);
begin
PageSpin.MinValue := 1;
PageSpin.MaxValue := Count;
PageLabel.Caption := Format('of %d', [Count]);
end;
Zdarzenie odpala się wyłącznie dla formularzy Full XFA, których layout zmienia się w trakcie działania. Statyczne XFA i dokumenty AcroForm nigdy go nie podnoszą, więc viewer obsługujący oba może zostawić ten sam handler podpięty. Pozostawienie go niepodpiętym też jest bezpieczne; nadpisanie za TPdf.PageCount działa tak czy inaczej, a zdarzenie istnieje po to, żeby gospodarz odświeżył to, co cache'ował
Dlaczego pole wprowadzania zostaje na starej stronie, gdy pole się przeprowadza?
Obramowanie się przeprowadziło, a edytor nie, bo natywny notyfikator XFA porównywał prostokąt z nim samym. Gdy layout zmienia geometrię już wczytanego widgetu, PDFium ma zauważyć nowy prostokąt i zawołać PerformLayout na widgetcie, co przestawia edytor tekstu i jego obszar trafień. Sprawdzenie porównywało GetWidgetRect() z RecacheWidgetRect(). Obie funkcje zwracają referencję const do tego samego pola, a recache nadpisuje to pole w miejscu, więc porównanie zawsze widziało dwie identyczne wartości, a wczytane widgety pomijały swój relayout
Objaw wyszedł na jaw, gdy test zmienił wysokość subformu tak, że istniejące pola przeskoczyły na następną stronę. Na obu architekturach V8 obramowanie pola rysowało się na nowym miejscu, podczas gdy wpisany tekst i obszar trafień myszy zostawały na poprzednim Y. Jawny relayout nie pomagał, i ponowne wczytanie strony też nie, bo widget wciąż wierzył, że jego geometria jest aktualna. Biblioteki Windows V8 dostarczane z v3.126.1 kopiują stary prostokąt przez wartość przed recache i porównują tę kopię, więc przeprowadzone widgety robią relayout, a edytowana wartość pojawia się dokładnie tam, gdzie jest obramowanie. To natywna poprawka: podróżuje razem z bibliotekami DLL, więc zaktualizowanie modułów Pascal przy starszym pdfium.v8.dll zostawia przesunięte obszary trafień na miejscu. Check regresyjny, który to wywalczył, najpierw edytuje przeżywający wiersz na wartość inną niż domyślna, a potem wymaga tej wartości na nowym miejscu pola, bo wiersz przebudowany z wartościami domyślnymi wyglądałby inaczej jak przejście
Jak TPdfView wczytuje strony ponownie, nie wyrywając uchwytu sprzed PDFium?
Od v3.126.2 TPdfView odracza ponowne wczytanie strony następujące po zmianie layoutu XFA, aż natywny stos wywołań się rozwinie. Zdarzenie strony zwykle odpala się, gdy PDFium wciąż przetwarza wejście: użytkownik kliknął przycisk Add Row, klik wykonał skrypt, skrypt zmienił licznik instancji, a layout skończył się wewnątrz tego samego natywnego wywołania. Zamknięcie i ponowne otwarcie uchwytu strony w tej chwili zwolniłoby obiekt, którego wołający wciąż używa. Przed v3.126.2 viewer tylko unieważniał sam siebie, więc wyświetlany uchwyt strony mógł dalej wskazywać stan sprzed layoutu, a jeśli użytkownik był na ostatniej stronie, gdy zniknęła, wybrany numer strony wychodził poza zakres
Odroczone odświeżanie działa w kilku małych krokach, które tłumaczą zachowanie widziane od strony gospodarza:
- callback zdarzenia strony oznacza widok jako mający oczekujące odświeżenie layoutu XFA i wysyła prywatny komunikat okienkowy; powtórzone zdarzenia przed dotarciem komunikatu scaliłyby się w jedno odświeżenie
- widok bez uchwytu okna trzyma na razie flagę oczekującą i wysyła komunikat z
CreateWnd, a zmiana dokumentu, deaktywacja widoku albo jego zniszczenie czyszczą flagę - gdy komunikat dociera, widok czyści zaznaczenie tekstu, podświetlenie wyszukiwania i indeks pola z fokusem, bo wszystkie trzy odnosiły się do starego layoutu
- wybrana strona jest przycinana do nowego
PageCount; zmieniony numer strony idzie przez normalne przełączenie strony, w przeciwnym razie bieżąca strona jest wczytywana ponownie, a tryb dopasowania stosuje się jeszcze raz - jeśli layout nie zostawia żadnych stron, widok wyładowuje stary uchwyt strony, zamiast rysować stronę, która już nie istnieje
To samo ograniczenie dotyczy twojego własnego kodu. OnXfaPageCountChanged wykonuje się wewnątrz natywnego callbacka layoutu, więc traktuj go jak powiadomienie: aktualizuj tam etykiety, zakresy spinerów i stan paska narzędzi, a wszystko cięższe, jak zamknięcie dokumentu albo otwarcie innego, kolejkuj wysłanym komunikatem, żeby wykonało się po powrocie callbacka. TPdfView.OnPageChange mówi ci potem, kiedy widok faktycznie wczytał stronę ponownie, a odczyt PdfView1.PageNumber w tym momencie daje wartość przyciętą. Nawigowanie Tabem i sprawdzenia FormType, które viewer formularzy robi przy otwarciu, opisuje nawigacja po polach formularzy PDF z PDFium Component
Dlaczego kliknięcie pola Full XFA rzuca „Cannot open text page”?
Strony Full XFA nie mają tekstowej strony PDF, a przed v3.126.2 domyślne zaznaczanie tekstu i wykrywanie odnośników w viewerze i tak próbowały takową wczytać. Przy TPdfView.AllowUserTextSelection na domyślnym True najechanie myszą pytało warstwę tekstu o znak pod kursorem, a kliknięcie puszczeniem myszy puszczło automatyczne sondowanie URL po tekście strony. Na stronie Full XFA strona tekstowa nie da się otworzyć, więc zwyczajny klik w pole mógł skończyć się wyjątkiem Cannot open text page. Od v3.126.2 obie wewnętrzne ścieżki zwracają brak wyniku, gdy TPdf.FormType to ftXfaFull i runtime XFA jest dostępny, więc ustawienia domyślne działają, a wprowadzanie do pól zostaje dostępne
Wyłączenie AllowUserTextSelection dla dokumentów Full XFA to nadal rozsądny wybór w UI, bo nie ma tekstu strony do zaznaczenia, a gesty przeciągania nie powinny włączać trybu zaznaczania. Nie jest to jednak zamiennik aktualizacji: we wcześniejszych wersjach sonda URL przy kliku nie zależała od tej właściwości, więc viewer mógł trafić na ten sam wyjątek mimo wyłączonego zaznaczania
procedure TClaimForm.ConfigureViewerForForm;
begin
// FormType czyta otwarty dokument, więc wołaj to po FPdf.Active := True
if FPdf.XFA and (FPdf.FormType = ftXfaFull) and FPdf.XfaRuntimeAvailable then
begin
// Na stronach Full XFA nie ma warstwy tekstu PDF; pola zostają edytowalne
PdfView1.AllowUserTextSelection := False;
StatusBar1.SimpleText := Format('Dynamic XFA form, %d page(s)',
[FPdf.PageCount]);
end
else
PdfView1.AllowUserTextSelection := True;
end;
Pisanie potrzebowało własnej naprawy w v3.126.2. Natywny edytor tekstu XFA nie zastępuje zaznaczenia, gdy dostaje znak: FORM_OnChar wstawia przy kursorze, a Backspace kasuje pojedynczy znak, więc zaznaczenie wartości i wpisanie czegoś na to produkowało stary i nowy tekst obok siebie. PDFium Component zapamiętuje teraz, że klik wylądował na tekstowym polu XFA, i kieruje wpisane znaki, Backspace i Delete przez FORM_ReplaceSelection, ilekroć istnieje zaznaczenie, a dokument przyznaje uprawnienie wypełniania formularzy albo modyfikacji. O tym, czy pole XFA tylko do odczytu może się zmienić, nadal decyduje natywny edytor, więc pole oznaczone w formularzu jako tylko do odczytu zachowuje wartość nawet w dokumencie, który w innym wypadku pozwala wypełniać. Ustawienie TPdfView.AllowFormEvents na False także zatrzymuje to trasowanie klawiatury, co utrzymuje viewera tylko do odczytu w tym stanie
Ściąga: dynamiczne XFA w viewerze Delphi
| Objaw | Przyczyna | Naprawione w |
|---|---|---|
| Liczba stron pokazuje 1, gdy formularz urośnie do dwóch stron | Natywne zdarzenie strony podaje deltę dodane/usunięte, nie sumę | v3.126.1 (wrapper) |
| Obramowanie pola się przeprowadza, wpisany tekst i obszar trafień zostają | Wczytany widget pomijał relayout po porównaniu samego ze sobą | v3.126.1 (biblioteki Windows V8) |
| Viewer rysuje albo kieruje wejście do stanu strony sprzed layoutu | Uchwyt strony niewczytany ponownie po repaginacji | v3.126.2 (odroczone odświeżenie) |
| Klik w pole rzuca Cannot open text page | Zaznaczanie tekstu i sonda URL na stronach bez warstwy tekstu | v3.126.2 |
| Pisanie po zaznaczonej wartości dokleja zamiast zastępować | Natywny edytor XFA wstawia przy kursorze | v3.126.2 |
- Ustaw
EnableV8EnginenaTrue, zanim jakikolwiek dokument się wczyta, i obsłużOnXfaRuntimeMissingna wypadek, gdy zwykła biblioteka zdążyła się wczytać pierwsza - Czytaj sumę z
TPdf.PageCountalbo z parametruNewCountOnXfaPageCountChanged; nigdy nie dodawaj ani nie odejmuj liczb stron sam - Trzymaj handler
OnXfaPageCountChangedlekki, bo wykonuje się wewnątrz natywnego callbacka layoutu - Synchronizuj wskaźnik bieżącej strony w
TPdfView.OnPageChange, które odpala się po tym, jak odroczone wczytanie przycina numer strony - Rozprowadzaj biblioteki Windows V8 z v3.126.1 albo nowsze razem z modułami; poprawka relayoutu widgetów mieszka w kodzie natywnym
- Testuj na formularzu, który faktycznie zmienia liczbę stron i przenosi edytowane pole przez łamanie strony, bo sztywne próbki ukrywają każdy bug z tej listy
Dynamiczne XFA zamienia liczbę stron i geometrię pól w wartości żywe, a viewer pozostaje poprawny tylko wtedy, gdy bierze je z ukończonego layoutu i wczytuje strony ponownie w bezpiecznym momencie. PDFium Component załatwia jedno i drugie wewnątrz TPdf i TPdfView, więc gospodarz musi tylko słuchać. Szczegóły i pobieranie są na stronie produktu PDFium Component for Delphi