پیام خطا 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 میکوشد همان مسیر را بنویسد، سیستمعامل رد میکند چون نمایشگر یک اشتراک خواندن نگه داشته که نویسندهها را مسدود میکند، و شما با شکست دسترسی روبهرو میشوید. این یکی واقعاً یک مسئله قفل فایل ویندوز است نه مسئله حالت کامپوننت، و پاسخی واقعی میطلبد نه یک دور زدن
دور زدنی که دستبهدست میشود — شمارش پنجرههای سطح بالا و ارسال 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 کلاسیک هر دو به همان تغییر نام ویندوز نگاشت میشوند، که تنها وقتی اتمی است که مبدأ و مقصد روی یک ولوم بنشینند، و دقیقاً به همین دلیل است که فایل موقت بهجای TPath.GetTempPath در پوشه مقصد ساخته میشود. و جفت حذفسپسجابهجایی خودش یک گام اتمی نیست: پنجره کوتاهی هست که در آن هیچکدام از دو فایل وجود ندارند. برای یک برنامه دسکتاپ که گزارشی را دوباره تولید میکند آن پنجره بیاهمیت است؛ خوانندهای که روی همان ولوم قرارداد محکمتری میخواهد میتواند مستقیماً 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 برای دلفی و C++Builder هستند