Die Fehlermeldung lautet Please load the document before using BeginDoc, und sie erscheint fast immer beim zweiten Durchlauf. Das erste Dokument wird einwandfrei geschrieben. Dann soll dieselbe THotPDF-Instanz ein zweites starten, BeginDoc löst eine Ausnahme aus, und die Meldung verweist auf das Laden eines Dokuments, was das Gegenteil dessen ist, was der Code versucht. Die Diskrepanz zwischen Symptom und Meldung ist das, was diesen Fehler so hartnäckig macht. Das eigentliche Thema ist der Komponentenlebenszyklus, und sobald der verstanden ist, verliert der Fehler seinen mysteriösen Charakter.

Eine THotPDF-Instanz ist ein Dokument, keine Dokumentfabrik
Das verlockende gedankliche Modell ist, dass THotPDF ein Dienstobjekt ist, das man einmal hochfährt und dem man dann Dokument für Dokument übergibt, ähnlich wie man eine Datenbankverbindung offen hält und beliebig viele Abfragen darüber schickt. Das ist es nicht. Eine Instanz modelliert ein einziges Dokument im Aufbau, und ihre interne Zustandsmaschine geht davon aus, dass sie den Pfad einmal durchläuft: von leer über ein offenes Dokument bis zur gespeicherten Datei. BeginDoc öffnet diesen Pfad und markiert die Instanz als laufendes Dokument. EndDoc serialisiert alles nach FileName und schließt es ab. BeginDoc erneut auf derselben fertiggestellten Instanz aufzurufen, verlangt von ihr, in einen Zustand einzutreten, den sie nie sauber verlassen hat, und die Prüfung, die dann ausgelöst wird, nennt in ihrer Meldung das Laden, weil intern die Bedingungen „bereit zum Starten" und „hat ein geladenes Dokument" zusammen geprüft werden.
Die Meldung ist also irreführend, aber die Prüfung tut ihre Arbeit. Sie weigert sich, ein neues Dokument auf einer Komponente zu starten, die noch immer glaubt, mitten in einem Dokument zu sein. Die Lösung besteht nicht darin, die Prüfung zu umgehen. Sie besteht darin, eine verbrauchte Instanz nicht wiederzuverwenden.
Der Lebenszyklus, in der Reihenfolge, in der er ablaufen muss
Jedes Dokument, das HotPDF von Grund auf schreibt, folgt denselben vier Schritten, und die Reihenfolge ist nicht verhandelbar. Create allokiert die Komponente. BeginDoc öffnet das Dokument und legt die strukturellen Entscheidungen fest; alles, was die gesamte Datei betrifft (Seitengröße, Kompression, Verschlüsselung, Ausgabedateiname), muss zwischen Create und BeginDoc gesetzt werden. Dann wird gezeichnet. Dann schreibt EndDoc die Bytes auf die Festplatte. Free gibt die Instanz frei. Zeichenaufrufe vor BeginDoc haben keine Seite, auf der sie landen können; ganzseitige Eigenschaften, die danach zugewiesen werden, werden ohne Rückmeldung ignoriert.
var
Pdf: THotPDF;
begin
Pdf := THotPDF.Create(nil);
try
Pdf.FileName := 'invoice.pdf';
Pdf.BeginDoc; // opens the document
Pdf.CurrentPage.SetFont('Arial', [], 11);
Pdf.CurrentPage.TextOut(50, 760, 0, 'Invoice 2026-042');
Pdf.EndDoc; // writes invoice.pdf, closes it out
finally
Pdf.Free; // one instance, one document
end;
end;
Das ist die Arbeitseinheit. Ein Create, ein BeginDoc, ein EndDoc, ein Free, eine Datei auf der Festplatte. Sobald eine zweite Datei gewünscht wird, beginnt eine neue Arbeitseinheit, was eine neue Instanz bedeutet.
Was „Wiederverwendung" bedeuten sollte: eine neue Instanz pro Datei
Die fehlerhafte Version versucht sparsam mit der Allokation umzugehen: die Komponente einmal erstellen, in einer Schleife über einen Stapel iterieren, BeginDoc und EndDoc innerhalb der Schleife aufrufen. Die zweite Iteration schlägt fehl. Die korrekte Version behandelt jede Ausgabe als kurzlebiges eigenes Objekt. Die Allokationskosten für das Erstellen einer Komponente sind verglichen mit dem Aufwand für Layout und Serialisierung eines PDFs vernachlässigbar, sodass nichts durch das Festhalten an der Instanz gewonnen wird.
procedure WriteBatch(const Names: TArray<string>);
var
I: Integer;
Pdf: THotPDF;
begin
for I := 0 to High(Names) do
begin
Pdf := THotPDF.Create(nil); // new instance each pass
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 das Konstrukt, das bei Code-Reviews verteidigt werden sollte. Wenn BeginDoc oder ein Zeichenaufruf mitten in einem Dokument eine Ausnahme auslöst, wird die Instanz dieser Iteration noch vor Beginn der nächsten freigegeben, sodass ein fehlerhafter Datensatz keine halb aufgebaute Komponente hinterlässt, die den Rest des Durchlaufs vergiftet. Das Create außerhalb der Schleife zu ziehen, um zu „optimieren", führt zurück zum ursprünglichen Fehler, diesmal mit einer Stapelschleife als Verpackung.
Eine bestehende Datei ändern ist ein anderer Einstiegspunkt
Es gibt eine zweite Lesart von „Wiederverwenden", die vollkommen legitim ist: Es soll kein leeres Dokument erstellt, sondern ein vorhandenes PDF geöffnet und verändert werden. Dieser Weg führt überhaupt nicht über BeginDoc, was genau der Grund ist, warum die Fehlermeldung das Laden erwähnt. Die Datei wird geladen, bearbeitet und unter einem beliebigen Namen gespeichert.
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 Seitenanzahl zurück; ein Wert von null oder kleiner bedeutet, dass das Laden fehlgeschlagen ist, was vor dem Zugriff auf CurrentPage geprüft werden sollte. Die Paarung ist wichtig: Ein Dokument, das mit LoadFromFile geöffnet wurde, wird mit SaveLoadedDocument gespeichert, nicht mit dem BeginDoc/EndDoc-Paar, das für von Grund auf erstellte Dokumente reserviert ist. Die beiden zu mischen, ist der häufigste Weg, dieselbe Zustandsmaschine zu verwirren, die den ursprünglichen Fehler verursacht hat. Beide Abläufe sollten gedanklich getrennt gehalten werden: BeginDoc ... EndDoc erstellt, LoadFromFile ... SaveLoadedDocument bearbeitet.
Das Dateisperrproblem ist real, und die Antwort ist nicht das Schließen von Betrachter-Fenstern
Der Wiederverwendungsfehler tritt oft zusammen mit einer zweiten Beschwerde auf, und beide verflechten sich, weil sie im selben Workflow zum Regenerieren von Dateien auftreten. Ein Benutzer öffnet das soeben erstellte PDF, lässt es in Acrobat oder Foxit geöffnet und löst dann eine Neuerstellung aus. EndDoc versucht, denselben Pfad zu schreiben, das Betriebssystem verweigert dies, weil der Betrachter einen Lesezugriff hält, der Schreibzugriffe blockiert, und ein Zugriffsverweigerungsfehler erscheint. Dies ist tatsächlich ein Windows-Dateisperrproblem und kein Problem mit dem Komponentenstatus, und es verdient eine echte Lösung statt eines Workarounds.
Der kursierende Workaround, Fenster der obersten Ebene aufzuzählen und WM_CLOSE an alles zu senden, dessen Titel nach einem PDF-Betrachter aussieht, ist der falsche Ansatz. Er greift über Prozessgrenzen hinweg, um Fenster zu schließen, die das Programm nicht besitzt, errät Betrachter anhand von Titeltext und kann ungespeicherte Anmerkungen eines Benutzers ohne Rückfrage verwerfen. Dieser gesamte Ansatz sollte als Warnsignal behandelt werden. Die zuverlässige Lösung besteht darin, niemals in einen Pfad zu schreiben, den ein anderer Prozess möglicherweise hält. Zuerst in eine temporäre Datei im selben Verzeichnis serialisieren, dann nach erfolgreichem EndDoc per atomarem Umbenennen an den Zielort verschieben. Hält ein Betrachter noch die alte Datei geöffnet, gelingt das Umbenennen entweder sauber oder schlägt laut fehl, und es wird eine klare Meldung angezeigt, statt gegen die Sperre anzukämpfen.
Bei einem Server mit hohem Volumen, der Dokumente ständig neu generiert, ist die sauberere Disziplin, jede Ausgabe unter einem eindeutigen Namen zu schreiben (Zeitstempel oder Job-ID), sodass zwei Durchläufe nie um denselben Pfad konkurrieren, und eine separate Aufbewahrungsrichtlinie übernimmt die Bereinigung alter Dateien. In jedem Fall gilt dasselbe Prinzip: Das Design so gestalten, dass die zu schreibende Datei im Schreibmoment ausschließlich dem eigenen Prozess gehört. Die Sperre verschwindet nicht, weil ein Fenster zwangsweise geschlossen wurde, sondern weil nichts anderes die Bytes berührt.
Die Gestalt der Lösung
Werden beide Probleme auf ihre Wurzeln zurückgeführt, geht es in beiden Fällen darum, Grenzen zu respektieren. Der Zustandsmaschinenfehler verlangt, die Instanzgrenze zu respektieren: ein THotPDF, ein Dokument, dann loslassen und eine neue erstellen. Der Dateisperrfehler verlangt, die Dateigrenze zu respektieren: schreiben, wo nichts anderes liest, dann das Ergebnis an den richtigen Ort verschieben. Keines von beiden erfordert einen Patch an der Bibliothek oder Skripte auf dem Desktop. Beides ergibt sich daraus, jedes Dokument als in sich geschlossene Arbeitseinheit zu behandeln, frisch erstellt, sauber geschrieben und freigegeben, was dasselbe Muster ist, das den Rest der Komponente vorhersehbar macht.
Die hier gezeigten Aufrufe BeginDoc, EndDoc, LoadFromFile und SaveLoadedDocument sind Teil der HotPDF Component für Delphi und C++Builder.