כשטופס dynamic XFA בצופן Delphi מוסיף או מסיר עמודים, PDFium Component מדווח על הסך החדש דרך TPdf.PageCount ו-TPdf.OnXfaPageCountChanged מאז v3.126.1, כי אירוע העמודים הילידי נושא דלתא של נוסף/הוסר ולא סך כולל. ספריות ה-V8 של Windows ב-v3.126.1 מזיזות גם את אזורי הפגיעה של הקלט יחד עם שדות שהועברו, ו-v3.126.2 טוען מחדש מצביעי עמודים מיושנים אחרי שcallback הפריסה חוזר. דיווח הבאג שפתח את זה היה טופס הצהרת הוצאות: לוחצים פעמיים על Add Row, הטופס גדל לשני עמודים, ומחוון העמודים מצהיר בגאווה 1 מתוך 1. מקלידים לשדה שעבר לעמוד 2 והתווים נוחתים איפשהו בלתי נראה. אף אחד מזה לא הופיע בטפסים באורך קבוע שכולם בודקים איתם קודם, והסיבות שוות היכרות לכל מי שמטמיע צופה טפסים
מה קורה כשטופס dynamic XFA מתפרק לעמודים מחדש?
לטופס dynamic XFA אין רשימת עמודים קבועה, ולכן מספר העמודים שלו הוא פלט של פריסה ויכול להשתנות בכל עריכת נתונים של המשתמש. XFA 3.3 מתאר את הטופס בתור עץ של subforms; subform חוזר נשלט על ידי instanceManager, ותסריט כמו _Row.addInstance() משכפל שורה נוספת. מעבד הפריסה אז מזרים את התוכן אל אזורי העמודים מחדש, מה שעשוי להוסיף עמוד, להשמיט עמוד או לדחוף שדות קיימים אל עמוד אחר. ISO 32000-1 §12.7.8 מגדיר רק איך חבילות ה-XFA יושבות בתוך ה-PDF; כל מה שקורה אחר כך שייך למנוע ה-XFA, שב-PDFium Component הוא הפריסה של XFA של PDFium עצמו רצה בתוך תהליך המארח. צופן Delphi מתמודד לכן עם מסמך שמספר העמודים, גדלי העמודים ומיקומי ה-widgets שלו כולם מצב חי. שלושה דברים משתבשים כשהמארח מניח אחרת:
- מספר העמודים שהמארח שומר במטמון עבור ניווט, טווחי גלילה ומחווני עמודים מתיישן, או גרוע מכך, מתעדכן במספר הלא נכון
- שדות שעברו מקום מציגים את המסגרת שלהם במיקום החדש בזמן שהעורך ואזור הפגיעה של העכבר נשארים בקואורדינטות הישנות
- הצופה מחזיק מצביע עמודים שהפריסה החליפה, ולכן קליקים וציור הולכים אל עמוד שכבר לא קיים באותו טופס
שימור עריכות שורות על פני שמירה ופתיחה מחדש הוא בעיה נפרדת עם כללים משלה; המאמר הזה נשאר עם מה שקורה בזמן ריצה בתוך הצופה
איזה רנטיים PDFium צריך עבור dynamic XFA?
Dynamic XFA ב-PDFium Component דורש את build ה-V8/XFA של הספרייה הילידית, שנבחר על ידי המשתנה הגלובלי EnableV8Engine ביחידת PDFium לפני הטעינה של המסמך הראשון. התהליך מתחייב ל-DLL אחת בפעם הראשונה שכל TPdf טוען את הספרייה, ו-build PDFium רגיל לא יכול להריץ את מנוע ה-XFA בכלל. כשמסמך נפתח, TPdf כן צופה בקובץ אחר סמני XFA ועובר אוטומטית אל build ה-V8, אבל רק אם אף ספרייה רגילה עדיין לא נטענה באותו תהליך. כשההתחייבות כבר הלכה לכיוון הלא נכון, TPdf.OnXfaRuntimeMissing נורה פעם אחת כדי שהמארח יוכל לבקש מהמשתמש להפעיל מחדש. הגדרת הדגל במפורש בהפעלה מסירה את הניחושים. מבנה ה-callbacks של FPDF_FORMFILLINFO שנושא את אירועי ה-XFA חייב גם הוא להתאים ל-DLL; הרקע ב-FPDF_FORMFILLINFO גרסה 2 וה-ABI של ה-callbacks של XFA, ו-זיהוי טפסי XFA וקריאת החבילות שלהם מכסה הבחנה בין טיפוסי הטפסים לפני שפותחים צופה
uses
PDFium;
procedure TClaimForm.FormCreate(Sender: TObject);
begin
// מכריעים לפני שה-TPdf הראשון טוען את הספרייה הילידית:
// התהליך לא יכול לעבור מ-pdfium.dll אל 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;
מדוע PageCount דיווח 1 עבור טופס בן שני עמודים?
לפני v3.126.1, PDFium Component שמר את הארגומנט page_count של אירוע העמודים הילידי בתור הסך של המסמך, ואותו ארגומנט הוא בעצם ההפרש המוחלט בין מספרי העמודים החדש והישן. PDFium מעלה FFI_PageEvent אחרי שמעבר פריסה מסתיים עם טיפוס אירוע של עמוד נוסף או עמוד הוסר; פנימית הוא מעדכן קודם את מספר העמודים השמור שלו ואז מעביר abs(new - old). בפריסה הראשונית הספירה הישנה היא אפס, ולכן הדלתא שווה לסך, ודגימה סטטית של שלושה עמודים מדווחת שלושה עמודים כמצופה. זו בדיוק הסיבה שטפסי בדיקה באורך קבוע לעולם לא חשפו את הבאג. בפעם הראשונה שטופס דינמי גדל מעמוד אחד לשניים הדלתא היא 1, וה-wrapper הגדיר גם את TPdf.PageCount וגם את הפרמטר NewCount של OnXfaPageCountChanged ל-1. הסרת שורה מטופס של שלושה עמודים הפיקה את אותו סוג של איוולת בכיוון השני
הצטברות הדלתא על הערך הקודם אינה תיקון בטוח גם כן. סדר ה-callbacks של האתחול והפריסה אומר שה-wrapper לא תמיד יכול לסמוך על הספירה הקודמת שלו בתור בסיס, ולכן סכום נע יכול לסטות. מאז v3.126.1 ה-callback מתעלם מהארגומנט בתור ספירה וקורא ל-FPDF_GetPageCount על המסמך, שקורא את הסך מהפריסה שהושלמה זה עתה. הוא אז מנקה את סצנות העמודים המטמוניות, שומר את הסך הזה בתור עקיפת מספר-העמודים של ה-XFA מאחורי TPdf.PageCount, ורק אחר כך מעלה את OnXfaPageCountChanged. בזמן שה-handler שלכם רץ, NewCount ו-FPdf.PageCount מסכימים
procedure TClaimForm.PdfXfaPageCountChanged(Sender: TObject; NewCount: Integer);
begin
// v3.126.1 ומעלה: NewCount הוא הסך הכול של הפריסה שהושלמה, לעולם לא דלתא.
// זה רץ בתוך callback הפריסה של PDFium: מעדכנים רק מצב UI של ה-host,
// לא סוגרים את המסמך ולא טוענים עמודים מחדש מכאן
UpdatePageRange(NewCount);
end;
procedure TClaimForm.PdfViewPageChange(Sender: TObject);
begin
// נורה אחרי כל טעינת עמוד מחדש, כולל רענון ה-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;
האירוע נורה רק עבור טפסי Full XFA שהפריסה שלהם משתנה בזמן ריצה. מסמכי Static XFA ו-AcroForm לעולם לא מעלים אותו, ולכן צופה שמטפל בשניהם יכול להשאיר את אותו handler משויך. השארתו לא משויך בטוחה גם כן; העקיפה מאחורי TPdf.PageCount מיושמת בכל מקרה, והאירוע קיים כדי שהמארח ירענן את מה שמטמון
מדוע תיבת הקלט נשארת בעמוד הישן כששדה עובר מקום?
המסגרת זזה והעורך לא כי ה-notifier של ה-XFA הילידי השווה מלבן אל עצמו. כשפריסה משנה את הגיאומטריה של widget שכבר נטען, PDFium אמור לשים לב למלבן החדש ולקרוא ל-PerformLayout על ה-widget, מה שממקם מחדש את עורך הטקסט ואת אזור הפגיעה שלו. הבדיקה השוותה בין GetWidgetRect() ל-RecacheWidgetRect(). שתי הפונקציות מחזירות הפניה const לאותו חבר, וה-recache דורס את החבר הזה במקום, ולכן ההשוואה תמיד ראתה שני ערכים זהים ו-widgets טעונים דילגו על הסידור מחדש שלהם
הסימפטום עלה כשבדיקה שינתה גובה של subform כך ששדות קיימים חצו אל העמוד הבא. בשתי ארכיטקטורות ה-V8, מסגרת השדה צוירה במיקומה החדש בזמן שהטקסט שהוקלד ואזור הפגיעה של העכבר נשארו בקואורדינטת ה-Y הקודמת. סידור מחדש מפורש לא תיקן, וגם טעינת העמוד מחדש לא, כי ה-widget עדיין האמין שהגיאומטריה שלו עדכנית. ספריות ה-V8 של Windows שנשלחו עם v3.126.1 מעתיקות את המלבן הישן לפי ערך לפני ה-recache ומשוות את העותק הזה, ולכן widgets שזזו מסתדרים מחדש והערך הערוך מופיע בדיוק היכן שהמסגרת. זה תיקון ילידי: הוא נוסע עם ה-DLLs, ולכן עדכון יחידות ה-Pascal תוך השארת pdfium.v8.dll ישנה משאיר את אזורי הפגיעה המוזזים במקומם. בדיקת הרגרסיה שהניעה אותו עורכת קודם שורה שורדת לערך שאינו ברירת מחדל ואז דורשת את הערך הזה במיקום החדש של השדה, כי שורה שנבנתה מחדש עם ערכי ברירת מחדל הייתה נראית כמו עמידה
איך TPdfView טוען עמודים מחדש בלי לשלוף מצביע מתחת ל-PDFium?
מאז v3.126.2, TPdfView דוחה את טעינת העמודים מחדש שבאה בעקבות שינוי פריסת XFA עד שמחסנית הקריאות הילידית מתפרקת. אירוע העמודים בדרך כלל נורה בזמן ש-PDFium עדיין מעבד קלט: המשתמש לחץ על כפתור Add Row, הלחיצה הריצה תסריט, התסריט שינה את מספר המופעים, והפריסה הסתיימה בתוך אותה קריאה ילידית. סגירה ופתיחה מחדש של מצביע העמודים באותו רגע הייתה משחררת אובייקט שהקורא עדיין משתמש בו. לפני v3.126.2 הצופה רק פסל את עצמו, ולכן מצביע העמודים המוצג יכול היה להמשיך להצביע על מצב שלפני הפריסה, ואם המשתמש היה בעמוד האחרון כשהוא נעלם, מספר העמוד הנבחר היה מחוץ לטווח
הרענון הנדחה עובד בכמה צעדים קטנים, והם מסבירים את ההתנהגות שרואים מהמארח:
- ה-callback של אירוע העמודים מסמן את התצוגה בתור מחכה לרענון פריסת XFA ושולח הודעת חלון פרטית; אירועים חוזרים לפני שההודעה מגיעה מתמזגים לרענון אחד
- תצוגה שעדיין אין לה מצביע חלון שומרת על הדגל הממתין ושולחת את ההודעה מ-
CreateWnd, בזמן שהחלפת מסמכים, כיבוי התצוגה או השמדתה מנקים את הדגל - כשההודעה מגיעה, התצוגה מנקה את בחירת הטקסט, את הדגשת החיפוש ואת אינדקס השדה הממוקד, כי שלושתם התייחסו לפריסה הישנה
- העמוד הנבחר נחתך אל ה-
PageCountהחדש; מספר עמוד שהשתנה עובר את המעבר הרגיל בין עמודים, אחרת העמוד הנוכחי נטען מחדש, ומצב ההתאמה מוחל שוב - אם הפריסה משאירה בלי עמודים בכלל, התצוגה מפרקת את מצביע העמודים הישן שלה במקום לצייר עמוד שכבר לא קיים
אותה מגבלה חלה על הקוד שלכם. OnXfaPageCountChanged רץ בתוך callback הפריסה הילידי הזה, ולכן מתייחסים אליו בתור הודעה: מעדכנים שם תוויות, טווחי spinner ומצב סרגל כלים, וכל דבר כבד יותר, כמו סגירת המסמך או פתיחת אחר, נכנס לתור עם הודעה שנשלחת כדי שירוץ אחרי שה-callback חוזר. TPdfView.OnPageChange אז אומר לכם מתי התצוגה באמת טענה מחדש את העמוד, וקריאת PdfView1.PageNumber באותה נקודה נותנת לכם את הערך החתוך. מעבר טאב ובדיקות ה-FormType שצופה טפסים מריץ בפתיחה מכוסים ב-ניווט שדות טפסים ב-PDF עם PDFium Component
מדוע לחיצה על שדה Full XFA מעלה "Cannot open text page"?
לעמודי Full XFA אין עמוד טקסט PDF, ולפני v3.126.2 בחירת הטקסט וזיהוי הקישורים של ברירת המחדל של הצופה ניסו לטעון אחד בכל זאת. עם TPdfView.AllowUserTextSelection בברירת המחדל True, ריחוף שאל את שכבת הטקסט על תו תחת העכבר, ולחיצת עכבר-למעלה הריצה probe אוטומטי של URL על טקסט העמוד. בעמוד Full XFA אי אפשר לפתוח את עמוד הטקסט, ולכן קליק רגיל לתוך שדה יכל להסתיים בחריגה Cannot open text page. מאז v3.126.2 שני הנתיבים הפנימיים מחזירים אין תוצאה כש-TPdf.FormType הוא ftXfaFull והרנטיים של ה-XFA זמין, ולכן ההגדרות שמוגדרות כברירת מחדל עובדות וקלט השדות נשאר זמין
כיבוי AllowUserTextSelection עבור מסמכי Full XFA נשאר בחירת UI סבירה, כי אין טקסט עמוד לבחור ומחוות גרירה לא צריכות להתחיל מצב בחירה. זה לא תחליף לשדרוג, לעומת זאת: בגרסאות קודמות ה-probe של ה-URL בלחיצה לא היה תלוי בתכונה הזאת, ולכן צופה יכל להיתקל באותה חריגה גם כשהבחירה כבויה
procedure TClaimForm.ConfigureViewerForForm;
begin
// FormType קורא את המסמך הפתוח, ולכן קוראים לזה אחרי FPdf.Active := True
if FPdf.XFA and (FPdf.FormType = ftXfaFull) and FPdf.XfaRuntimeAvailable then
begin
// אין שכבת טקסט PDF בעמודי Full XFA; השדות נשארים ניתנים לעריכה
PdfView1.AllowUserTextSelection := False;
StatusBar1.SimpleText := Format('Dynamic XFA form, %d page(s)',
[FPdf.PageCount]);
end
else
PdfView1.AllowUserTextSelection := True;
end;
להקלדה היה תיקון משלה ב-v3.126.2. עורך הטקסט של ה-XFA הילידי לא מחליף בחירה כשהוא מקבל תו: FORM_OnChar מכניס בסמן, ו-Backspace מוחק תו אחד, ולכן בחירת ערך והקלדה עליו הפיקה טקסט ישן וחדש זה לצד זה. PDFium Component זוכר עכשיו שהלחיצה נחתה על שדה טקסט של XFA ומנתב תווים שהוקלדו, Backspace ו-Delete דרך FORM_ReplaceSelection בכל פעם שיש בחירה והמסמך מעניק הרשאת fill-forms או modify. האם שדה XFA לקריאה בלבד רשאי להשתנות נקבע עדיין על ידי העורך הילידי, ולכן שדה שסומן לקריאה בלבד בטופס שומר על ערכו גם במסמך שמתיר מילוי באופן אחר. הגדרת TPdfView.AllowFormEvents ל-False גם עוצרת את הניתוב הזה של המקלדת, מה ששומר צופה לקריאה בלבד לקריאה בלבד
עזר זריז: dynamic XFA בצופן Delphi
| סימפטום | סיבה | תוקן ב |
|---|---|---|
| מספר העמודים מציג 1 אחרי שהטופס גדל לשני עמודים | אירוע העמודים הילידי מעביר דלתא של נוסף/הוסר, לא סך | v3.126.1 (wrapper) |
| מסגרת השדה זזה, הטקסט שהוקלד ואזור הפגיעה נשארים מאחור | widget טעון דילג על סידור מחדש אחרי השוואה-עצמית | v3.126.1 (ספריות ה-V8 של Windows) |
| הצופה מצייר או מנתב קלט אל מצב עמוד שלפני הפריסה | מצביע העמודים לא נטען מחדש אחרי רפאגינציה | v3.126.2 (רענון נדחה) |
| לחיצה לתוך שדה מעלה Cannot open text page | בחירת טקסט ו-probe של URL על עמודים בלי שכבת טקסט | v3.126.2 |
| הקלדה על ערך נבחר מוסיפה במקום להחליף | עורך ה-XFA הילידי מכניס בסמן | v3.126.2 |
- מגדירים
EnableV8Engineל-Trueלפני טעינת מסמך כלשהו, ומטפלים ב-OnXfaRuntimeMissingעבור המקרה שהספרייה הרגילה נטענה קודם - קוראים את הסך מ-
TPdf.PageCountאו מהפרמטרNewCountשלOnXfaPageCountChanged; לעולם לא מחברים או מחסירים מספרי עמודים בעצמכם - משאירים את ה-handler של
OnXfaPageCountChangedקל משקל, כי הוא רץ בתוך callback הפריסה הילידי - מסנכרנים את מחוון העמוד הנוכחי ב-
TPdfView.OnPageChange, שנורה אחרי שהטעינה מחדש הנדחה חותכת את מספר העמוד - מפיצים את ה-DLLs של ה-V8 ל-Windows מ-v3.126.1 ומעלה יחד עם היחידות; תיקון סידור ה-widgets מחדש חי בקוד ילידי
- בודקים עם טופס שבאמת משנה את מספר העמודים שלו ומזיז שדה ערוך מעבר לשבירת עמוד, כי דגימות באורך קבוע מסתירות כל באג ברשימה הזאת
Dynamic XFA הופך את מספר העמודים ואת הגיאומטריה של השדות לערכים חיים, וצופה נשאר נכון רק כשהוא לוקח אותם מהפריסה שהושלמה וטוען עמודים מחדש ברגע בטוח. PDFium Component מטפל בשניהם בתוך TPdf ו-TPdfView, ולכן למארח נותר רק להקשיב. פרטים והורדות ב-דף המוצר של PDFium Component ל-Delphi