Feilmeldingen lyder Please load the document before using BeginDoc, og den dukker nesten alltid opp andre gang. Det første dokumentet skrives helt fint. Så blir den samme THotPDF-instansen bedt om å starte et nummer to, BeginDoc kaster et unntak, og meldingen peker på å laste et dokument, som er det motsatte av det koden prøver å gjøre. Misforholdet mellom symptomet og meldingen er det som gjør at denne setter seg fast. Det egentlige temaet er komponentens livssyklus, og når den brikken faller på plass, slutter feilen å være mystisk

En THotPDF-instans er ett dokument, ikke en dokumentfabrikk
Den fristende mentale modellen er at THotPDF er et tjenesteobjekt du starter opp én gang og mater dokumenter inn i, slik du kanskje holder en databaseforbindelse åpen og kjører spørring etter spørring gjennom den. Slik er det ikke. En instans modellerer ett enkelt dokument under bygging, og den interne tilstandsmaskinen bærer forutsetningen om at den går veien én gang: fra tom, gjennom et åpent dokument, til en lagret fil. BeginDoc åpner den veien og markerer instansen som et dokument under arbeid. EndDoc serialiserer alt til FileName og avslutter det. Å kalle BeginDoc igjen på den samme ferdige instansen ber den om å gå inn i en tilstand den aldri forlot rent, og vakten som slår til, er den hvis melding tilfeldigvis nevner lasting, fordi betingelsene "klar til å begynne" og "har et lastet dokument" internt sjekkes sammen
Meldingen er altså villedende, men vakten gjør jobben sin. Den nekter deg å starte et nytt dokument oppå en komponent som fortsatt tror den er midt i et dokument. Løsningen er ikke å slå ut vakten. Den er å slutte å gjenbruke en oppbrukt instans
Livssyklusen, i den rekkefølgen den må skje
Hvert dokument HotPDF skriver fra bunnen av, følger de samme fire taktslagene, og rekkefølgen er ikke forhandlingsbar. Create allokerer komponenten. BeginDoc åpner dokumentet og låser de strukturelle valgene, så alt som påvirker hele filen (sidestørrelse, komprimering, kryptering, utdatafilnavn) må settes mellom Create og BeginDoc. Så tegner du. Så skriver EndDoc bytene til disk. Free frigjør instansen. Tegnekall plassert før BeginDoc har ingen side å lande på; egenskaper for hele dokumentet som tildeles etter det, blir ignorert uten en lyd
var
Pdf: THotPDF;
begin
Pdf := THotPDF.Create(nil);
try
Pdf.FileName := 'invoice.pdf';
Pdf.BeginDoc; // åpner dokumentet
Pdf.CurrentPage.SetFont('Arial', [], 11);
Pdf.CurrentPage.TextOut(50, 760, 0, 'Invoice 2026-042');
Pdf.EndDoc; // skriver invoice.pdf og avslutter den
finally
Pdf.Free; // én instans, ett dokument
end;
end;
Les det som arbeidsenheten. Én Create, én BeginDoc, én EndDoc, én Free, én fil på disk. I det øyeblikket du vil ha en fil nummer to, starter du en ny arbeidsenhet, og det betyr en ny instans
Hva "gjenbruk" bør bety: en ny instans per fil
Versjonen som brekker, prøver å være nøysom med allokering: bygg komponenten én gang, gå i løkke over en bunke, kall BeginDoc og EndDoc inne i løkken. Andre runde kaster et unntak. Versjonen som virker, behandler hver utdatafil som sitt eget kortlivde objekt, og allokeringskostnaden ved å opprette en komponent er ubetydelig ved siden av arbeidet med å sette opp og serialisere en PDF, så det er ingenting å spare på å hamstre instansen
procedure WriteBatch(const Names: TArray<string>);
var
I: Integer;
Pdf: THotPDF;
begin
for I := 0 to High(Names) do
begin
Pdf := THotPDF.Create(nil); // ny instans for hver runde
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 plassert inne i løkken er den delen det er verdt å forsvare i en kodegjennomgang. Hvis BeginDoc eller et tegnekall kaster et unntak midtveis i ett dokument, blir instansen for den runden likevel frigjort før den neste begynner, så én dårlig post etterlater ikke en halvbygd komponent som forgifter resten av kjøringen. Dra Create ut over løkken for å "optimalisere", og du er tilbake til den opprinnelige feilen, nå ikledd en bunkeløkke
Å endre en eksisterende fil er et annet inngangspunkt
Det finnes en annen lesning av "gjenbruk" som er helt legitim: du vil ikke ha et blankt dokument, du vil åpne en PDF som allerede finnes og endre den. Den veien går ikke gjennom BeginDoc i det hele tatt, og det er nettopp derfor feilmeldingen nevner lasting. Du laster inn filen, redigerer den og lagrer under det navnet du velger
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 returnerer sideantallet, og en verdi på null eller lavere betyr at innlastingen mislyktes, så det er verdt å sjekke før du rører CurrentPage. Parvisheten betyr noe: et dokument du åpnet med LoadFromFile, lagres med SaveLoadedDocument, ikke med paret BeginDoc/EndDoc, som hører til dokumenter du forfatter fra ingenting. Å blande de to er den vanligste måten å forvirre den samme tilstandsmaskinen som ga den opprinnelige feilen. Hold de to flytene adskilt i hodet: BeginDoc ... EndDoc oppretter, LoadFromFile ... SaveLoadedDocument redigerer
Fillåsproblemet er reelt, og svaret er ikke å drepe visningsvinduer
Gjenbruksfeilen kommer ofte i følge med en klage nummer to, og de to blir blandet sammen fordi de dukker opp i den samme arbeidsflyten der filen genereres på nytt. En bruker åpner PDF-en du nettopp lagde, lar den ligge åpen i Acrobat eller Foxit, og utløser så en ny bygging. EndDoc prøver å skrive til den samme stien, operativsystemet nekter fordi visningsprogrammet holder en leseandel som blokkerer skrivere, og du får en feil om nektet tilgang. Denne er virkelig et spørsmål om fillåsing i Windows og ikke om komponentens tilstand, og den fortjener et ekte svar i stedet for en omvei
Omveien som sirkulerer, å telle opp vinduer på øverste nivå og sende WM_CLOSE til alt som har en tittel som ligner på et PDF-visningsprogram, er feil instinkt. Den strekker seg over prosessgrenser for å lukke vinduer programmet ditt ikke eier, den gjetter på visningsprogrammer ut fra titteltekst, og den kan kaste bort en brukers ulagrede annotasjoner uten å spørre. Behandle hele den tilnærmingen som en vond lukt. Den pålitelige løsningen er å aldri skrive til en sti en annen prosess kan holde. Serialiser til en midlertidig fil i den samme katalogen, og bytt den så på plass med et atomisk navnebytte når EndDoc har lykkes. Har et visningsprogram fortsatt den gamle filen åpen, vil navnebyttet enten lykkes rent eller feile høylytt, og du får fram en tydelig melding i stedet for å slåss med låsen
uses
System.SysUtils, System.IOUtils;
procedure WritePdfAtomically(const FinalPath: string);
var
Pdf: THotPDF;
TempPath: string;
begin
// Midlertidig fil i SAMME katalog som målet: et navnebytte inne i ett
// NTFS-volum bytter navnet atomisk, mens en flytting på tvers av volumer
// degraderer til kopier-og-slett og mister den garantien
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; // den midlertidige filen er komplett på disk her
finally
Pdf.Free;
end;
// Bytt på plass. TFile.Move nekter å overskrive, så rydd bort et gammelt
// mål først; holder et visningsprogram fortsatt den gamle filen, er det
// slettingen som feiler, høylytt, før de gode bytene røres
if TFile.Exists(FinalPath) then
TFile.Delete(FinalPath);
TFile.Move(TempPath, FinalPath); // eller: RenameFile(TempPath, FinalPath)
except
if TFile.Exists(TempPath) then
TFile.Delete(TempPath); // etterlat aldri en halvskrevet midlertidig fil
raise;
end;
end;
To ærlige fotnoter til den koden. TFile.Move og den klassiske RenameFile peker begge til det samme navnebyttet i Windows, som bare er atomisk når kilde og mål ligger på samme volum, og det er nøyaktig derfor den midlertidige filen legges i målkatalogen og ikke i TPath.GetTempPath. Og paret slett-så-flytt er ikke i seg selv ett atomisk steg: det finnes et kort vindu der ingen av filene finnes. For et skrivebordsprogram som genererer en rapport på nytt, er det vinduet uten betydning; lesere som trenger en strengere kontrakt på samme volum, kan kalle Win32-funksjonene ReplaceFile eller MoveFileEx med MOVEFILE_REPLACE_EXISTING direkte, som slår byttet sammen til ett kall
For en server med stort volum som stadig genererer dokumenter på nytt, er den ryddigere disiplinen å skrive hver utdatafil under et unikt navn (et tidsstempel eller en jobb-ID) slik at to kjøringer aldri kjemper om én sti, og la en egen oppbevaringspolicy rydde bort gamle filer. Mønsteret er én linje med navnedisiplin per forespørsel
// Én utdatasti per forespørsel: to samtidige jobber kan aldri kjempe
// om det samme navnet, så ingen omdøpingsdans og ingen lås å miste
OutName := Format('statement-%s-%s.pdf',
[CustomerId, TGUID.NewGuid.ToString.Trim(['{', '}'])]);
Pdf.FileName := TPath.Combine(OutputDir, OutName);
En forespørsels-ID eller jobb-ID fungerer like godt som GUID-en når rammeverket rundt allerede gir deg en, og den gjør filnavnet sporbart tilbake til en loggline gratis. Uansett er prinsippet det samme: design slik at filen du skriver, er din alene i det øyeblikket du skriver den. Låsen forsvinner ikke fordi du tvang et vindu igjen, men fordi ingenting annet rører bytene
Formen på løsningen
Skrell de to problemene tilbake til røttene, og begge handler om å respektere grenser. Tilstandsmaskinfeilen vil at du skal respektere instansgrensen: én THotPDF, ett dokument, og så slippe den og lage en ny. Fillåsfeilen vil at du skal respektere filgrensen: skriv der ingenting annet leser, og flytt så resultatet på plass. Ingen av dem krever at du lapper biblioteket eller skripter skrivebordet. Begge faller ut av å behandle hvert dokument som en selvstendig arbeidsenhet, opprettet på nytt, skrevet rent og sluppet, som er det samme mønsteret som gjør resten av komponenten forutsigbar
Kallene BeginDoc, EndDoc, LoadFromFile og SaveLoadedDocument som vises her, er en del av HotPDF Delphi Component for Delphi og C++Builder