Die Meldung lautet Please load the document before using BeginDoc, und sie taucht fast immer beim zweiten Anlauf auf. Das erste Dokument wird sauber geschrieben. Dann soll dieselbe THotPDF-Instanz ein zweites beginnen, BeginDoc löst aus, und die Meldung verweist auf das Laden eines Dokuments, also auf das Gegenteil dessen, was der Code versucht. Genau dieser Bruch zwischen Symptom und Meldung macht den Fall so hartnäckig. Das eigentliche Thema ist der Lebenszyklus der Komponente, und sobald der sitzt, hört der Fehler auf, rätselhaft zu sein

Eine THotPDF-Instanz ist ein Dokument, keine Dokumentfabrik
Das verlockende Denkmodell sagt, THotPDF sei ein Dienstobjekt, das man einmal hochfährt und dem man Dokumente zuführt, so wie man eine Datenbankverbindung offen hält und Abfrage um Abfrage darüber laufen lässt. Das ist es nicht. Eine Instanz bildet ein einzelnes, im Aufbau befindliches Dokument ab, und ihr innerer Zustandsautomat trägt die Annahme, dass er den Weg genau einmal geht: von leer über ein offenes Dokument bis zur gespeicherten Datei. BeginDoc öffnet diesen Weg und markiert die Instanz als eine mit laufendem Dokument. EndDoc serialisiert alles nach FileName und schließt ab. BeginDoc auf derselben fertigen Instanz erneut aufzurufen, verlangt von ihr, in einen Zustand zurückzukehren, den sie nie sauber verlassen hat, und die Wache, die anschlägt, ist jene, deren Meldung zufällig vom Laden spricht, weil intern die Bedingungen „bereit zum Beginnen“ und „hat ein geladenes Dokument“ gemeinsam geprüft werden
Die Meldung führt also in die Irre, doch die Wache tut ihre Arbeit. Sie weigert sich, ein frisches Dokument auf einer Komponente zu starten, die sich noch mitten im Dokument wähnt. Die Abhilfe besteht nicht darin, die Wache auszuhebeln. Sie besteht darin, eine verbrauchte Instanz nicht weiterzuverwenden
Der Lebenszyklus, in der Reihenfolge, die sein muss
Jedes Dokument, das HotPDF von Grund auf schreibt, folgt denselben vier Takten, und die Reihenfolge ist nicht verhandelbar. Create legt die Komponente an. BeginDoc öffnet das Dokument und legt die strukturellen Entscheidungen fest, sodass alles, was die ganze Datei betrifft (Seitengröße, Komprimierung, Verschlüsselung, Ausgabedateiname), zwischen Create und BeginDoc gesetzt werden muss. Dann zeichnen Sie. Dann schreibt EndDoc die Bytes auf die Platte. Free gibt die Instanz frei. Zeichenaufrufe vor BeginDoc haben keine Seite, auf der sie landen könnten; Eigenschaften für das ganze Dokument, die danach zugewiesen werden, werden ohne Klage ignoriert
var
Pdf: THotPDF;
begin
Pdf := THotPDF.Create(nil);
try
Pdf.FileName := 'invoice.pdf';
Pdf.BeginDoc; // öffnet das Dokument
Pdf.CurrentPage.SetFont('Arial', [], 11);
Pdf.CurrentPage.TextOut(50, 760, 0, 'Invoice 2026-042');
Pdf.EndDoc; // schreibt invoice.pdf und schließt ab
finally
Pdf.Free; // eine Instanz, ein Dokument
end;
end;
Lesen Sie das als Arbeitseinheit. Ein Create, ein BeginDoc, ein EndDoc, ein Free, eine Datei auf der Platte. In dem Moment, in dem Sie eine zweite Datei wollen, beginnen Sie eine neue Arbeitseinheit, und das heißt: eine neue Instanz
Was „Wiederverwendung“ heißen sollte: eine frische Instanz je Datei
Die kaputte Fassung will bei der Speicherbelegung sparsam sein: die Komponente einmal bauen, über einen Stapel schleifen, BeginDoc und EndDoc in der Schleife aufrufen. Der zweite Durchlauf fliegt Ihnen um die Ohren. Die funktionierende Fassung behandelt jede Ausgabe als eigenes, kurzlebiges Objekt, und die Kosten für das Anlegen einer Komponente sind neben dem Aufwand, ein PDF zu setzen und zu serialisieren, verschwindend, es gibt also nichts zu sparen, wenn man die Instanz hortet
procedure WriteBatch(const Names: TArray<string>);
var
I: Integer;
Pdf: THotPDF;
begin
for I := 0 to High(Names) do
begin
Pdf := THotPDF.Create(nil); // pro Durchlauf eine neue Instanz
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;
Das try/finally innerhalb der Schleife ist der Teil, den man im Review verteidigen sollte. Wenn BeginDoc oder irgendein Zeichenaufruf mitten in einem Dokument auslöst, wird die Instanz dieses Durchlaufs dennoch freigegeben, bevor der nächste beginnt, sodass ein schlechter Datensatz keine halb gebaute Komponente stranden lässt und den Rest des Laufs vergiftet. Ziehen Sie das Create zum „Optimieren“ vor die Schleife, sind Sie zurück beim ursprünglichen Fehler, nun im Gewand einer Stapelschleife
Eine vorhandene Datei zu ändern ist ein anderer Einstiegspunkt
Es gibt eine zweite Lesart von „Wiederverwendung“, die völlig legitim ist: Sie wollen kein leeres Dokument, sondern ein bereits vorhandenes PDF öffnen und ändern. Dieser Weg führt gar nicht über BeginDoc, und genau deshalb nennt die Fehlermeldung das Laden. Sie laden die Datei, bearbeiten sie und speichern sie unter dem Namen, den Sie wählen
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 gibt die Seitenzahl zurück, und ein Wert von null oder darunter bedeutet, dass das Laden fehlschlug, also lohnt sich die Prüfung, bevor Sie CurrentPage anfassen. Die Paarung zählt: Ein Dokument, das Sie mit LoadFromFile geöffnet haben, wird mit SaveLoadedDocument gespeichert, nicht mit dem Paar BeginDoc/EndDoc, das zu Dokumenten gehört, die Sie aus dem Nichts verfassen. Beides zu mischen ist der häufigste Weg, denselben Zustandsautomaten zu verwirren, der den ursprünglichen Fehler erzeugt hat. Halten Sie die beiden Abläufe gedanklich getrennt: BeginDoc … EndDoc erzeugt, LoadFromFile … SaveLoadedDocument bearbeitet
Das Problem mit der Dateisperre ist echt, und die Antwort ist nicht, Viewer-Fenster abzuschießen
Der Wiederverwendungsfehler reist oft mit einer zweiten Beschwerde, und die beiden verheddern sich, weil sie im selben Ablauf des Neuerzeugens auftauchen. Ein Anwender öffnet das gerade erzeugte PDF, lässt es in Acrobat oder Foxit offen und stößt dann einen Neuaufbau an. EndDoc versucht, denselben Pfad zu schreiben, das Betriebssystem verweigert das, weil der Viewer eine Lesefreigabe hält, die Schreiber blockiert, und Sie bekommen einen Zugriffsfehler. Das ist wirklich ein Problem der Dateisperren von Windows und kein Zustandsproblem der Komponente, und es verdient eine echte Antwort statt eines Behelfs
Der kursierende Behelf, die Fenster oberster Ebene aufzuzählen und allem, dessen Titel nach einem PDF-Viewer aussieht, ein WM_CLOSE zu schicken, ist der falsche Reflex. Er greift über Prozessgrenzen hinweg, um Fenster zu schließen, die Ihrem Programm nicht gehören, er rät den Viewer anhand des Titeltexts, und er kann ungespeicherte Anmerkungen eines Anwenders ungefragt wegwerfen. Behandeln Sie diesen ganzen Ansatz als Warnzeichen. Die verlässliche Abhilfe ist, nie auf einen Pfad zu schreiben, den ein anderer Prozess halten könnte. Serialisieren Sie in eine Temporärdatei im selben Verzeichnis und tauschen Sie diese nach erfolgreichem EndDoc mit einem atomaren Rename an ihren Platz. Hält ein Viewer die alte Datei noch offen, gelingt das Rename entweder sauber oder es scheitert hörbar, und Sie melden eine klare Nachricht, statt gegen die Sperre zu kämpfen
uses
System.SysUtils, System.IOUtils;
procedure WritePdfAtomically(const FinalPath: string);
var
Pdf: THotPDF;
TempPath: string;
begin
// Temporärdatei im SELBEN Verzeichnis wie das Ziel: Ein Rename innerhalb
// eines NTFS-Volumes tauscht den Namen atomar, während ein Verschieben
// über Volume-Grenzen zu Kopieren plus Löschen wird und die Garantie verliert
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; // die Temporärdatei liegt hier vollständig vor
finally
Pdf.Free;
end;
// An den Platz tauschen. TFile.Move überschreibt nicht, also zuerst ein
// veraltetes Ziel entfernen; hält ein Viewer die alte Datei noch, scheitert
// das Löschen, und zwar hörbar, bevor die guten Bytes angefasst werden
if TFile.Exists(FinalPath) then
TFile.Delete(FinalPath);
TFile.Move(TempPath, FinalPath); // oder: RenameFile(TempPath, FinalPath)
except
if TFile.Exists(TempPath) then
TFile.Delete(TempPath); // nie eine halb geschriebene Temporärdatei lassen
raise;
end;
end;
Zwei ehrliche Fußnoten zu diesem Code. TFile.Move und das klassische RenameFile bilden beide auf dasselbe Rename von Windows ab, das nur atomar ist, wenn Quelle und Ziel auf demselben Volume liegen, und genau darum landet die Temporärdatei im Zielverzeichnis und nicht in TPath.GetTempPath. Und das Paar aus Löschen und Verschieben ist selbst kein atomarer Schritt: Es gibt ein kurzes Fenster, in dem keine der beiden Dateien existiert. Für eine Desktop-Anwendung, die einen Bericht neu erzeugt, ist dieses Fenster ohne Belang; wer auf demselben Volume einen strengeren Vertrag braucht, kann direkt das Win32-ReplaceFile oder MoveFileEx mit MOVEFILE_REPLACE_EXISTING aufrufen, was den Tausch auf einen einzigen Aufruf zusammenzieht
Für einen Server mit hohem Durchsatz, der ständig Dokumente neu erzeugt, ist die sauberere Disziplin, jede Ausgabe unter einem eindeutigen Namen zu schreiben (ein Zeitstempel oder eine Job-ID), sodass zwei Läufe nie um einen Pfad streiten, und eine getrennte Aufbewahrungsregel alte Dateien aufräumen zu lassen. Das Muster ist eine Zeile Namensdisziplin je Anfrage
// Ein Ausgabepfad je Anfrage: Zwei gleichzeitige Jobs können nie um denselben
// Namen streiten, also kein Rename-Tanz und keine Sperre, die verloren geht
OutName := Format('statement-%s-%s.pdf',
[CustomerId, TGUID.NewGuid.ToString.Trim(['{', '}'])]);
Pdf.FileName := TPath.Combine(OutputDir, OutName);
Eine Request-ID oder Job-ID taugt genauso gut wie die GUID, wenn das umgebende Framework Ihnen schon eine reicht, und sie macht den Dateinamen ganz nebenbei bis zur Logzeile zurückverfolgbar. So oder so gilt dasselbe Prinzip: Entwerfen Sie so, dass die Datei, die Sie schreiben, in dem Moment allein Ihnen gehört. Die Sperre verschwindet nicht, weil Sie ein Fenster zugezwungen haben, sondern weil niemand sonst die Bytes anfasst
Die Form der Abhilfe
Führt man beide Probleme auf ihre Wurzel zurück, geht es in beiden Fällen um respektierte Grenzen. Der Fehler des Zustandsautomaten verlangt, die Grenze der Instanz zu achten: eine THotPDF, ein Dokument, dann loslassen und eine neue bauen. Der Fehler mit der Dateisperre verlangt, die Grenze der Datei zu achten: dorthin schreiben, wo niemand liest, und das Ergebnis danach an seinen Platz schieben. Keiner von beiden ruft nach einem Patch der Bibliothek oder nach Skripten auf dem Desktop. Beide lösen sich auf, wenn man jedes Dokument als in sich geschlossene Arbeitseinheit behandelt, frisch angelegt, sauber geschrieben und freigegeben, und genau dieses Muster macht auch den Rest der Komponente vorhersehbar
Die hier gezeigten Aufrufe BeginDoc, EndDoc, LoadFromFile und SaveLoadedDocument gehören zur HotPDF Delphi Component für Delphi und C++Builder