Повідомлення про помилку Please load the document before using BeginDoc майже завжди з'являється під час другого запуску. Перший документ записується чудово. Потім той самий екземпляр THotPDF отримує запит розпочати другий, BeginDoc генерує виняток, і повідомлення вказує на завантаження документа, що є прямою протилежністю тому, що намагається зробити код. Саме ця невідповідність між симптомом і повідомленням змушує задуматися. Справжня причина криється в життєвому циклі компонента, і щойно це стає зрозумілим, помилка перестає бути загадкою

Після завершення документа внутрішній стан THotPDF ще містить ресурси попереднього файлу, тому повторне використання вимагає явного завершення та нового циклу ініціалізації
Екземпляр THotPDF представляє один документ, а не фабрику документів
Спокуслива ментальна модель полягає в тому, що THotPDF є сервісним об'єктом, який ви запускаєте один раз і згодовуєте йому документи, так само як ви могли б тримати відкритим з'єднання з базою даних і виконувати запит за запитом через нього. Але це не так. Екземпляр моделює один документ, що будується, і його внутрішній автомат станів передбачає, що він проходить шлях лише один раз: від порожнього, через відкритий документ, до збереженого файлу. BeginDoc відкриває цей шлях і позначає екземпляр як такий, що має документ у процесі обробки. EndDoc серіалізує все у FileName і закриває його. Повторний виклик BeginDoc для того самого завершеного екземпляра змушує його повторно увійти до стану, з якого він ніколи не виходив коректно, і спрацьовує перевірка, повідомлення якої випадково згадує завантаження, оскільки всередині умови "готовий до початку" та "має завантажений документ" перевіряються разом
Отже, повідомлення вводить в оману, але перевірка виконує свою роботу. Вона відмовляється дозволити вам почати новий документ поверх компонента, який усе ще вважає, що перебуває посеред документа. Виправлення полягає не в тому, щоб обійти цю перевірку. Воно полягає в тому, щоб припинити повторно використовувати відпрацьований екземпляр
Життєвий цикл, у порядку, в якому він має відбуватися
Кожен документ, який 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, один файл на диску. У ту саму мить, коли вам потрібен другий файл, ви починаєте нову одиницю роботи, що означає новий екземпляр
Що має означати "повторне використання": свіжий екземпляр для кожного файлу
Версія, яка ламається, намагається бути ощадливою з виділенням пам'яті: створює компонент один раз, проходить через цикл, викликає BeginDoc і EndDoc усередині циклу. На другій ітерації виникає виняток. Версія, яка працює, розглядає кожен вихідний файл як окремий короткоживучий об'єкт, а витрати на виділення пам'яті для створення компонента є мізерними порівняно з роботою з компонування та серіалізації 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 намагається записати той самий шлях, операційна система відмовляє, оскільки програма перегляду утримує спільний доступ на читання, який блокує записувачів, і ви отримуєте помилку відмови в доступі (access denied). Це справжня проблема блокування файлів Windows, а не проблема стану компонента, і вона заслуговує на справжнє рішення замість тимчасового обхідного шляху (workaround)
Обхідний шлях, що циркулює навколо, який перераховує вікна верхнього рівня та надсилає WM_CLOSE будь-якому, чий заголовок схожий на програму перегляду PDF, є неправильним інстинктом. Він долає межі процесів, щоб закрити вікна, якими ваша програма не володіє, вгадує програми перегляду за текстом заголовка і може викинути незбережені анотації користувача без запиту. Ставтеся до всього цього підходу як до поганого коду (code smell). Надійне виправлення полягає в тому, щоб ніколи не писати за шляхом, який може утримуватися іншим процесом. Зробіть серіалізацію у тимчасовий файл у тому ж каталозі, а потім перемістіть його на потрібне місце за допомогою атомарного перейменування, щойно EndDoc завершиться успішно. Якщо програма перегляду все ще тримає відкритим старий файл, перейменування або успішно завершується, або голосно зазнає невдачі, і ви виводите чітке повідомлення замість того, щоб боротися з блокуванням
Для високонавантаженого сервера, який постійно генерує документи, чистіша дисципліна полягає в тому, щоб записувати кожен результат під унікальним іменем (міткою часу або ідентифікатором завдання), щоб два запуски ніколи не конкурували за один шлях, і дозволити окремій політиці зберігання очищати старі файли. У будь-якому випадку принцип однаковий: проєктуйте так, щоб файл, який ви пишете, належав лише вам у момент запису. Блокування зникає не тому, що ви примусово закрили вікно, а тому, що ніщо інше не торкається цих байтів
Форма виправлення
Зведіть обидві проблеми до їхніх коренів, і вони обидві стосуються дотримання меж. Помилка автомата станів вимагає від вас поважати межу екземпляра: один THotPDF, один документ, потім відпустіть його і створіть інший. Помилка блокування файлу вимагає поважати межу файлу: пишіть туди, де ніщо інше не читає, а потім перемістіть результат на потрібне місце. Жодна з них не вимагає виправлення бібліотеки або написання скриптів для робочого столу. Обидві вирішуються через ставлення до кожного документа як до автономної одиниці роботи, створеної свіжою, записаної чисто і звільненої, що є тим самим шаблоном, який робить решту компонента передбачуваною
Виклики BeginDoc, EndDoc, LoadFromFile та SaveLoadedDocument, показані тут, є частиною HotPDF Component для Delphi та C++Builder