Eroarea sună Please load the document before using BeginDoc și apare aproape întotdeauna a doua oară. Primul document se scrie fără probleme. Apoi aceleiași instanțe THotPDF i se cere să înceapă un al doilea, BeginDoc ridică excepția, iar mesajul arată spre încărcarea unui document, adică exact opusul a ceea ce încearcă să facă acel cod. Nepotrivirea dintre simptom și mesaj este cea care face ca această problemă să persiste. Subiectul real este ciclul de viață al componentei, iar odată ce acesta se așază, eroarea încetează să mai fie misterioasă

O instanță THotPDF este un document, nu o fabrică de documente
Modelul mental tentant este că THotPDF ar fi un obiect de serviciu pe care îl porniți o dată și căruia îi dați documente, așa cum ați ține deschisă o conexiune la baza de date și ați rula prin ea interogare după interogare. Nu este așa ceva. O instanță modelează un singur document aflat în construcție, iar mașina ei internă de stări poartă presupunerea că parcurge drumul o singură dată: de la gol, printr-un document deschis, până la un fișier salvat. BeginDoc deschide acel drum și marchează instanța ca având un document în lucru. EndDoc serializează totul în FileName și îl închide. Apelarea din nou a lui BeginDoc pe aceeași instanță deja încheiată îi cere să reintre într-o stare din care nu a ieșit niciodată curat, iar garda care se declanșează este cea al cărei mesaj se întâmplă să pomenească încărcarea, pentru că, pe plan intern, condițiile „gata de început” și „are un document încărcat” sunt verificate împreună
Așadar mesajul induce în eroare, dar garda își face treaba. Ea refuză să vă lase să porniți un document nou peste o componentă care încă se crede în mijlocul unui document. Corectura nu este să învingeți garda, ci să nu mai reutilizați o instanță consumată
Ciclul de viață, în ordinea în care trebuie să se petreacă
Fiecare document pe care HotPDF îl scrie de la zero urmează aceleași patru bătăi, iar ordinea nu este negociabilă. Create alocă componenta. BeginDoc deschide documentul și fixează alegerile structurale, așa că orice afectează întregul fișier (dimensiunea paginii, compresia, criptarea, numele fișierului de ieșire) trebuie setat între Create și BeginDoc. Apoi desenați. Apoi EndDoc scrie octeții pe disc. Free eliberează instanța. Apelurile de desenare puse înainte de BeginDoc nu au nicio pagină pe care să aterizeze; proprietățile la nivel de document atribuite după el sunt ignorate fără nicio plângere
var
Pdf: THotPDF;
begin
Pdf := THotPDF.Create(nil);
try
Pdf.FileName := 'invoice.pdf';
Pdf.BeginDoc; // deschide documentul
Pdf.CurrentPage.SetFont('Arial', [], 11);
Pdf.CurrentPage.TextOut(50, 760, 0, 'Invoice 2026-042');
Pdf.EndDoc; // scrie invoice.pdf și îl închide
finally
Pdf.Free; // o instanță, un document
end;
end;
Citiți asta ca pe unitatea de lucru. Un Create, un BeginDoc, un EndDoc, un Free, un fișier pe disc. În clipa în care vreți un al doilea fișier, începeți o nouă unitate de lucru, ceea ce înseamnă o instanță nouă
Ce ar trebui să însemne „reutilizare”: o instanță nouă pentru fiecare fișier
Varianta care se strică încearcă să fie economă cu alocarea: construiește componenta o dată, parcurge un lot în buclă și apelează BeginDoc și EndDoc în interiorul buclei. A doua iterație aruncă excepția. Varianta care funcționează tratează fiecare ieșire ca pe un obiect propriu, de scurtă durată, iar costul de alocare al creării unei componente este neglijabil pe lângă munca de aranjare și serializare a unui PDF, deci nu se economisește nimic prin păstrarea instanței
procedure WriteBatch(const Names: TArray<string>);
var
I: Integer;
Pdf: THotPDF;
begin
for I := 0 to High(Names) do
begin
Pdf := THotPDF.Create(nil); // instanță nouă la fiecare trecere
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;
Blocul try/finally așezat în interiorul buclei este partea care merită apărată la o revizuire. Dacă BeginDoc sau vreun apel de desenare ridică o excepție la jumătatea unui document, instanța acelei iterații este totuși eliberată înainte să înceapă următoarea, deci o înregistrare defectă nu lasă în urmă o componentă construită pe jumătate și nu otrăvește restul rulării. Scoateți Create deasupra buclei ca să „optimizați” și v-ați întors la defectul original, acum îmbrăcat într-o buclă de lot
Modificarea unui fișier existent este un alt punct de intrare
Există o a doua citire a „reutilizării”, perfect legitimă: nu vreți un document gol, ci vreți să deschideți un PDF care există deja și să îl schimbați. Acea cale nu trece deloc prin BeginDoc, ceea ce explică exact de ce mesajul de eroare pomenește încărcarea. Încărcați fișierul, îl editați și îl salvați sub ce nume alegeți
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 returnează numărul de pagini, iar o valoare de zero sau mai mică înseamnă că încărcarea a eșuat, așa că merită verificată înainte să atingeți CurrentPage. Perechea contează: un document pe care l-ați deschis cu LoadFromFile se salvează cu SaveLoadedDocument, nu cu perechea BeginDoc/EndDoc, care aparține documentelor create de la zero. Amestecarea celor două este cel mai frecvent mod de a deruta chiar mașina de stări care a produs eroarea inițială. Țineți cele două fluxuri separate în minte: BeginDoc ... EndDoc creează, LoadFromFile ... SaveLoadedDocument editează
Problema blocării fișierului este reală, iar soluția nu este uciderea ferestrelor de vizualizare
Eroarea de reutilizare călătorește adesea cu o a doua plângere, iar cele două se încurcă între ele pentru că apar în același flux de regenerare a fișierului. Un utilizator deschide PDF-ul tocmai produs, îl lasă deschis în Acrobat sau Foxit, apoi declanșează o reconstruire. EndDoc încearcă să scrie aceeași cale, sistemul de operare refuză pentru că vizualizatorul ține o partajare de citire care blochează scriitorii, iar dumneavoastră primiți un eșec de tip acces refuzat. Acesta este cu adevărat o problemă de blocare a fișierelor în Windows, nu una de stare a componentei, și merită un răspuns real în locul unei improvizații
Improvizația care circulă, enumerarea ferestrelor de nivel superior și trimiterea unui WM_CLOSE spre orice fereastră al cărei titlu seamănă cu al unui vizualizator PDF, este instinctul greșit. Ea trece granițele dintre procese ca să închidă ferestre pe care programul dumneavoastră nu le deține, ghicește vizualizatoarele după textul din titlu și poate arunca fără să întrebe adnotările nesalvate ale unui utilizator. Tratați întreaga abordare ca pe un semn rău. Corectura de încredere este să nu scrieți niciodată într-o cale pe care alt proces ar putea să o țină. Serializați într-un fișier temporar din același director, apoi puneți-l la locul lui printr-o redenumire atomică, odată ce EndDoc a reușit. Dacă un vizualizator încă are vechiul fișier deschis, redenumirea fie reușește curat, fie eșuează zgomotos, iar dumneavoastră scoateți la suprafață un mesaj clar în loc să vă luptați cu blocarea
uses
System.SysUtils, System.IOUtils;
procedure WritePdfAtomically(const FinalPath: string);
var
Pdf: THotPDF;
TempPath: string;
begin
// Fișier temporar în ACELAȘI director cu ținta: o redenumire în interiorul
// unui volum NTFS schimbă numele atomic, în timp ce o mutare între volume
// degenerează în copiere plus ștergere și pierde această garanție
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; // aici fișierul temporar este complet pe disc
finally
Pdf.Free;
end;
// Punere la loc. TFile.Move refuză să suprascrie, deci curățați întâi o
// țintă veche; dacă un vizualizator încă ține vechiul fișier, ștergerea
// este cea care eșuează, zgomotos, înainte de a atinge octeții buni
if TFile.Exists(FinalPath) then
TFile.Delete(FinalPath);
TFile.Move(TempPath, FinalPath); // sau: RenameFile(TempPath, FinalPath)
except
if TFile.Exists(TempPath) then
TFile.Delete(TempPath); // nu lăsați niciodată în urmă un temporar pe jumătate scris
raise;
end;
end;
Două note cinstite la acel cod. TFile.Move și clasicul RenameFile se mapează amândouă pe aceeași redenumire Windows, care este atomică doar când sursa și destinația stau pe același volum, și tocmai de aceea fișierul temporar ajunge în directorul de destinație, nu în TPath.GetTempPath. Iar perechea ștergere-apoi-mutare nu este ea însăși un singur pas atomic: există o scurtă fereastră în care niciun fișier nu există. Pentru o aplicație desktop care regenerează un raport, acea fereastră este irelevantă; cititorii care au nevoie de un contract mai puternic pe același volum pot apela direct funcțiile Win32 ReplaceFile sau MoveFileEx cu MOVEFILE_REPLACE_EXISTING, ceea ce comprimă schimbul într-un singur apel
Pentru un server cu volum mare, care regenerează documente în permanență, disciplina mai curată este să scrieți fiecare ieșire sub un nume unic (un marcaj de timp sau un id de sarcină), astfel încât două rulări să nu se dispute niciodată o cale, și să lăsați o politică de retenție separată să curețe fișierele vechi. Tiparul este o singură linie de disciplină a denumirii pentru fiecare cerere
// O singură cale de ieșire pentru fiecare cerere: două sarcini concurente nu
// se pot disputa niciodată același nume, deci fără dans de redenumire și fără blocare pierdută
OutName := Format('statement-%s-%s.pdf',
[CustomerId, TGUID.NewGuid.ToString.Trim(['{', '}'])]);
Pdf.FileName := TPath.Combine(OutputDir, OutName);
Un id de cerere sau un id de sarcină funcționează la fel de bine ca GUID-ul atunci când cadrul din jur vă dă deja unul, iar numele fișierului devine astfel, pe gratis, urmăribil până la o linie de jurnal. Oricum ar fi, principiul este același: proiectați astfel încât fișierul pe care îl scrieți să fie numai al dumneavoastră în momentul în care îl scrieți. Blocarea dispare nu pentru că ați forțat închiderea unei ferestre, ci pentru că nimic altceva nu atinge octeții
Forma corecturii
Reduceți cele două probleme la rădăcinile lor și amândouă țin de respectarea granițelor. Eroarea mașinii de stări vă cere să respectați granița instanței: un THotPDF, un document, apoi lăsați-l să plece și faceți altul. Eroarea de blocare a fișierului vă cere să respectați granița fișierului: scrieți acolo unde nimic altceva nu citește, apoi mutați rezultatul la locul lui. Niciuna nu cere peticirea bibliotecii sau scriptarea desktopului. Amândouă decurg din tratarea fiecărui document ca pe o unitate de lucru de sine stătătoare, creată de la zero, scrisă curat și eliberată, adică același tipar care face previzibil restul componentei
Apelurile BeginDoc, EndDoc, LoadFromFile și SaveLoadedDocument prezentate aici fac parte din HotPDF Delphi Component pentru Delphi și C++Builder