Klaida skamba Please load the document before using BeginDoc ir beveik visada iškyla antrąjį kartą. Pirmasis dokumentas įrašomas puikiai. Paskui to paties THotPDF egzemplioriaus paprašoma pradėti antrą, BeginDoc meta išimtį, o pranešimas rodo į dokumento įkėlimą, tai yra į priešingybę tam, ką kodas bando daryti. Būtent simptomo ir pranešimo neatitikimas daro šią bėdą tokią atkaklią. Tikroji tema yra komponento gyvavimo ciklas, ir kai jis susidėlioja, klaida nustoja būti paslaptinga

THotPDF egzempliorius yra vienas dokumentas, o ne dokumentų fabrikas
Viliojantis mintinis modelis yra toks, kad THotPDF yra tarnybinis objektas, kurį paleidžiate vieną kartą ir šeriate dokumentais, panašiai kaip laikote atvirą duomenų bazės ryšį ir per jį leidžiate užklausą po užklausos. Jis nėra toks. Egzempliorius modeliuoja vieną kuriamą dokumentą, o jo vidinis būsenų automatas remiasi prielaida, kad kelią jis nueina vieną kartą: nuo tuščios būsenos, per atvirą dokumentą, iki įrašyto failo. BeginDoc tą kelią atveria ir pažymi egzempliorių kaip turintį vykdomą dokumentą. EndDoc viską serializuoja į FileName ir jį uždaro. BeginDoc iškvietimas dar kartą tam pačiam jau baigtam egzemplioriui prašo jo vėl įžengti į būseną, iš kurios jis niekada švariai neišėjo, o suveikianti sargyba yra ta, kurios pranešimas kaip tik mini įkėlimą, nes viduje sąlygos „pasiruošęs pradėti“ ir „turi įkeltą dokumentą“ tikrinamos kartu
Taigi pranešimas klaidina, bet sargyba dirba savo darbą. Ji atsisako leisti pradėti šviežią dokumentą ant komponento, kuris vis dar mano esantis dokumento viduryje. Sprendimas nėra sargybą apeiti. Sprendimas yra liautis naudoti išnaudotą egzempliorių
Gyvavimo ciklas ta tvarka, kuria jis privalo vykti
Kiekvienas dokumentas, kurį HotPDF rašo nuo nulio, praeina tuos pačius keturis taktus, ir tvarka nediskutuotina. Create išskiria komponentą. BeginDoc atveria dokumentą ir užfiksuoja struktūrinius pasirinkimus, todėl viskas, kas veikia visą failą (puslapio dydis, glaudinimas, šifravimas, išvesties failo vardas), turi būti nustatyta tarp Create ir BeginDoc. Tada piešiate. Tada EndDoc įrašo baitus į diską. Free atlaisvina egzempliorių. Piešimo iškvietimai, padėti prieš BeginDoc, neturi puslapio, ant kurio nusileistų; viso dokumento savybės, priskirtos po jo, ignoruojamos be jokio skundo
var
Pdf: THotPDF;
begin
Pdf := THotPDF.Create(nil);
try
Pdf.FileName := 'invoice.pdf';
Pdf.BeginDoc; // atveria dokumentą
Pdf.CurrentPage.SetFont('Arial', [], 11);
Pdf.CurrentPage.TextOut(50, 760, 0, 'Invoice 2026-042');
Pdf.EndDoc; // įrašo invoice.pdf ir jį uždaro
finally
Pdf.Free; // vienas egzempliorius, vienas dokumentas
end;
end;
Skaitykite tai kaip darbo vienetą. Vienas Create, vienas BeginDoc, vienas EndDoc, vienas Free, vienas failas diske. Tą akimirką, kai užsinorite antro failo, pradedate naują darbo vienetą, o tai reiškia naują egzempliorių
Ką turėtų reikšti „pakartotinis naudojimas“: naujas egzempliorius kiekvienam failui
Lūžtanti versija stengiasi taupyti atminties išskyrimą: sukurti komponentą vieną kartą, apeiti paketą cikle, o BeginDoc ir EndDoc kviesti ciklo viduje. Antroji iteracija meta išimtį. Veikianti versija kiekvieną išvestį traktuoja kaip savo trumpaamžį objektą, o komponento sukūrimo kaina yra nereikšminga palyginti su PDF išdėstymo ir serializavimo darbu, todėl laikantis įsikibus egzemplioriaus nieko nesutaupoma
procedure WriteBatch(const Names: TArray<string>);
var
I: Integer;
Pdf: THotPDF;
begin
for I := 0 to High(Names) do
begin
Pdf := THotPDF.Create(nil); // naujas egzempliorius kiekvienam ratui
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;
Ciklo viduje sėdintis try/finally yra ta dalis, kurią verta apginti kodo peržiūroje. Jei BeginDoc ar bet kuris piešimo iškvietimas išmeta išimtį vieno dokumento viduryje, tos iteracijos egzempliorius vis tiek atlaisvinamas prieš prasidedant kitai, todėl vienas blogas įrašas nepalieka pusiau sukurto komponento ir neužnuodija likusio paleidimo. Ištraukite Create virš ciklo, kad „optimizuotumėte“, ir grįšite prie pirminės klaidos, tik dabar apsirengusios paketiniu ciklu
Esamo failo keitimas yra kitas įėjimo taškas
Yra ir antras „pakartotinio naudojimo“ skaitymas, visiškai teisėtas: jums reikia ne tuščio dokumento, o atverti jau egzistuojantį PDF ir jį pakeisti. Tas kelias iš viso neina per BeginDoc, ir būtent todėl klaidos pranešimas mini įkėlimą. Failą įkeliate, redaguojate ir įrašote tokiu vardu, kokį pasirenkate
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 grąžina puslapių skaičių, o nulis ar mažesnė reikšmė reiškia, kad įkėlimas nepavyko, todėl verta tai patikrinti prieš liečiant CurrentPage. Poravimas svarbus: dokumentas, atvertas su LoadFromFile, įrašomas su SaveLoadedDocument, o ne su BeginDoc/EndDoc pora, kuri priklauso dokumentams, kuriuos kuriate iš nieko. Šių dviejų maišymas yra dažniausias būdas supainioti tą patį būsenų automatą, kuris ir pagimdė pirminę klaidą. Laikykite abu srautus mintyse atskirai: BeginDoc ... EndDoc kuria, LoadFromFile ... SaveLoadedDocument redaguoja
Failo užrakto problema yra tikra, o atsakymas nėra žudyti peržiūros langus
Pakartotinio naudojimo klaida dažnai keliauja kartu su antru nusiskundimu, ir šie du susipainioja, nes iškyla toje pačioje failo pergeneravimo darbo eigoje. Naudotojas atveria ką tik jūsų pagamintą PDF, palieka jį atvirą Acrobat ar Foxit programoje, o tada paleidžia perkūrimą. EndDoc bando rašyti tuo pačiu keliu, operacinė sistema atsisako, nes peržiūros programa laiko skaitymo dalį, blokuojančią rašytojus, ir jūs gaunate prieigos uždraudimo klaidą. Ši bėda iš tikrųjų yra Windows failų užrakinimo, o ne komponento būsenos klausimas, ir ji nusipelno tikro atsakymo, o ne apėjimo
Sklandantis apėjimas, tai yra viršutinio lygio langų išvardijimas ir WM_CLOSE siuntimas viskam, kieno antraštė atrodo kaip PDF peržiūros programa, yra klaidingas instinktas. Jis peržengia procesų ribas, kad uždarytų langus, kurie jūsų programai nepriklauso, spėlioja peržiūros programas pagal antraštės tekstą ir gali be klausimo išmesti naudotojo neišsaugotas anotacijas. Traktuokite visą tą požiūrį kaip blogą kvapą. Patikimas sprendimas yra niekada nerašyti keliu, kurį gali laikyti kitas procesas. Serializuokite į laikinąjį failą tame pačiame kataloge, o tada, kai EndDoc pavyksta, perkelkite jį į vietą atominiu pervadinimu. Jei peržiūros programa vis dar laiko atvirą seną failą, pervadinimas arba pavyksta švariai, arba žlunga garsiai, ir jūs parodote aiškų pranešimą, o ne kovojate su užraktu
uses
System.SysUtils, System.IOUtils;
procedure WritePdfAtomically(const FinalPath: string);
var
Pdf: THotPDF;
TempPath: string;
begin
// Laikinasis failas TAME PAČIAME kataloge kaip ir tikslas: pervadinimas
// vieno NTFS tomo viduje vardą sukeičia atomiškai, o perkėlimas tarp tomų
// nusirita iki kopijavimo ir trynimo bei tą garantiją praranda
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; // čia laikinasis failas diske jau baigtas
finally
Pdf.Free;
end;
// Sukeičiame į vietą. TFile.Move atsisako perrašyti, todėl pirma
// pašaliname pasenusį tikslą; jei peržiūros programa vis dar laiko seną
// failą, žlunga būtent trynimas, garsiai, dar neliečiant gerų baitų
if TFile.Exists(FinalPath) then
TFile.Delete(FinalPath);
TFile.Move(TempPath, FinalPath); // arba: RenameFile(TempPath, FinalPath)
except
if TFile.Exists(TempPath) then
TFile.Delete(TempPath); // niekada nepalikite pusiau įrašyto laikino failo
raise;
end;
end;
Dvi sąžiningos išnašos apie tą kodą. TFile.Move ir klasikinis RenameFile abu atitinka tą patį Windows pervadinimą, kuris yra atominis tik tada, kai šaltinis ir paskirtis yra tame pačiame tome, ir būtent todėl laikinasis failas keliauja į paskirties katalogą, o ne į TPath.GetTempPath. Ir trynimo bei perkėlimo pora pati savaime nėra vienas atominis žingsnis: yra trumpas tarpsnis, kai neegzistuoja nė vienas failas. Stalinei programai, pergeneruojančiai ataskaitą, tas tarpsnis nesvarbus; skaitytojai, kuriems tame pačiame tome reikia griežtesnės garantijos, gali tiesiogiai kviesti Win32 ReplaceFile arba MoveFileEx su MOVEFILE_REPLACE_EXISTING, ir tai sukeitimą suspaudžia į vieną iškvietimą
Didelio srauto serveriui, nuolat pergeneruojančiam dokumentus, švaresnė drausmė yra kiekvieną išvestį rašyti unikaliu vardu (laiko žyma ar užduoties identifikatoriumi), kad du paleidimai niekada nesivaržytų dėl vieno kelio, o senus failus tvarkytų atskira saugojimo politika. Šis modelis yra viena vardų drausmės eilutė vienai užklausai
// Vienas išvesties kelias vienai užklausai: dvi lygiagrečios užduotys
// niekada nesivaržo dėl to paties vardo, tad nėra nei pervadinimo šokio, nei užrakto
OutName := Format('statement-%s-%s.pdf',
[CustomerId, TGUID.NewGuid.ToString.Trim(['{', '}'])]);
Pdf.FileName := TPath.Combine(OutputDir, OutName);
Užklausos ar užduoties identifikatorius tinka lygiai taip pat gerai kaip GUID, kai aplinkinė karkasas jau tokį jums paduoda, ir tai nemokamai padaro failo vardą atsekamą iki žurnalo eilutės. Bet kuriuo atveju principas tas pats: projektuokite taip, kad rašomas failas rašymo akimirką priklausytų tik jums. Užraktas dingsta ne todėl, kad priverstinai uždarėte langą, o todėl, kad daugiau niekas neliečia tų baitų
Sprendimo forma
Nuluptos iki šaknų abi problemos yra apie ribų gerbimą. Būsenų automato klaida nori, kad gerbtumėte egzemplioriaus ribą: vienas THotPDF, vienas dokumentas, o tada paleiskite jį ir pasidarykite kitą. Failo užrakto klaida nori, kad gerbtumėte failo ribą: rašykite ten, kur niekas neskaito, o tada rezultatą perkelkite į vietą. Nė viena nereikalauja lopyti bibliotekos ar skriptuoti darbastalio. Abi išsisprendžia traktuojant kiekvieną dokumentą kaip savarankišką darbo vienetą, sukurtą iš naujo, švariai įrašytą ir paleistą, o tai tas pats modelis, kuris ir visą likusį komponentą daro nuspėjamą
Čia parodyti BeginDoc, EndDoc, LoadFromFile ir SaveLoadedDocument iškvietimai yra HotPDF Delphi komponento, skirto Delphi ir C++Builder, dalis