השגיאה קוראת Please load the document before using BeginDoc, והיא כמעט תמיד מופיעה בפעם השנייה. המסמך הראשון נכתב בסדר. לאחר מכן אותו מופע THotPDF מתבקש להתחיל מסמך שני, BeginDoc מעלה (raises) שגיאה, וההודעה מצביעה על טעינת מסמך, שזה ההפך ממה שהקוד מנסה לעשות. אי-ההתאמה בין התסמין להודעה היא מה שהופך את השגיאה הזו לזכורה. הנושא האמיתי הוא מחזור החיים של הרכיב, וברגע שזה מובן, השגיאה מפסיקה להיות מסתורית

מופע THotPDF הוא מסמך אחד, לא מפעל למסמכים
המודל המנטלי המפתה הוא ש-THotPDF הוא אובייקט שירות שאתה מפעיל פעם אחת ומזין לו מסמכים, באותו אופן שאתה עשוי להשאיר חיבור למסד נתונים פתוח ולהריץ דרכו שאילתה אחר שאילתה. זה לא המצב. מופע ממדל מסמך יחיד הנבנה, ומכונת המצבים (state machine) הפנימית שלו נושאת את ההנחה שהיא הולכת בנתיב פעם אחת: מריק, דרך מסמך פתוח, לקובץ שמור. BeginDoc פותח את הנתיב הזה ומסמן את המופע כבעל מסמך בתהליך. EndDoc מבצע סריאליזציה של הכל ל-FileName וסוגר אותו. קריאה ל-BeginDoc שוב על אותו מופע שהסתיים מבקשת ממנו להיכנס מחדש למצב שהוא מעולם לא עזב בצורה נקייה, והשומר (guard) שמופעל הוא זה שההודעה שלו מזכירה במקרה טעינה, משום שבאופן פנימי התנאים "מוכן להתחיל" ו"יש מסמך טעון" נבדקים יחד
אז ההודעה מטעה, אך השומר עושה את עבודתו. הוא מסרב לאפשר לך להתחיל מסמך חדש על גבי רכיב שעדיין מאמין שהוא באמצע מסמך. התיקון הוא לא להביס את השומר. אלא להפסיק לעשות שימוש חוזר במופע מנוצל
מחזור החיים, בסדר בו הוא חייב לקרות
כל מסמך ש-HotPDF כותב מאפס עוקב אחר אותן ארבע פעימות, והסדר אינו נתון למשא ומתן. Create מקצה את הרכיב. BeginDoc פותח את המסמך ומקבע את הבחירות המבניות, כך שכל מה שמשפיע על הקובץ כולו (גודל עמוד, דחיסה, הצפנה, שם קובץ הפלט) חייב להיקבע בין Create ל-BeginDoc. לאחר מכן אתה מצייר. לאחר מכן EndDoc כותב את הבתים לדיסק. Free משחרר את המופע. קריאות ציור הממוקמות לפני BeginDoc אין להן עמוד לנחות עליו; מאפיינים של כל המסמך המוקצים אחריו זוכים להתעלמות ללא תלונה
var
Pdf: THotPDF;
begin
Pdf := THotPDF.Create(nil);
try
Pdf.FileName := 'invoice.pdf';
Pdf.BeginDoc; // opens the document
Pdf.CurrentPage.SetFont('Arial', [], 11);
Pdf.CurrentPage.TextOut(50, 760, 0, 'Invoice 2026-042');
Pdf.EndDoc; // writes invoice.pdf, closes it out
finally
Pdf.Free; // one instance, one document
end;
end;
קרא זאת כיחידת העבודה. Create אחד, BeginDoc אחד, EndDoc אחד, Free אחד, קובץ אחד בדיסק. ברגע שאתה רוצה קובץ שני, אתה מתחיל יחידת עבודה חדשה, שמשמעותה מופע חדש
מה "שימוש חוזר" אמור להיות: מופע חדש לכל קובץ
הגרסה שנשברת מנסה להיות חסכונית בהקצאה: בנה את הרכיב פעם אחת, עבור בלולאה על אצווה (batch), קרא ל-BeginDoc ול-EndDoc בתוך הלולאה. האיטרציה השנייה זורקת שגיאה. הגרסה שעובדת מתייחסת לכל פלט כאובייקט קצר-מועד משלו, ועלות ההקצאה של יצירת רכיב היא זניחה לעומת העבודה של פריסה (layout) וסריאליזציה של PDF, כך שאין מה לחסוך על ידי אגירת המופע
procedure WriteBatch(const Names: TArray<string>);
var
I: Integer;
Pdf: THotPDF;
begin
for I := 0 to High(Names) do
begin
Pdf := THotPDF.Create(nil); // new instance each pass
try
Pdf.FileName := Names[I] + '.pdf';
Pdf.BeginDoc;
Pdf.CurrentPage.SetFont('Arial', [], 12);
Pdf.CurrentPage.TextOut(50, 760, 0, 'Statement for ' + Names[I]);
Pdf.EndDoc;
finally
Pdf.Free;
end;
end;
end;
ה-try/finally שיושב בתוך הלולאה הוא החלק ששווה להגן עליו בסקירת הקוד. אם BeginDoc או קריאת ציור כלשהי מעלים שגיאה באמצע מסמך אחד, המופע של אותה איטרציה עדיין משוחרר לפני תחילת הבאה, כך שרשומה גרועה אחת אינה משאירה רכיב בנוי למחצה ומרעילה את שאר הריצה. משוך את ה-Create החוצה מעל הלולאה כדי "לייעל" ותחזור לבאג המקורי, שעכשיו עוטה לולאת אצווה
שינוי קובץ קיים הוא נקודת כניסה שונה
ישנה קריאה שנייה של "שימוש חוזר" שהיא לגיטימית לחלוטין: אינך רוצה מסמך ריק, אתה רוצה לפתוח PDF שכבר קיים ולשנות אותו. נתיב זה אינו עובר דרך BeginDoc כלל, וזו בדיוק הסיבה שהודעת השגיאה מציינת טעינה. אתה טוען את הקובץ, עורך אותו, ושומר תחת איזה שם שתבחר
var
Pdf: THotPDF;
PageCount: Integer;
begin
Pdf := THotPDF.Create(nil);
try
PageCount := Pdf.LoadFromFile('contract.pdf');
if PageCount > 0 then
begin
Pdf.CurrentPage.SetFont('Arial', [fsBold], 10);
Pdf.CurrentPage.TextOut(40, 30, 0, 'REVIEWED');
Pdf.SaveLoadedDocument('contract-reviewed.pdf');
end;
finally
Pdf.Free;
end;
end;
LoadFromFile מחזיר את ספירת העמודים, וערך של אפס או פחות פירושו שהטעינה נכשלה, כך ששווה לבדוק לפני שאתה נוגע ב-CurrentPage. הזיווג משנה: מסמך שפתחת עם LoadFromFile נשמר עם SaveLoadedDocument, לא עם הצמד BeginDoc/EndDoc, השייך למסמכים שאתה יוצר מכלום. ערבוב השניים הוא הדרך הנפוצה ביותר לבלבל את אותה מכונת מצבים שהפיקה את השגיאה המקורית. שמור על שתי הזרימות נפרדות מבחינה מנטלית: BeginDoc ... EndDoc יוצר, LoadFromFile ... SaveLoadedDocument עורך
בעיית נעילת הקבצים אמיתית, והתשובה אינה להרוג חלונות צופייה
שגיאת השימוש החוזר נוסעת לעתים קרובות עם תלונה שנייה, והשתיים מסתבכות זו בזו מכיוון שהן צצות באותה זרימת עבודה של יצירה מחודשת של הקובץ. משתמש פותח את ה-PDF שזה עתה הפקת, משאיר אותו פתוח ב-Acrobat או Foxit, ולאחר מכן מפעיל בנייה מחדש. EndDoc מנסה לכתוב את אותו נתיב, מערכת ההפעלה מסרבת מכיוון שהצופה (viewer) מחזיק בשיתוף קריאה החוסם כותבים, ואתה מקבל שגיאת גישה-נדחתה. זוהי באמת בעיית נעילת קבצים (file-locking) של Windows ולא בעיית מצב של הרכיב, והיא ראויה לתשובה אמיתית במקום מעקף (workaround)
המעקף שמסתובב, המונה חלונות ברמה העליונה ושולח WM_CLOSE לכל מה שהכותרת שלו נראית כמו צופה PDF, הוא האינסטינקט השגוי. הוא חוצה גבולות תהליכים (process boundaries) כדי לסגור חלונות שהתוכנית שלך אינה הבעלים שלהם, הוא מנחש לגבי צופים לפי טקסט הכותרת, והוא יכול לזרוק הערות (annotations) לא שמורות של משתמש מבלי לשאול. התייחס לכל הגישה הזו כסירחון קוד (smell). התיקון האמין הוא לעולם לא לכתוב לנתיב שתהליך אחר עשוי להחזיק. בצע סריאליזציה לקובץ זמני באותה ספרייה, ואז החלף אותו למקומו עם שינוי שם אטומי (atomic rename) לאחר ש-EndDoc מצליח. אם צופה עדיין מחזיק את הקובץ הישן פתוח, שינוי השם או מצליח בצורה נקייה או נכשל ברעש, ואתה מציף הודעה ברורה במקום להילחם בנעילה
עבור שרת בנפח גבוה המייצר מסמכים מחדש ללא הרף, משמעת נקייה יותר היא לכתוב כל פלט תחת שם ייחודי (חותמת זמן או מזהה עבודה) כך ששתי ריצות לעולם לא יתמודדו על נתיב אחד, ולתת למדיניות שימור נפרדת לנקות קבצים ישנים. כך או כך העיקרון זהה: תכנן כך שהקובץ שאתה כותב הוא שלך בלבד ברגע שאתה כותב אותו. הנעילה נעלמת לא בגלל שאילצת חלון להיסגר אלא בגלל ששום דבר אחר לא נוגע בבתים
צורת התיקון
הפשט את שתי הבעיות בחזרה לשורשים שלהן ושתיהן עוסקות בכיבוד גבולות. שגיאת מכונת המצבים (state-machine) רוצה שתכבד את גבול המופע: THotPDF אחד, מסמך אחד, ואז שחרר אותו וצור אחר. שגיאת נעילת הקובץ רוצה שתכבד את גבול הקובץ: כתוב היכן ששום דבר אחר לא קורא, ואז הזז את התוצאה למקומה. אף אחת מהן לא דורשת תיקון של הספרייה או כתיבת סקריפטים לשולחן העבודה. שתיהן נובעות מהתייחסות לכל מסמך כיחידת עבודה עצמאית, נוצרת מחדש, נכתבת בצורה נקייה ומשוחררת, שזהו אותו דפוס שהופך את שאר הרכיב לצפוי
הקריאות BeginDoc, EndDoc, LoadFromFile, ו-SaveLoadedDocument המוצגות כאן הן חלק מרכיב HotPDF עבור Delphi ו-C++Builder