A hibaüzenet így szól: Please load the document before using BeginDoc, és szinte mindig másodszorra bukkan fel. Az első dokumentum rendben megíródik. Aztán ugyanazt a THotPDF példányt megkérik, hogy kezdjen egy másodikat, a BeginDoc kivételt dob, az üzenet pedig egy dokumentum betöltésére mutat, ami épp az ellenkezője annak, amit a kód tenni próbál. A tünet és az üzenet közti eltérés az, amitől ez a hiba megragad. A valódi téma a komponens életciklusa, és amint ez a helyére kattan, a hiba megszűnik rejtélyes lenni

A THotPDF példány egy dokumentum, nem dokumentumgyár
A csábító gondolati modell az, hogy a THotPDF szolgáltatásobjektum, amelyet egyszer indít el, majd dokumentumokkal etet, ahogy egy adatbázis-kapcsolatot tartana nyitva, és lekérdezést lekérdezés után futtatna rajta. Nem ez a helyzet. Egy példány egyetlen épülő dokumentumot modellez, és a belső állapotgépe azt a feltevést hordozza, hogy egyszer járja végig az utat: az üres állapotból egy nyitott dokumentumon át egy mentett fájlig. A BeginDoc nyitja meg ezt az utat, és megjelöli a példányt úgy, hogy folyamatban lévő dokumentuma van. Az EndDoc mindent a FileName útvonalra sorosít és lezárja. Ha ugyanazon a befejezett példányon újra hívja a BeginDoc metódust, azt kéri tőle, hogy lépjen vissza olyan állapotba, amelyet soha nem hagyott el tisztán, és az az őr sül el, amelynek üzenete véletlenül a betöltést említi, mert belül a „kezdésre kész” és a „van betöltött dokumentuma” feltételt együtt ellenőrzi
Az üzenet tehát félrevezető, de az őr a dolgát végzi. Megtagadja, hogy friss dokumentumot kezdjen egy olyan komponens tetején, amely még mindig azt hiszi, dokumentum közepén jár. A javítás nem az őr kicselezése. Hanem az, hogy hagyja abba az elhasznált példány újrahasználatát
Az életciklus abban a sorrendben, ahogy történnie kell
Minden dokumentum, amelyet a HotPDF a nulláról ír, ugyanazt a négy ütemet követi, és a sorrend nem tárgyalható. A Create lefoglalja a komponenst. A BeginDoc megnyitja a dokumentumot és rögzíti a szerkezeti döntéseket, tehát mindent, ami a teljes fájlra hat (oldalméret, tömörítés, titkosítás, kimeneti fájlnév), a Create és a BeginDoc között kell beállítani. Utána rajzol. Aztán az EndDoc lemezre írja a bájtokat. A Free elengedi a példányt. Azoknak a rajzolási hívásoknak, amelyek a BeginDoc elé kerülnek, nincs oldaluk, ahová leszállhatnának; a dokumentumszintű tulajdonságokat pedig, ha utána adja meg, panasz nélkül figyelmen kívül hagyja
var
Pdf: THotPDF;
begin
Pdf := THotPDF.Create(nil);
try
Pdf.FileName := 'invoice.pdf';
Pdf.BeginDoc; // megnyitja a dokumentumot
Pdf.CurrentPage.SetFont('Arial', [], 11);
Pdf.CurrentPage.TextOut(50, 760, 0, 'Invoice 2026-042');
Pdf.EndDoc; // kiírja az invoice.pdf fájlt és lezárja
finally
Pdf.Free; // egy példány, egy dokumentum
end;
end;
Olvassa ezt munkaegységként. Egy Create, egy BeginDoc, egy EndDoc, egy Free, egy fájl a lemezen. Abban a pillanatban, amikor második fájlt akar, új munkaegységet kezd, ami új példányt jelent
Mit jelentsen az „újrahasználat”: fájlonként friss példányt
A törött változat takarékos akar lenni a foglalással: egyszer építi fel a komponenst, végigmegy egy kötegen, és a cikluson belül hívja a BeginDoc és EndDoc metódust. A második iteráció kivételt dob. A működő változat minden kimenetet saját, rövid életű objektumként kezel, és egy komponens létrehozásának foglalási költsége elenyésző egy PDF elrendezésének és sorosításának munkájához képest, tehát nincs mit megtakarítani a példány felhalmozásával
procedure WriteBatch(const Names: TArray<string>);
var
I: Integer;
Pdf: THotPDF;
begin
for I := 0 to High(Names) do
begin
Pdf := THotPDF.Create(nil); // minden körben új példány
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;
A cikluson belül ülő try/finally az a rész, amelyet érdemes megvédeni egy kódátnézésen. Ha a BeginDoc vagy bármelyik rajzolási hívás félúton kivételt dob egy dokumentumon belül, az adott iteráció példánya akkor is felszabadul, mielőtt a következő elkezdődne, tehát egyetlen rossz rekord nem hagy magára egy félig felépített komponenst, és nem mérgezi meg a futás többi részét. Emelje ki a Create hívást a ciklus fölé „optimalizálásként”, és máris az eredeti hibánál tart, csak most kötegelt ciklusba öltözve
Egy meglévő fájl módosítása másik belépési pont
Az „újrahasználat” szónak van egy teljesen jogos második olvasata: nem üres dokumentumot akar, hanem egy már létező PDF-et megnyitni és megváltoztatni. Ez az út egyáltalán nem megy át a BeginDoc metóduson, és pontosan ezért említ betöltést a hibaüzenet. Betölti a fájlt, szerkeszti, és tetszőleges néven menti
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;
A LoadFromFile az oldalszámot adja vissza, és a nulla vagy annál kisebb érték sikertelen betöltést jelent, tehát érdemes ellenőrizni, mielőtt a CurrentPage tulajdonsághoz nyúlna. A párosítás számít: azt a dokumentumot, amelyet a LoadFromFile hívással nyitott meg, a SaveLoadedDocument metódussal kell menteni, nem a BeginDoc/EndDoc párossal, amely a nulláról írt dokumentumoké. A kettő keverése a leggyakoribb mód arra, hogy összezavarja ugyanazt az állapotgépet, amely az eredeti hibát is előállította. Tartsa a két folyamot gondolatban külön: a BeginDoc ... EndDoc létrehoz, a LoadFromFile ... SaveLoadedDocument szerkeszt
A fájlzárolás gondja valós, és a megoldás nem a megjelenítőablakok kilövése
Az újrahasználati hiba gyakran egy második panasszal együtt utazik, és a kettő azért gabalyodik össze, mert ugyanabban az újragenerálási munkafolyamatban bukkannak fel. Egy felhasználó megnyitja az imént előállított PDF-et, nyitva hagyja Acrobatban vagy Foxitban, majd újraépítést indít. Az EndDoc ugyanarra az útvonalra próbál írni, az operációs rendszer megtagadja, mert a megjelenítő olyan olvasási megosztást tart, amely kizárja az írókat, és hozzáférés-megtagadási hibát kap. Ez valóban Windows-fájlzárolási kérdés, nem komponensállapot-kérdés, és valódi választ érdemel, nem kerülő megoldást
Az a kerülő megoldás, amely kering — hogy felsorolja a legfelső szintű ablakokat, és WM_CLOSE üzenetet küld mindennek, aminek a címe PDF-megjelenítőre hasonlít —, rossz ösztön. Folyamathatárokon nyúl át, hogy olyan ablakokat zárjon be, amelyeket a programja nem birtokol, a címszöveg alapján találgatja a megjelenítőket, és kérdés nélkül eldobhatja a felhasználó nem mentett jegyzeteit. Kezelje az egész megközelítést rossz jelként. A megbízható javítás az, hogy soha ne írjon olyan útvonalra, amelyet más folyamat tarthat. Sorosítson ideiglenes fájlba ugyanabban a könyvtárban, majd cserélje a helyére atomi átnevezéssel, ha az EndDoc sikerrel járt. Ha egy megjelenítőnél még mindig nyitva van a régi fájl, az átnevezés vagy tisztán sikerül, vagy hangosan elhasal, és Ön világos üzenetet jelenít meg ahelyett, hogy a zárral küzdene
uses
System.SysUtils, System.IOUtils;
procedure WritePdfAtomically(const FinalPath: string);
var
Pdf: THotPDF;
TempPath: string;
begin
// Ideiglenes fájl a célfájllal AZONOS könyvtárban: az átnevezés egyetlen
// NTFS köteten belül atomi módon cseréli a nevet, míg a kötetek közti
// mozgatás másolásra és törlésre esik szét, és elveszti ezt a garanciát
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; // az ideiglenes fájl itt már teljes a lemezen
finally
Pdf.Free;
end;
// Csere a helyére. A TFile.Move nem ír felül, ezért előbb takarítsa el az
// elavult célfájlt; ha egy megjelenítő még fogja a régi fájlt, a törlés az,
// ami hangosan elhasal, mielőtt a jó bájtokhoz bárki hozzáérne
if TFile.Exists(FinalPath) then
TFile.Delete(FinalPath);
TFile.Move(TempPath, FinalPath); // vagy: RenameFile(TempPath, FinalPath)
except
if TFile.Exists(TempPath) then
TFile.Delete(TempPath); // sose hagyjon magára félig írt ideiglenes fájlt
raise;
end;
end;
Két őszinte lábjegyzet ehhez a kódhoz. A TFile.Move és a klasszikus RenameFile ugyanarra a Windows-átnevezésre képződik le, amely csak akkor atomi, ha a forrás és a cél ugyanazon a köteten van, és pontosan ezért kerül az ideiglenes fájl a célkönyvtárba, nem a TPath.GetTempPath helyére. A törlés és a mozgatás párosa pedig önmagában nem egyetlen atomi lépés: van egy rövid ablak, amelyben egyik fájl sem létezik. Egy jelentést újragyártó asztali alkalmazásnál ez az ablak lényegtelen; azok az olvasók, akiknek ugyanazon a köteten erősebb szerződés kell, közvetlenül hívhatják a Win32 ReplaceFile függvényt vagy a MoveFileEx hívást a MOVEFILE_REPLACE_EXISTING jelzővel, ami egyetlen hívássá vonja össze a cserét
Egy nagy forgalmú kiszolgálón, amely folyamatosan újragenerálja a dokumentumokat, a tisztább fegyelem az, hogy minden kimenetet egyedi néven ír ki (időbélyeggel vagy feladatazonosítóval), így két futás soha nem verseng egyetlen útvonalért, a régi fájlokat pedig külön megőrzési irányelv takarítja. A minta kérésenként egyetlen sornyi elnevezési fegyelem
// Kérésenként egy kimeneti útvonal: két párhuzamos feladat soha nem versenghet
// ugyanazért a névért, tehát nincs átnevezési tánc és nincs elvesztendő zár
OutName := Format('statement-%s-%s.pdf',
[CustomerId, TGUID.NewGuid.ToString.Trim(['{', '}'])]);
Pdf.FileName := TPath.Combine(OutputDir, OutName);
Egy kérés- vagy feladatazonosító ugyanolyan jól működik, mint a GUID, ha a környező keretrendszer amúgy is ad egyet, ráadásul ingyen visszakövethetővé teszi a fájlnevet egy naplósorig. Az elv mindkét esetben ugyanaz: úgy tervezzen, hogy az a fájl, amelyet ír, kizárólag az Öné legyen abban a pillanatban, amikor írja. A zár nem azért tűnik el, mert erőszakkal becsukott egy ablakot, hanem mert semmi más nem nyúl a bájtokhoz
A javítás alakja
Fejtse vissza a két gondot a gyökeréig, és mindkettő a határok tiszteletben tartásáról szól. Az állapotgép hibája azt kéri, hogy tartsa tiszteletben a példányhatárt: egy THotPDF, egy dokumentum, aztán engedje el és készítsen újat. A fájlzárolási hiba azt kéri, hogy tartsa tiszteletben a fájlhatárt: oda írjon, ahol semmi más nem olvas, aztán mozgassa a helyére az eredményt. Egyik sem kíván könyvtárfoltozást vagy asztali szkriptelést. Mindkettő abból következik, hogy minden dokumentumot önálló munkaegységként kezel: frissen létrehozva, tisztán kiírva és elengedve — ugyanaz a minta, amely a komponens többi részét is kiszámíthatóvá teszi
Az itt bemutatott BeginDoc, EndDoc, LoadFromFile és SaveLoadedDocument hívások a HotPDF Delphi Component részei, Delphihez és C++Builderhez