L'errore recita Please load the document before using BeginDoc, e si presenta quasi sempre al secondo giro. Il primo documento viene scritto senza problemi. Poi alla stessa istanza di THotPDF viene chiesto di iniziarne un secondo, BeginDoc solleva l'eccezione e il messaggio parla di caricare un documento, cioè l'opposto di quello che il codice sta tentando di fare. È proprio lo scarto fra il sintomo e il messaggio a rendere memorabile questo caso. Il vero argomento è il ciclo di vita del componente, e una volta afferrato quello l'errore smette di essere misterioso

Una istanza THotPDF è un documento, non una fabbrica di documenti
Il modello mentale allettante è che THotPDF sia un oggetto di servizio da istanziare una volta sola e a cui dare in pasto documenti, come si terrebbe aperta una connessione al database eseguendovi query su query. Non è così. Una istanza modella un singolo documento in costruzione, e la sua macchina a stati interna porta con sé il presupposto di percorrere il tragitto una volta sola: da vuoto, attraverso un documento aperto, fino a un file salvato. BeginDoc apre quel tragitto e segna l'istanza come titolare di un documento in lavorazione. EndDoc serializza tutto su FileName e chiude la partita. Chiamare di nuovo BeginDoc sulla stessa istanza esaurita le chiede di rientrare in uno stato da cui non è mai uscita in modo pulito, e la guardia che scatta è quella il cui messaggio parla di caricamento, perché internamente la condizione «pronto a iniziare» e quella «ha un documento caricato» vengono verificate insieme
Il messaggio è quindi fuorviante, ma la guardia sta facendo il suo mestiere. Si rifiuta di lasciarvi aprire un documento nuovo sopra un componente che si crede ancora a metà documento. La soluzione non è aggirare la guardia. È smettere di riutilizzare una istanza già consumata
Il ciclo di vita, nell'ordine in cui deve avvenire
Ogni documento che HotPDF scrive da zero segue gli stessi quattro tempi, e l'ordine non è negoziabile. Create alloca il componente. BeginDoc apre il documento e fissa le scelte strutturali, perciò tutto ciò che riguarda il file intero (dimensione della pagina, compressione, cifratura, nome del file di uscita) va impostato fra Create e BeginDoc. Poi disegnate. Poi EndDoc scrive i byte su disco. Free rilascia l'istanza. Le chiamate di disegno collocate prima di BeginDoc non hanno una pagina su cui atterrare; le proprietà valide per l'intero documento assegnate dopo vengono ignorate senza un fiato
var
Pdf: THotPDF;
begin
Pdf := THotPDF.Create(nil);
try
Pdf.FileName := 'invoice.pdf';
Pdf.BeginDoc; // apre il documento
Pdf.CurrentPage.SetFont('Arial', [], 11);
Pdf.CurrentPage.TextOut(50, 760, 0, 'Invoice 2026-042');
Pdf.EndDoc; // scrive invoice.pdf e lo chiude
finally
Pdf.Free; // una istanza, un documento
end;
end;
Leggetelo come unità di lavoro. Un Create, un BeginDoc, un EndDoc, un Free, un file su disco. Nell'istante in cui volete un secondo file, state avviando una nuova unità di lavoro, il che significa una nuova istanza
Che cosa dovrebbe significare «riutilizzo»: una istanza nuova per ogni file
La versione che si rompe cerca di risparmiare sull'allocazione: costruisce il componente una volta, cicla su un lotto, chiama BeginDoc ed EndDoc dentro il ciclo. La seconda iterazione solleva l'eccezione. La versione che funziona tratta ogni uscita come un oggetto di breve vita a sé stante, e il costo di allocazione di un componente è irrisorio rispetto al lavoro di impaginare e serializzare un PDF, quindi non c'è nulla da guadagnare tenendosi stretta l'istanza
procedure WriteBatch(const Names: TArray<string>);
var
I: Integer;
Pdf: THotPDF;
begin
for I := 0 to High(Names) do
begin
Pdf := THotPDF.Create(nil); // nuova istanza a ogni passaggio
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;
Il try/finally che sta dentro il ciclo è la parte da difendere in sede di revisione. Se BeginDoc o una qualsiasi chiamata di disegno solleva una eccezione a metà di un documento, l'istanza di quella iterazione viene comunque liberata prima che inizi la successiva, così un record difettoso non lascia in giro un componente costruito a metà che avvelena il resto dell'esecuzione. Tirate fuori il Create sopra il ciclo per «ottimizzare» e siete di nuovo al bug originale, stavolta in veste di ciclo su lotto
Modificare un file esistente è un punto di ingresso diverso
C'è una seconda lettura di «riutilizzo» del tutto legittima: non volete un documento vuoto, volete aprire un PDF che esiste già e cambiarlo. Quel percorso non passa affatto per BeginDoc, ed è esattamente per questo che il messaggio di errore nomina il caricamento. Caricate il file, lo modificate e lo salvate con il nome che preferite
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 restituisce il numero di pagine, e un valore pari a zero o inferiore significa che il caricamento è fallito, quindi conviene controllarlo prima di toccare CurrentPage. L'abbinamento conta: un documento aperto con LoadFromFile si salva con SaveLoadedDocument, non con la coppia BeginDoc/EndDoc, che appartiene ai documenti scritti dal nulla. Mescolare i due è il modo più comune di confondere la stessa macchina a stati che ha prodotto l'errore iniziale. Tenete i due flussi separati anche mentalmente: BeginDoc ... EndDoc crea, LoadFromFile ... SaveLoadedDocument modifica
Il problema del lock sul file è reale, e la risposta non è chiudere a forza le finestre dei visualizzatori
L'errore di riutilizzo viaggia spesso in compagnia di una seconda lamentela, e le due si aggrovigliano perché affiorano nello stesso flusso di rigenerazione del file. Un utente apre il PDF appena prodotto, lo lascia aperto in Acrobat o Foxit, poi fa scattare una ricostruzione. EndDoc tenta di scrivere sullo stesso percorso, il sistema operativo rifiuta perché il visualizzatore detiene una condivisione in lettura che blocca gli scrittori, e ottenete un fallimento per accesso negato. Questo è genuinamente un problema di file locking di Windows più che di stato del componente, e merita una risposta vera invece di un espediente
L'espediente che circola, enumerare le finestre di primo livello e inviare WM_CLOSE a qualunque titolo somigli a un visualizzatore PDF, è l'istinto sbagliato. Attraversa i confini di processo per chiudere finestre che il vostro programma non possiede, indovina i visualizzatori dal testo del titolo e può buttare via le annotazioni non salvate di un utente senza chiedere. Considerate tutto quell'approccio un cattivo odore. La correzione affidabile è non scrivere mai su un percorso che un altro processo potrebbe tenere occupato. Serializzate su un file temporaneo nella stessa directory, poi mettetelo al suo posto con una rinomina atomica appena EndDoc è andato a buon fine. Se un visualizzatore tiene ancora aperto il vecchio file, la rinomina o riesce in modo pulito o fallisce rumorosamente, e voi mostrate un messaggio chiaro invece di combattere contro il lock
uses
System.SysUtils, System.IOUtils;
procedure WritePdfAtomically(const FinalPath: string);
var
Pdf: THotPDF;
TempPath: string;
begin
// File temporaneo nella STESSA directory del bersaglio: una rinomina
// dentro un solo volume NTFS scambia il nome in modo atomico, mentre uno
// spostamento fra volumi degrada a copia-più-cancella e perde la garanzia
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; // qui il temporaneo è completo su disco
finally
Pdf.Free;
end;
// Scambio in posizione. TFile.Move rifiuta di sovrascrivere: eliminate
// prima un bersaglio obsoleto; se un visualizzatore tiene ancora il
// vecchio file, fallisce la cancellazione, rumorosamente, prima dei byte buoni
if TFile.Exists(FinalPath) then
TFile.Delete(FinalPath);
TFile.Move(TempPath, FinalPath); // oppure: RenameFile(TempPath, FinalPath)
except
if TFile.Exists(TempPath) then
TFile.Delete(TempPath); // mai lasciare un temporaneo a metà
raise;
end;
end;
Due note oneste su quel codice. TFile.Move e il classico RenameFile mappano entrambi sulla stessa rinomina di Windows, atomica solo quando origine e destinazione stanno sullo stesso volume, ed è proprio per questo che il file temporaneo finisce nella directory di destinazione anziché in TPath.GetTempPath. E la coppia cancella-poi-sposta non è a sua volta un unico passo atomico: c'è una breve finestra in cui nessuno dei due file esiste. Per una applicazione desktop che rigenera un rapporto quella finestra è irrilevante; chi ha bisogno di una garanzia più forte sullo stesso volume può chiamare direttamente la Win32 ReplaceFile oppure MoveFileEx con MOVEFILE_REPLACE_EXISTING, che condensa lo scambio in una sola chiamata
Per un server ad alto volume che rigenera documenti di continuo, la disciplina più pulita è scrivere ogni uscita sotto un nome univoco (una marca temporale o un identificativo di job) così che due esecuzioni non contendano mai lo stesso percorso, lasciando a una politica di conservazione separata il compito di ripulire i vecchi file. Lo schema costa una riga di disciplina sui nomi per ogni richiesta
// Un percorso di uscita per richiesta: due job concorrenti non possono mai
// contendersi lo stesso nome, niente balletto di rinomine e nessun lock perso
OutName := Format('statement-%s-%s.pdf',
[CustomerId, TGUID.NewGuid.ToString.Trim(['{', '}'])]);
Pdf.FileName := TPath.Combine(OutputDir, OutName);
Un identificativo di richiesta o di job funziona bene quanto il GUID quando il framework circostante ve ne consegna già uno, e rende il nome del file tracciabile fino a una riga di log senza costi aggiuntivi. In un modo o nell'altro il principio è lo stesso: progettate in modo che il file che state scrivendo sia soltanto vostro nel momento in cui lo scrivete. Il lock sparisce non perché avete forzato la chiusura di una finestra ma perché nessun altro sta toccando quei byte
La forma della soluzione
Riducete i due problemi alle loro radici e scoprirete che entrambi parlano di rispettare confini. L'errore della macchina a stati vi chiede di onorare il confine dell'istanza: un THotPDF, un documento, poi lasciatelo andare e createne un altro. L'errore di lock vi chiede di onorare il confine del file: scrivete dove nessun altro sta leggendo, poi spostate il risultato al suo posto. Nessuno dei due richiede di modificare la libreria o di pilotare il desktop. Entrambi discendono dal trattare ogni documento come una unità di lavoro autonoma, creata da zero, scritta in modo pulito e rilasciata, che è lo stesso schema che rende prevedibile il resto del componente
Le chiamate BeginDoc, EndDoc, LoadFromFile e SaveLoadedDocument mostrate qui fanno parte del HotPDF Delphi Component per Delphi e C++Builder