פקד ה-TPDFlibViewer של PDF Library for Delphi מבצע עורך שדה-טופס-במקום דרך אירוע ה-OnExit של העורך עצמו, ובחירת העיצוב הזו מחביאה מלכודת VCL קלאסית של Delphi: הסתרה, שינוי-הורה, או השמדה של פקד ממוקד מתוך ה-handler של ה-OnExit שלו עצמו יכולה להפעיל OnExit פעם שנייה לפני שהקריאה הראשונה חוזרת, שולחת את לוגיקת-הביצוע בחזרה לתוך עצמה
הכשל שזה מייצר עומד בפני שחזור נקי. משתמש עובר במהירות עם Tab על ריצה של שדות טקסט בטופס בקשה סרוק, ומדי פעם המציג זורק הפרת-גישה, או גרוע יותר, ממשיך לרוץ בעוד הוא בשקט כותב את הערך הלא-נכון לתוך שדה שני Tab-ים אחורה. שחזר את זה לפי דרישה והבאג נראה ברור במבט לאחור; רדוף אחריו מדוח קריסה של לקוח בודד והוא נראה כמו רוח-רפאים, משום שאם ה-OnExit השני בפועל מופעל תלוי בתזמון ידית-חלון ופוקוס שמשתנה עם סוג-שדה, מהירות-הקלדה, ומה שתור-ההודעות עושה ברגע ההוא
איך TPDFlibViewer מניחה עורך אמיתי מעל עמוד מעובד
TPDFlibViewer מעבדת כל עמוד PDF לביטמאפ ולא הופכת שדות טופס לפקדי VCL חיים כברירת מחדל, כך ש-BeginEditFormField היא הפונקציה שמגשרת בין שני העולמות: נקראת עם אינדקס שדה, היא מחפשת את המלבן של השדה וממירה אותו לקואורדינטות לקוח, ואז מניחה TEdit או TMemo אמיתי מעל המלבן ההוא עבור שדה טקסט, או TComboBox בסגנון csDropDownList עבור שדה בחירה, שלם עם הערך הנוכחי של השדה כבר טעון. ISO 32000-2 סעיף 12.7 מגדיר מה שדה טופס טקסט או בחירה הוא בתוך PDF, אבל שום דבר במפרט ההוא לא אומר איך אפליקציית Windows צריכה לתת למישהו להקליד לתוך אחד, והפער הזה בדיוק מה ש-BeginEditFormField קיימת כדי למלא. גם OnKeyDown וגם OnExit מחווטים לאותן שתי פונקציות מציג, InplaceEditorKeyDown ו-InplaceEditorExit, על כל עורך ש-TPDFlibViewer יוצרת, צימוד ששולח ללא שינוי מאז שמילוי-טופס אינטראקטיבי נחת לראשונה ב-v3.220.0, ו-OnExit הוא המקום שבו הצרה מתחילה
למה הסתרת העורך מפעילה OnExit פעם שנייה?
TWinControl ב-VCL מתייחס לשינוי ב-Visible או Parent על פקד ממוקד כסיבה להזיז פוקוס ממנו מיד, והזזת פוקוס מפקד היא בדיוק מה שמפעיל את אירוע ה-OnExit של הפקד ההוא, סינכרונית, לפני שהצבת המאפיין שהפעילה את זה אפילו חוזרת. CommitInplaceEditor, הפונקציה ש-PDF Library for Delphi משתמשת בה כדי לסגור את העורך במקום ולכתוב את הערך שלו בחזרה לשדה הטופס, זקוקה לעשות בדיוק את שני הדברים האלה בדרך החוצה: הגדר את Editor.Visible ל-False והגדר את Editor.Parent ל-nil כך שהפקד יפסיק לצייר מעל העמוד ויפסיק לקבל קלט. עשה אחד מהם בעוד העורך עדיין מחזיק פוקוס, מה שהוא כמעט תמיד עושה שכן המשתמש הרגע עזב אותו, ו-OnExit מופעל שוב באמצע אותה קריאה בדיוק שהייתה אמורה להיות הדבר האחרון שה-OnExit של העורך ההוא אי-פעם הפעיל
מה משתבש כאשר CommitInplaceEditor נכנסת-מחדש לעצמה?
פונקציית ביצוע תמימה משלמת על זה באחת משתי דרכים. או שהיא כותבת את ערך השדה פעמיים, פעם אחת מהקריאה המקורית ופעם אחת מהקריאה הנכנסת-מחדש שהתגנבה לפני שהראשונה סיימה לגעת במצב שלה עצמה, או שהיא מנסה לשחרר את פקד העורך בעוד מסגרת-stack עמוקה יותר עדיין בתוך ה-handler של האירוע של אותו פקד עצמו, שזה שטח בלתי-מוגדר ב-VCL ומופיע כהפרת-גישה שיכולה להצביע על כמעט כל שורה, לא בהכרח זו שבפועל גרמה לזה. אף כשל לא זקוק לטופס גדול כדי להיות מופעל; מסמך בן-שני-שדות מספיק, בתנאי שהמשתמש עוזב את השדה השני מהר מספיק כדי שמערכת ההפעלה עדיין תפרום הודעות פוקוס מהראשון
// גרסה נאיבית: נקראת תקין בסקירה, נכשלת רק במהירות הקלדה אמיתית
procedure TMyPdfViewer.EditorExit(Sender: TObject);
begin
CommitEditor; // עדיין רץ בתוך ה-OnExit של FEditor עצמו
end;
procedure TMyPdfViewer.CommitEditor;
begin
if not Assigned(FEditor) then
Exit;
SaveFieldValue(FEditor.Text);
FEditor.Parent := nil; // הפקד הממוקד משנה הורה כאן: OnExit
// מופעל שוב, נכנס-מחדש לאותה מתודה בדיוק
FEditor.Free; // משוחרר בעוד קורא עמוק יותר במטה
FEditor := nil; // ה-stack עדיין בתוך handler ה-OnExit שלו
end;
אפס את ההפניה לפני שאתה נוגע בפקד
התיקון ש-PDF Library for Delphi שולחת הוא סידור-מחדש בודד: לכוד את העורך במשתנה מקומי, נקה את השדה שמצביע עליו, ורק אז התחל לשנות את מאפייני הפקד. CommitInplaceEditor קוראת את FInplaceEditor לתוך משתנה מקומי Editor, מגדירה את FInplaceEditor ל-nil מיד, ורק אחר-כך מציבה את Editor.Visible ו-Editor.Parent. קריאה נכנסת-מחדש שמופעלת על ידי אחת משתי ההצבות ההן קוראת את FInplaceEditor עצמה, מוצאת אותה כבר nil, ויוצאת בשורה הראשונה שלה בדיוק, לפני שהיא יכולה לגעת ב-Editor או לכתוב את ערך השדה פעם שנייה
procedure TPDFlibViewer.CommitInplaceEditor(Save: Boolean);
var
Editor: TWinControl;
begin
Editor := FInplaceEditor;
if not Assigned(Editor) then
Exit; // קריאה נכנסת-מחדש נוחתת כאן ועוצרת
FInplaceEditor := nil; // נתק לפני שנוגעים בפקד בכלל
if Save then
SaveEditorValue(Editor); // בטוח: FInplaceEditor כבר nil
Editor.Visible := False;
Editor.Parent := nil; // עשוי להפעיל OnExit שוב; השומר למעלה
// הופך את הקריאה הנכנסת-מחדש ההיא ל-no-op
ReapDeadEditor; // שחרר כל מה שחנה במחזור הקודם
FDeadEditor := Editor; // חנה את זה במקום לשחרר אותו כאן
end;
SaveEditorValue ברשימה ההיא עומדת במקום הענף האמיתי, שבודק אם Editor הוא TComboBox, TMemo, או TEdit וקורא את הערך שלו בהתאם, שכן PDF Library for Delphi יוצרת פקד שונה בהתאם לשאלה אם השדה הוא שדה-טקסט או שדה-בחירה. השמירה לא אכפת לה איזה ענף רץ, רק ש-FInplaceEditor הוא nil לפני שכל דבר שמסוגל להפעיל OnExit מבוצע, וזה אילוץ הסידור היחיד שהופך את שאר הפונקציה לבטוחה לכתיבה בכל סגנון שאחרת טבעי
לעולם אל תשחרר פקד מתוך האירוע שלו עצמו
TPDFlibViewer.CommitInplaceEditor אף פעם לא קוראת ל-Editor.Free ישירות, וזה מכוון: שחרור פקד לא בטוח בעוד מסגרת-stack ששייכת לאותו פקד עצמו של שיגור-האירוע שלו אולי עדיין נפרמת מעל הקריאה ששחררה אותו, OnExit נכנס-מחדש או לא. במקום זאת PDF Library for Delphi מוסרת את העורך המנותק למקום-חנייה בעל-משבצת-בודדת, FDeadEditor, משחררת כל מה שיושב שם ממחזור העריכה הקודם דרך עוזר קטן, ReapDeadEditor, נקראת בתחילת ה-BeginEditFormField הבא ופעם נוספת מ-CloseDocument; כל עורך שהמציג יוצר גם בבעלות המציג עצמו, TEdit.Create(Self) ולא TEdit.Create(nil), כך שאפילו פקד שעדיין חונה ב-FDeadEditor כשהמציג נהרס נאסף על ידי בעלות רכיב VCL רגילה במקום לדלוף
procedure TPDFlibViewer.ReapDeadEditor;
begin
if Assigned(FDeadEditor) then
begin
FDeadEditor.Free; // בטוח עכשיו: ה-OnExit של הפקד הזה עצמו
FDeadEditor := nil; // הסתיים לפחות מחזור עריכה אחד קודם
end;
end;
function TPDFlibViewer.BeginEditFormField(FieldIndex: Integer): Integer;
var
Edit: TEdit;
begin
Result := 0;
CommitInplaceEditor(True); // שטוף כל עורך שעדיין פתוח
// ... חיפוש השדה והמרת המלבן הושמטו ...
ReapDeadEditor; // עכשיו בטוח לשחרר את העורך שחנה במחזור הקודם
Edit := TEdit.Create(Self);
Edit.Parent := Self;
Edit.OnExit := InplaceEditorExit;
FInplaceEditor := Edit;
FInplaceEditor.SetFocus;
Result := 1;
end;
למה זה מופיע הכי חזק במהלך ניווט Tab מהיר?
FocusNextFormField, הפונקציה ש-PDF Library for Delphi הוסיפה ב-v3.226.0 כדי להניע ניווט Tab ו-Shift+Tab על פני טופס, קוראת ל-BeginEditFormField עבור השדה הכשיר הבא בכל קפיצה בודדת, ו-BeginEditFormField נפתחת בקריאה ל-CommitInplaceEditor(True) כדי לשטוף כל עורך שהשדה הקודם השאיר פתוח. זה אומר שכל לחיצת Tab שמשתמש עושה בעת מילוי טופס רב-שדות מריצה את רצף הניתוק-ואז-נגיעה המדויק שתואר לעיל פעם אחת, שזה בדיוק נתיב הקוד הכי סביר שעדיין יש לו פקד ממוקד באמת ברגע ש-Visible ו-Parent משתנים, משום ש-Tab הוא האינטראקציה האחת שכמעט מובטח שתשאיר את העורך היוצא מחזיק פוקוס ממש עד שהחדש מבקש אותו
שום דבר מזה לא הופך את הבאג לאמין להדגמה, וזה שווה לומר בפירוש ולא לטשטש. אם הצבת Visible או Parent נתונה בפועל כופה OnExit סינכרוני תלוי במצב פוקוס וידית-חלון שדיבאגר משנה סתם על ידי היותו מחובר, שציור-מחדש בלתי-קשור או טיימר יכולים לערער, ושמתנהג אחרת בהתאם לאיזה מ-TEdit, TMemo, או TComboBox במקרה הפקד במשחק. שמירה שרק לפעמים מופעלת היא הסיבה שהסוג הזה של פגם שורד סקירת קוד ובדיקה ידנית כאחד, וזו גם הסיבה שהתיקון חייב להיות נכון מבנייתו, מאפס את ההפניה לפני שכל דבר אחר קורה, ולא נכון לפי איזו התנהגות שכמה מעברי בדיקה ידנית במקרה צפו בה
הצורה הכללית של התיקון הזה נוסעת הרבה מעבר לפקד מציג אחד. כל משטח עריכה מותאם אישית שבנוי על ידי שכיבת פקד VCL חי מעל תוכן מעובד, לא רק שדה טופס PDF, יורש את אותה סכנה ברגע שלוגיקת הסגור-ובצע שלו יכולה להיות מופעלת גם על ידי פעולת משתמש מפורשת וגם על ידי שינוי פוקוס משתמע, ואותה תשובה דו-חלקית חלה: נקה את ההפניה שמזהה את הפקד הפעיל לפני שאתה עושה כל דבר שעלול להפעיל את אירוע-היציאה שלו עצמו, ולעולם אל תקרא ל-Free מנתיב קוד שאולי עדיין רץ מתחת לשיגור-האירוע של אותו פקד עצמו. משטח מילוי-הטפסים והעיבוד הרחב יותר של TPDFlibViewer, כולל איך היא מחליטה איזה סוג פקד להציג עבור איזה שדה, מכוסה בסקירת בניית פקד מציג PDF אינטראקטיבי ב-Delphi VCL עם PDF Library for Delphi, ומטמון ביטמאפ העמוד ש-SetFormFieldValueAndRefresh חייבת לבטל בכל עריכה שמבוצעת מכוסה בנפרד בהחלק על מטמון עמוד-דיסק לפי-DPI-של-מוניטור של המציג
עריכת שדה-טופס במקום, ניווט שדה מונע-Tab, ונתיב-הביצוע הבטוח-לכניסה-חוזרת מאחורי שניהם הם חלק מפקד המציג האינטראקטיבי שנשלח עם PDF Library for Delphi, ספריית ה-PDF עבור Delphi ו-C++Builder, לצד שאר משטח ה-API של עיבוד עמודים, הערות, ושדות-טופס שלה