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

Екземпляр 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; // відкриває документ
Pdf.CurrentPage.SetFont('Arial', [], 11);
Pdf.CurrentPage.TextOut(50, 760, 0, 'Invoice 2026-042');
Pdf.EndDoc; // пише invoice.pdf і закриває його
finally
Pdf.Free; // один екземпляр, один документ
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); // новий екземпляр на кожен прохід
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 намагається записати той самий шлях, операційна система відмовляє, бо переглядач тримає спільний доступ на читання, який блокує записувачів, і ви отримуєте відмову в доступі. Оце вже справді проблема блокування файлів у Windows, а не проблема стану компонента, і вона заслуговує на справжню відповідь замість обхідного маневру
Обхідний шлях, що ходить по руках, - перелічити вікна верхнього рівня й надіслати WM_CLOSE усьому, чий заголовок схожий на переглядач PDF, - є хибним інстинктом. Він тягнеться через межі процесів, щоб закрити вікна, якими ваша програма не володіє, він вгадує переглядачі за текстом заголовка й може викинути незбережені анотації користувача, не спитавши. Ставтеся до всього цього підходу як до дурного запаху. Надійне виправлення полягає в тому, щоб ніколи не писати за шляхом, який може тримати інший процес. Серіалізуйте у тимчасовий файл у тій самій теці, а потім поставте його на місце атомарним перейменуванням, щойно EndDoc завершиться успішно. Якщо переглядач досі тримає старий файл відкритим, перейменування або пройде чисто, або впаде гучно, і ви покажете зрозуміле повідомлення замість того, щоб битися з блокуванням
uses
System.SysUtils, System.IOUtils;
procedure WritePdfAtomically(const FinalPath: string);
var
Pdf: THotPDF;
TempPath: string;
begin
// Тимчасовий файл у ТІЙ САМІЙ теці, що й ціль: перейменування всередині
// одного тому NTFS міняє ім'я атомарно, тоді як переміщення між томами
// вироджується в копіювання з видаленням і втрачає цю гарантію
TempPath := TPath.Combine(TPath.GetDirectoryName(FinalPath),
TGUID.NewGuid.ToString + '.pdf.tmp');
try
Pdf := THotPDF.Create(nil);
try
Pdf.FileName := TempPath;
Pdf.BeginDoc;
Pdf.CurrentPage.SetFont('Arial', [], 11);
Pdf.CurrentPage.TextOut(50, 760, 0, 'Invoice 2026-042');
Pdf.EndDoc; // тут тимчасовий файл на диску вже повний
finally
Pdf.Free;
end;
// Ставимо на місце. TFile.Move відмовляється перезаписувати, тож спершу
// приберіть застарілу ціль; якщо переглядач досі тримає старий файл,
// саме видалення впаде, гучно, ще до того, як зачепить добрі байти
if TFile.Exists(FinalPath) then
TFile.Delete(FinalPath);
TFile.Move(TempPath, FinalPath); // або: RenameFile(TempPath, FinalPath)
except
if TFile.Exists(TempPath) then
TFile.Delete(TempPath); // ніколи не лишайте недописаний тимчасовий файл
raise;
end;
end;
Дві чесні примітки до цього коду. TFile.Move і класичний RenameFile обидва відображаються на те саме перейменування Windows, яке є атомарним лише тоді, коли джерело й призначення лежать на одному томі, і саме тому тимчасовий файл потрапляє в теку призначення, а не в TPath.GetTempPath. А пара "видалити, потім перемістити" сама по собі не є одним атомарним кроком: існує коротке вікно, у якому не існує жодного з файлів. Для настільного застосунку, що перегенеровує звіт, це вікно не має значення; читачі, яким потрібен суворіший контракт у межах одного тому, можуть напряму викликати Win32 ReplaceFile або MoveFileEx з MOVEFILE_REPLACE_EXISTING, що згортає обмін в один виклик
Для високонавантаженого сервера, який постійно перегенеровує документи, чистішою дисципліною є запис кожного виходу під унікальним іменем (позначка часу або ідентифікатор завдання), щоб два прогони ніколи не змагалися за один шлях, а прибирання старих файлів лишити окремій політиці зберігання. Цей патерн коштує один рядок іменувальної дисципліни на запит
// Один вихідний шлях на запит: два паралельні завдання ніколи не змагаються
// за одне ім'я, тож ані танців із перейменуванням, ані блокувань не буде
OutName := Format('statement-%s-%s.pdf',
[CustomerId, TGUID.NewGuid.ToString.Trim(['{', '}'])]);
Pdf.FileName := TPath.Combine(OutputDir, OutName);
Ідентифікатор запиту чи завдання працює тут не гірше за GUID, коли навколишній фреймворк уже вам його видає, і завдяки цьому ім'я файлу безкоштовно простежується назад до рядка в журналі. Так чи інак принцип той самий: проєктуйте так, щоб файл, який ви пишете, належав лише вам тієї миті, коли ви його пишете. Блокування зникає не тому, що ви силою закрили вікно, а тому, що байтів більше ніхто не торкається
Форма виправлення
Зведіть обидві проблеми до коренів - і обидві виявляться про повагу до меж. Помилка автомата станів хоче, щоб ви шанували межу екземпляра: один THotPDF, один документ, а далі відпустіть його й зробіть інший. Помилка блокування файлу хоче, щоб ви шанували межу файлу: пишіть туди, де ніхто інший не читає, а потім перемістіть результат на місце. Жодна з них не потребує ані латання бібліотеки, ані скриптування робочого столу. Обидві розв'язуються самі, щойно ви трактуєте кожен документ як самодостатню одиницю роботи, створену наново, чисто записану й звільнену, а це той самий патерн, який робить передбачуваною і решту компонента
Показані тут виклики BeginDoc, EndDoc, LoadFromFile та SaveLoadedDocument є частиною HotPDF Delphi Component для Delphi та C++Builder