Nhấn Tab trong một form PDF mà code của bạn đã dựng, và con trỏ rơi vào một ô cách xa hai trường so với chỗ nó lẽ ra phải tới, hoặc bỏ qua hẳn cột thứ hai, hoặc nhảy về đầu sau trường thứ ba thay vì trường thứ tư. Người điền một hóa đơn trong viewer của bạn kỳ vọng bàn phím sẽ đi qua form theo cách nó đi qua mọi form web mà họ từng dùng. Khi nó không làm vậy, họ với tay lấy chuột, lùng sục tìm ô kế tiếp, và âm thầm quyết định rằng công cụ của bạn chưa hoàn thiện. Việc di chuyển qua các trường một cách có thể đoán trước chính là sự khác biệt giữa một viewer nhập liệu mà người ta chịu đựng được và một viewer mà họ tin tưởng, và đó gần như hoàn toàn là chuyện dùng đúng API focus thay vì giả lập bàn phím bằng các cú click mô phỏng
Các ví dụ dưới đây dùng PDFium Component, một component VCL/LCL dựa trên PDFium cho Delphi, C++Builder, và Lazarus. Điều hướng là một trong ba thứ mà một form viewer phải làm đúng; hai thứ còn lại, mở form đúng cách và lưu giá trị đã điền sao cho chúng thực sự hiện ra, là nơi ẩn giấu hầu hết các bất ngờ, nên cả ba đều được trình bày dưới đây
Mở một form: FormFill, FormType, và câu hỏi về XFA
Truy cập trường đòi hỏi hệ thống con form-fill, được điều khiển bởi thuộc tính FormFill, phải được bật trước khi tài liệu được mở. Một khi đã hoạt động, FormType cho bạn biết bạn đang đối mặt với loại form nào, và câu trả lời đó thay đổi tập tính năng bạn có thể hứa hẹn:
Pdf.FileName := FormPath;
Pdf.FormFill := True; // bật trước khi Active; bắt buộc để truy cập bất kỳ trường nào
Pdf.Active := True;
case Pdf.FormType of
ftNone:
DisableFormPanel('This document has no interactive form');
ftAcroForm:
BuildFieldList; // điều hướng và chỉnh sửa trường đầy đủ đều khả dụng
ftXfaFull:
ShowXfaNotice; // XFA render từ template XML của riêng nó;
// hãy coi việc chỉnh sửa trường là bị giới hạn
end;
Hai lưu ý thực tế nảy ra từ công tắc đó. AcroForm là mô hình form chuẩn của ISO 32000, và đó là thứ mọi API ở đây nhắm tới. Tài liệu XFA nhúng kiến trúc form XML của riêng chúng, nên hứa hẹn với khách hàng khả năng chỉnh sửa XFA đầy đủ sau một bản demo AcroForm nhanh là một cam kết bạn sẽ hối hận. Lưu ý thứ hai là về hiệu ứng phụ: đặt FormFill thành True cũng khởi tạo luôn JavaScript của tài liệu. Trong một viewer nhập liệu thì điều đó hoàn toàn đúng đắn, vì các script tính toán chính là thứ giữ cho một tổng số đang chạy luôn cập nhật khi ai đó gõ phím. Trong một cửa sổ xem trước cho các tệp không rõ nguồn gốc thì điều đó lại hoàn toàn sai. Bài viết về xem trước PDF an toàn trình bày phía FormFill := False của sự đánh đổi đó
Di chuyển bằng phím Tab đến đúng nơi người dùng kỳ vọng
Quay lại vấn đề bàn phím từ đầu. Cám dỗ là giả lập Tab bằng cách tổng hợp một cú click chuột lên hình chữ nhật của widget kế tiếp, cách này gãy ngay khi một trường bị cuộn ra ngoài màn hình hoặc hai widget chồng lên nhau. Thay vào đó, API focus di chuyển trực tiếp focus của chính form, không cần đoán mò hình học. Năm lệnh gọi bao trùm việc đó: FocusFormField theo chỉ mục, FocusNextFormField và FocusPreviousFormField để bước tới bước lui, FocusedFormFieldIndex để đọc bạn đang ở đâu, và ClearFormFieldFocus để bỏ hẳn focus
procedure TFormViewer.HandleTabKey(Shift: TShiftState);
begin
if ssShift in Shift then
PdfView.FocusPreviousFormField
else
PdfView.FocusNextFormField;
UpdateFieldStatus; // ví dụ "Trường 4 trên 17: InvoiceDate"
end;
Điều duy nhất về hành vi khiến người ta vấp phải là việc quay vòng. Việc di chuyển hoạt động xuyên qua thứ tự tab của trang hiện tại và lặp vòng bên trong nó: bước qua trường cuối cùng và bạn quay lại trường đầu tiên. Cả hai hàm bước đều trả về chỉ mục trường mới, hoặc -1 khi trang không chứa trường nào cả. Việc lặp vòng đó diễn ra theo từng trang, không theo cả tài liệu, nghĩa là việc băng sang trang kế tiếp là việc của bạn, không phải của thư viện. Hãy so sánh chỉ mục trả về với chỉ mục bạn xuất phát, để ý khi nó đã quay vòng, và tự tiến PageNumber lên nếu form được thiết kế để đọc như một chuỗi liên tục duy nhất. Bỏ qua lượt kiểm tra đó và một form hai trang sẽ âm thầm nhốt con trỏ ở trang một, đó là một biến thể riêng của khiếu nại Tab-bị-gãy
Việc di chuyển chỉ trở nên hữu ích khi phần còn lại của UI phản ứng lại với nó. Sự kiện OnFormFieldEnter bắn ra khi focus tới nơi, và trên viewer, OnFormFieldFocusChange báo cáo chỉ mục trường mới, để một panel bên cạnh có thể đồng bộ với bất cứ thứ gì bàn phím vừa chọn. Khi bạn cần phép ánh xạ ngược, từ một vị trí màn hình sang một trường, thuộc tính có chỉ mục FormFieldAt thực hiện hit-testing cho các bản xem trước tooltip và panel click-để-sửa. Có một lợi ích trợ năng thầm lặng trong tất cả những điều này: vì focus đi theo đúng thứ tự trường của chính tài liệu, đường bạn nối cho phím Tab cũng chính là đường mà một screen reader xướng lên, không cần thêm việc gì cả
Hiển thị tên trường thay vì các con số chỉ mục thô cần thêm một thuộc tính nữa. FormFieldInfo[] trả về một bản ghi TPdfFormFieldInfo cho mỗi chỉ mục, mang theo tên trường, loại, cỡ font, trạng thái checked, giá trị export, và thành viên nhóm, đây chính là những gì một danh sách điều hướng nên hiển thị ("Trường 4 trên 17: InvoiceDate" thay vì chỉ "4"). Radio group là trường hợp đáng có một tệp test riêng. Nhiều widget có thể chia sẻ chung một tên trường, nên một danh sách được lắp ráp ngây thơ từ các widget sẽ hiện cùng một nhóm nhiều lần và khiến bất kỳ ai đọc nó cũng bối rối
Vì sao giá trị đã điền lại ra trống rỗng, và lệnh gọi khắc phục nó
Lời khiếu nại khác lấp đầy hàng đợi hỗ trợ còn đáng báo động hơn cả một phím Tab hành xử sai: một form được điền bằng chương trình, khách hàng mở nó trong Acrobat, và mọi trường đều trông trống rỗng. Click vào một trường và giá trị của nó bật ra hiển thị ngay. Dữ liệu vẫn nằm trong tệp suốt cả thời gian đó. Thứ bị thiếu là bức tranh của dữ liệu, và lý do đáng để hiểu một lần cho xong vì nó giải thích cả một họ lỗi
Một trường text của AcroForm lưu giá trị của nó trong mục /V của dictionary trường (ISO 32000-1 §12.7.3.3). Thứ mà một viewer thực sự vẽ ra lại là một thứ tách biệt: appearance stream của widget dưới /AP (§12.5.5), một đoạn nội dung nhỏ đã render sẵn. Ghi /V mà để nguyên /AP, và hai thứ đó trôi lệch khỏi nhau. Giá trị vẫn ở đó; phiên bản đã render của nó thì cũ hoặc vắng mặt. Acrobat tình cờ dựng lại appearance của một trường khi nó nhận focus, đó là toàn bộ lời giải thích cho việc giá trị chỉ hiện ra khi click. Cờ NeedAppearances cũ, vốn yêu cầu viewer tự tái tạo appearance giúp bạn, chưa bao giờ hoạt động nhất quán và đã bị deprecated trong PDF 2.0, còn các print server và trình tạo thumbnail thì phớt lờ nó hoàn toàn. Chúng chỉ vẽ /AP và không gì khác, nên nếu /AP rỗng thì chúng in ra một ô trống
Gán một giá trị qua FormField[i] chỉ ghi /V mà thôi. Đó là lý do vì sao điền một form là một chuỗi ba bước, và bước mà các đội hay bỏ sót chính là bước ở giữa:
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]; // chỉ ghi /V
// Dựng lại các appearance stream /AP; nếu không làm điều này thì form
// sẽ trông trống rỗng trong Acrobat cho tới khi từng trường được click vào
Pdf.GenerateFormAppearances;
Pdf.SaveAs(OutputPath);
end;
GenerateFormAppearances chính là toàn bộ cách khắc phục. Nó dựng lại appearance stream của mọi widget từ các giá trị, font, và quadding hiện tại, để một viewer chưa bao giờ chạy một sự kiện focus, một print server hay một trình tạo thumbnail, vẫn vẽ ra trạng thái đã điền. Hãy gọi nó một lần sau cả lô gán giá trị, không phải một lần cho mỗi trường. Việc sinh appearance thực hiện công việc layout thật sự, và các lệnh gọi theo từng trường sẽ nhân chi phí đó lên khắp một form lớn một cách vô ích
Tái tạo appearance cũng chính là khoảnh khắc font và căn chỉnh tự khẳng định mình, đó là nguồn gốc của một bất ngờ bậc hai. Stream mới dàn từng giá trị bên trong hình chữ nhật của widget bằng font, cỡ, và quadding của trường đó. Một giá trị vừa vặn thoải mái trong form thử nghiệm của bạn có thể bị cắt hay co lại trong bản của khách hàng nơi cùng trường đó hẹp hơn. Trường tự-co-cỡ (cỡ font bằng 0) co chữ lại cho vừa; trường cỡ cố định thì chỉ cắt nó đi. Cả hai đều hợp lệ, và cách trung thực duy nhất để biết một form cụ thể làm theo cách nào là nhìn vào kết quả đã tái tạo thay vì chuỗi bạn đã viết. Khi có ai đó báo cáo văn bản bị cắt ở mép một ô, đây gần như luôn là lý do
Hãy xem việc xác minh là một phần của việc hoàn thành công việc, không phải một ý nghĩ chợt đến sau cùng. Mở tệp đã lưu trong Acrobat và xác nhận các giá trị hiện rõ trước khi bạn chạm vào bất kỳ trường nào. Sau đó in nó ra PDF hoặc ra ảnh từ một viewer khác, một viewer bỏ qua hoàn toàn logic form, và xác nhận các giá trị cũng sống sót qua đường đó. Kết hợp lại, hai lượt kiểm tra đó bắt được mọi biến thể của sự trôi lệch /V-so-với-/AP
Các cấu hình trường vượt qua demo nhưng thất bại ngoài thực tế
Các form demo sạch sẽ che giấu một tập trường hợp biên mà các tệp khách hàng thì không. Bốn trong số đó chiếm phần lớn các báo cáo kiểu "nó chạy tốt trên máy tôi"
- Giá trị export của checkbox. Trạng thái "on" không phải lúc nào cũng là
Yes. Một form được tự do định nghĩa giá trị export riêng của nó, và ghi sai chuỗi sẽ khiến ô đó trông như chưa được tick trong khi code của bạn tin chắc rằng nó đã đặt giá trị đó rồi. Hãy đọc giá trị export từFormFieldInfo[]thay vì tự giả định một giá trị - Radio group dùng chung tên. Một trường, nhiều widget. Giá trị bạn gán quyết định widget nào được đọc là đã chọn, nên code UI giả định một tên ánh xạ tới một hình chữ nhật sẽ vẽ vòng focus lên nhầm nút
- Trường được tính toán. Các tổng số được duy trì bởi JavaScript của tài liệu cập nhật để phản hồi các sự kiện trường. Một lượt điền bằng chương trình bỏ qua các sự kiện đó phải hoặc kích hoạt tính lại, hoặc ghi đè trực tiếp lên các trường đã tính. Một form nơi các dòng hàng hóa và tổng số không khớp nhau còn tệ hơn cả hai cách sửa đó
- Trường bắt buộc bị ẩn. Các form có điều kiện ẩn những trường vẫn được gắn cờ bắt buộc. Hãy quyết định trước liệu việc validate của bạn có tôn trọng tính hiển thị hay cờ bắt buộc thô, rồi ghi lại quyết định đó ở đâu đó mà đội hỗ trợ có thể tìm thấy
Có một sự phân biệt đáng chốt lại trước khi nó cắn bạn: sinh appearance không phải là flatten. GenerateFormAppearances khiến các giá trị hiện rõ ở mọi nơi trong khi vẫn để các trường có thể chỉnh sửa được. Flatten nướng appearance vào thẳng nội dung trang tĩnh và loại bỏ tính tương tác vĩnh viễn, điều này đúng đắn cho một bản lưu trữ và sai cho một form mà người kế tiếp vẫn còn phải điền. Nếu FormType báo cáo ftXfaFull thay vì ftAcroForm, thì dù sao chẳng bề mặt chỉnh sửa nào ở đây áp dụng gọn gàng cả, vì tài liệu render từ chính template XML của nó; hãy phát hiện trường hợp đó và báo cho người dùng biết, thay vì để họ tự tìm ra giới hạn đó
Hệ thống con form-fill, việc di chuyển focus, và việc sinh appearance được trình bày ở đây đều là một phần của PDFium Component cho Delphi, C++Builder, và Lazarus/FPC. Nếu viewer của bạn cũng xử lý markup của người review song song với dữ liệu form, bài viết về review annotation trình bày mô hình liền kề đó