Khi một form XFA động trong một viewer Delphi thêm hay bớt trang, PDFium Component report tổng mới qua TPdf.PageCount và TPdf.OnXfaPageCountChanged kể từ v3.126.1, vì event trang native mang một delta thêm/bớt chứ không phải tổng. Các thư viện V8 Windows trong v3.126.1 còn kéo theo các vùng bắt input khi field đổi chỗ, và v3.126.2 nạp lại các page handle cũ sau khi callback layout trả về. Bug report khởi đầu cái này là một form khai báo chi phí: bấm Add Row hai lần, form phình ra hai trang, và chỉ báo trang vẫn tự hào hiện 1 of 1. Gõ vào một field vừa nhảy sang trang 2 thì các phím đáp xuống chỗ vô hình. Chẳng cái nào hiện ra với những form mẫu cố định độ dài mà ai cũng test đầu tiên, và lý do vì sao đáng biết nếu bạn nhúng một form viewer
Chuyện gì xảy ra khi một form XFA động repaginate?
Một form XFA động không có danh sách trang cố định, nên số trang của nó là một output của layout và có thể đổi mỗi lần người dùng sửa dữ liệu. XFA 3.3 mô tả form như một cây subform; một subform lặp lại được điều khiển bởi một instanceManager, và một script như _Row.addInstance() nhân bản thêm một hàng. Bộ xử lý layout rồi đổ nội dung vào các page area lần nữa, có thể thêm trang, bớt trang hay đẩy các field có sẵn sang trang khác. ISO 32000-1 §12.7.8 chỉ định nghĩa cách các gói XFA cõng bên trong PDF; mọi thứ xảy ra sau đó thuộc về XFA engine, mà trong PDFium Component là layout XFA của chính PDFium chạy trong tiến trình chủ. Viewer Delphi vì thế đối diện với một tài liệu mà số trang, kích cỡ trang và vị trí widget đều là trạng thái sống. Ba thứ hỏng khi host giả định ngược lại:
- Số trang mà host cache cho điều hướng, dải cuộn và page spinner bị cũ, tệ hơn là bị cập nhật bằng một con số sai
- Các field đổi chỗ hiện border ở vị trí mới trong khi editor và vùng bắt chuột vẫn ở tọa độ cũ
- Viewer giữ một page handle mà layout đã thay thế, nên click lẫn vẽ đều đổ vào một trang không còn tồn tại trong form ấy
Giữ các chỉnh sửa hàng qua save và mở lại là một vấn đề riêng với luật riêng của nó; bài này chỉ bám vào chuyện xảy ra lúc runtime bên trong viewer
XFA động cần runtime PDFium nào?
XFA động trong PDFium Component đòi hỏi bản dựng V8/XFA của thư viện native, chọn bằng biến toàn cục EnableV8Engine trong unit PDFium trước khi tài liệu đầu tiên nạp. Tiến trình cam kết với một DLL ngay lần đầu tiên một TPdf nào đó nạp thư viện, còn một bản dựng PDFium trơn không thể chạy XFA engine chút nào. Khi một tài liệu mở, TPdf có liếc tệp tìm dấu XFA và tự chuyển sang bản dựng V8, nhưng chỉ khi chưa có thư viện trơn nào được nạp trong tiến trình đó. Khi sự cam kết đã đi nhầm chiều, TPdf.OnXfaRuntimeMissing bắn một lần để host báo người dùng khởi động lại. Đặt cờ tường minh lúc startup xóa bỏ mọi phỏng đoán. Cấu trúc callback FPDF_FORMFILLINFO mang các event XFA cũng phải khớp với DLL; bối cảnh nằm trong FPDF_FORMFILLINFO version 2 và ABI callback XFA, còn phát hiện form XFA và đọc các gói XFA nói về việc phân biệt các loại form trước khi bạn mở viewer
uses
PDFium;
procedure TClaimForm.FormCreate(Sender: TObject);
begin
// Quyết định trước khi TPdf đầu tiên nạp thư viện native:
// tiến trình không thể chuyển từ pdfium.dll sang pdfium.v8.dll sau đó
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;
Vì sao PageCount report 1 cho một form hai trang?
Trước v3.126.1, PDFium Component lưu đối số page_count của event trang native thành tổng tài liệu, mà đối số đó thực ra là chênh lệch tuyệt đối giữa số trang mới và cũ. PDFium nêu FFI_PageEvent sau khi một lượt layout kết thúc với event type là trang thêm hay trang bớt; bên trong nó cập nhật số trang đã lưu trước rồi mới đưa ra abs(new - old). Ở layout khởi đầu, số cũ bằng không, nên delta bằng tổng, và một mẫu tĩnh ba trang report ba trang như kỳ vọng. Đó chính là lý do các form test cố định độ dài không bao giờ phơi ra bug này. Lần đầu tiên một form động phình từ một trang lên hai, delta là 1, và wrapper đặt cả TPdf.PageCount lẫn tham số NewCount của OnXfaPageCountChanged thành 1. Bớt một hàng khỏi form ba trang tạo ra cùng loại vô lý theo chiều ngược lại
Cộng dồn delta lên giá trị trước cũng không phải một bản sửa an toàn. Thứ tự giữa callback khởi tạo và layout nghĩa là wrapper không phải lúc nào cũng tin được số trang trước đó của nó làm baseline, nên một tổng chạy có thể trôi. Kể từ v3.126.1, callback bỏ qua đối số như một số đếm và gọi FPDF_GetPageCount trên tài liệu, hàm đọc tổng từ layout vừa hoàn tất. Nó rồi xóa các page scene đã cache, lưu tổng đó thành XFA page-count override đằng sau TPdf.PageCount, và chỉ sau đó mới nêu OnXfaPageCountChanged. Đến lúc handler của bạn chạy, NewCount và FPdf.PageCount đã đồng thuận
procedure TClaimForm.PdfXfaPageCountChanged(Sender: TObject; NewCount: Integer);
begin
// v3.126.1+: NewCount là tổng của layout đã hoàn tất, không bao giờ là delta.
// Cái này chạy trong callback layout của PDFium: chỉ cập nhật UI state phía host,
// đừng đóng tài liệu hay nạp lại trang từ đây
UpdatePageRange(NewCount);
end;
procedure TClaimForm.PdfViewPageChange(Sender: TObject);
begin
// Bắn sau mỗi lần nạp lại trang, gồm cả lần refresh XFA bị hoãn
PageSpin.Value := PdfView1.PageNumber;
end;
procedure TClaimForm.UpdatePageRange(Count: Integer);
begin
PageSpin.MinValue := 1;
PageSpin.MaxValue := Count;
PageLabel.Caption := Format('of %d', [Count]);
end;
Event chỉ bắn cho các form Full XFA mà layout đổi lúc runtime. Static XFA và AcroForm không bao giờ nêu nó, nên một viewer xử lý cả hai có thể để nguyên handler đã gán. Bỏ gán cũng an toàn; override đằng sau TPdf.PageCount vẫn được áp bất kể, còn event tồn tại để host làm mới những gì nó đã cache
Vì sao hộp input kẹt lại trang cũ khi một field đổi chỗ?
Border nhảy mà editor không nhảy vì notifier XFA native so một hình chữ nhật với chính nó. Khi layout đổi hình học của một widget đã nạp, lẽ ra PDFium phải nhận ra hình chữ nhật mới và gọi PerformLayout trên widget, thứ đặt lại vị trí text editor cùng vùng bắt chuột. Phép kiểm tra so GetWidgetRect() với RecacheWidgetRect(). Cả hai hàm trả về một const reference tới cùng một member, và recache ghi đè member đó tại chỗ, nên phép so sánh luôn thấy hai giá trị y hệt và các widget đã nạp bỏ qua relayout của chúng
Triệu chứng lộ ra khi một phép test đổi chiều cao subform khiến các field có sẵn tràn sang trang kế. Trên cả hai kiến trúc V8, border field được vẽ ở vị trí mới trong khi text đã gõ và vùng bắt chuột kẹt ở tọa độ Y cũ. Một relayout tường minh không sửa được, nạp lại trang cũng vậy, vì widget vẫn tin hình học của nó là cập nhật. Các thư viện V8 Windows đi kèm v3.126.1 chép hình chữ nhật cũ theo giá trị trước khi recache rồi so bản sao đó, nên các widget đổi chỗ relayout và giá trị đã sửa hiện đúng chỗ border đứng. Đây là một bản sửa native: nó đi cùng các DLL, nên cập nhật các unit Pascal mà giữ một pdfium.v8.dll cũ hơn là để nguyên các vùng bắt chuột lệch chỗ. Phép kiểm regression thúc đẩy nó sửa một hàng sống sót thành giá trị khác mặc định trước rồi đòi giá trị ấy ở vị trí mới của field, vì một hàng dựng lại với giá trị mặc định nếu không sẽ trông như đã qua
TPdfView nạp lại trang thế nào mà không rút handle từ dưới PDFium?
Kể từ v3.126.2, TPdfView hoãn việc nạp lại trang nối sau một thay đổi layout XFA cho tới khi call stack native đã tháo xong. Event trang thường bắn trong khi PDFium còn đang xử lý input: người dùng bấm một nút Add Row, cú bấm chạy một script, script đổi số instance, và layout kết thúc trong cùng một lời gọi native đó. Đóng rồi mở lại page handle tại khoảnh khắc đó sẽ giải phóng một object mà caller còn đang dùng. Trước v3.126.2, viewer chỉ tự vô hiệu, nên page handle hiển thị có thể cứ trỏ vào trạng thái trước layout, và nếu người dùng đang đứng ở trang cuối khi nó biến mất, số trang được chọn nằm ngoài dải
Lần refresh hoãn làm việc qua vài bước nhỏ, và chúng giải thích hành vi bạn thấy từ phía host:
- Callback page-event đánh dấu view là có một lần refresh layout XFA đang chờ và đăng một window message riêng tư; các event lặp lại trước khi message tới được gộp thành một lần refresh
- Một view chưa có window handle giữ cờ chờ và đăng message từ
CreateWnd, còn đổi tài liệu, vô hiệu hóa hay hủy view thì xóa cờ - Khi message tới, view xóa selection text, highlight tìm kiếm và chỉ số field đang focus, vì cả ba đều trỏ vào layout cũ
- Trang được chọn bị kẹp về
PageCountmới; số trang đổi thì đi qua phép chuyển trang bình thường, không thì trang hiện tại được nạp lại, và chế độ fit được áp lại - Nếu layout chẳng còn trang nào, view dỡ page handle cũ của nó thay vì vẽ một trang không còn tồn tại
Cùng ràng buộc đó áp dụng cho code của riêng bạn. OnXfaPageCountChanged chạy bên trong callback layout native đó, nên hãy đối xử với nó như một thông báo: cập nhật label, dải spinner và trạng thái toolbar ở đó, còn mọi thứ nặng hơn, như đóng tài liệu hay mở cái khác, hãy đưa vào hàng đợi bằng một message đăng lên để nó chạy sau khi callback trả về. TPdfView.OnPageChange rồi báo cho bạn khi view thật sự nạp lại trang, và đọc PdfView1.PageNumber tại điểm đó cho bạn giá trị đã kẹp. Điều hướng phím Tab và các phép kiểm FormType mà một form viewer chạy lúc mở được nói trong điều hướng form field PDF với PDFium Component
Vì sao bấm vào một field Full XFA nêu "Cannot open text page"?
Các trang Full XFA không có text page PDF nào, và trước v3.126.2 phần chọn text mặc định cùng phần dò link của viewer vẫn cố nạp một cái. Với TPdfView.AllowUserTextSelection ở mặc định True, việc rê chuột hỏi text layer một ký tự dưới con trỏ, và một cú click nhả chuột chạy một phép dò URL tự động trên text trang. Trên trang Full XFA, text page không mở nổi, nên một cú click thường vào field có thể kết thúc bằng một exception Cannot open text page. Kể từ v3.126.2, cả hai đường nội bộ trả về không có kết quả khi TPdf.FormType là ftXfaFull và XFA runtime có sẵn, nên các thiết lập mặc định hoạt động và input field vẫn khả dụng
Tắt AllowUserTextSelection cho tài liệu Full XFA vẫn là một lựa chọn UI hợp lý, vì chẳng có text trang nào để chọn và các cú kéo chuột không nên khởi động chế độ selection. Nhưng nó không phải một sự thay thế cho việc nâng cấp: trên các phiên bản cũ, phép dò URL khi click không phụ thuộc property đó, nên một viewer có thể dính cùng exception đó với selection đã tắt
procedure TClaimForm.ConfigureViewerForForm;
begin
// FormType đọc tài liệu đang mở, nên gọi cái này sau FPdf.Active := True
if FPdf.XFA and (FPdf.FormType = ftXfaFull) and FPdf.XfaRuntimeAvailable then
begin
// Không có text layer PDF nào trên các trang Full XFA; field vẫn editable
PdfView1.AllowUserTextSelection := False;
StatusBar1.SimpleText := Format('Dynamic XFA form, %d page(s)',
[FPdf.PageCount]);
end
else
PdfView1.AllowUserTextSelection := True;
end;
Gõ chữ cần một bản sửa riêng trong v3.126.2. Text editor XFA native không thay thế một selection khi nhận một ký tự: FORM_OnChar chèn tại caret, còn Backspace xóa một ký tự, nên chọn một giá trị rồi gõ đè lên cho ra text cũ và mới nằm cạnh nhau. PDFium Component giờ nhớ rằng cú click đã đáp lên một XFA text field và điều hướng các ký tự gõ, Backspace và Delete qua FORM_ReplaceSelection bất cứ khi nào có selection và tài liệu cấp quyền điền form hay sửa đổi. Một XFA field read-only có được đổi hay không vẫn do editor native quyết, nên một field được đánh dấu read-only trong form giữ giá trị của nó ngay cả trong một tài liệu vốn cho phép điền. Đặt TPdfView.AllowFormEvents thành False cũng ngưng luôn đường điều hướng bàn phím này, giữ một viewer read-only mãi read-only
Tra nhanh: XFA động trong một viewer Delphi
| Triệu chứng | Nguyên nhân | Sửa trong |
|---|---|---|
| Số trang hiện 1 sau khi form phình ra hai trang | Event trang native đưa một delta thêm/bớt, không phải tổng | v3.126.1 (wrapper) |
| Border field nhảy, text gõ và vùng bắt chuột kẹt lại | Widget đã nạp bỏ qua relayout sau một phép tự so sánh | v3.126.1 (thư viện Windows V8) |
| Viewer vẽ hay điều hướng input vào trạng thái trang trước layout | Page handle không được nạp lại sau repagination | v3.126.2 (refresh hoãn) |
| Bấm vào field nêu Cannot open text page | Chọn text và dò URL trên các trang không có text layer | v3.126.2 |
| Gõ đè lên một giá trị đang chọn nối thêm thay vì thay thế | Editor XFA native chèn tại caret | v3.126.2 |
- Đặt
EnableV8EnginethànhTruetrước khi bất kỳ tài liệu nào nạp, và xử lýOnXfaRuntimeMissingcho trường hợp thư viện trơn được nạp trước - Đọc tổng từ
TPdf.PageCounthay tham sốNewCountcủaOnXfaPageCountChanged; đừng bao giờ tự cộng trừ số trang - Giữ handler
OnXfaPageCountChangednhẹ nhàng, vì nó chạy bên trong callback layout native - Đồng bộ chỉ báo trang hiện tại trong
TPdfView.OnPageChange, thứ bắn sau khi lần nạp lại hoãn kẹp số trang - Triển khai các Windows V8 DLL v3.126.1 trở lên cùng với các unit; bản sửa relayout widget nằm trong code native
- Test với một form thực sự đổi số trang và đẩy một field đang sửa qua ranh giới trang, vì các mẫu cố định độ dài che giấu mọi bug trong danh sách này
XFA động biến số trang và hình học field thành các giá trị sống, và một viewer chỉ đúng khi lấy chúng từ layout đã hoàn tất và nạp lại trang vào một khoảnh khắc an toàn. PDFium Component lo cả hai bên trong TPdf và TPdfView, nên host chỉ việc lắng nghe. Chi tiết và bản tải về nằm trên trang sản phẩm PDFium Component cho Delphi