Klik na Cancel tokom štampanja u pregledaču zasnovanom na PDFium Component-u ponekad nije radio ništa: petlja je nastavila da iscrtava sve preostale stranice i kopije, a zadatak je ipak stizao do štampača. Uzrok je bila Free Pascal-ova for petlja, koja fiksira gornju granicu pri ulasku u petlju, pa promena promenljive iza CopyCount ili ToPage usred petlje nije uticala na već pokrenuto izvršavanje. Greška sa granicama for-petlje peti je problem otkriven u istoj reviziji kompatibilnosti PDFiumPas-a između Delphija i FPC-a koja je pronašla još četiri problema pri prevođenju između kompajlera, ali se za razliku od njih nalazi u celosti u proceduri za štampanje pregledača — tester je morao da drži Cancel tokom dugog zadatka da bi primetio da se štampač nikada nije zaustavio
Zašto se zadatak štampanja nastavlja nakon klika na Cancel?
Zadatak štampanja se nastavlja jer provera otkazivanja resetuje samo promenljive koje određuju granice petlji, a ne i same petlje, čije je granice Free Pascal već zaključao pri njihovom pokretanju. Rukovalac SpeedButtonPrintClick iza dugmeta Print u demonstracijama PDFViewer i MultiPageViewer gradi svaki zadatak pomoću tri ugnježdene petlje: spoljašnja prolazi kroz grupisane setove kopija, srednja kroz stranice, a unutrašnja kroz pojedinačne kopije iste stranice. PrintDialog.Collate određuje koji brojač, CollateCopyCount ili CopyCount, zaista sadrži traženi broj kopija, dok drugi ostaje na jedan. Otkazivanje mora istovremeno da prođe kroz sva tri nivoa, a prva verzija ovog koda pokušala je to da uradi resetovanjem samih promenljivih granica čim Cancel postane true
for CollateCopy:= 1 to CollateCopyCount do
for Page:= FromPage to ToPage do
for Copy:= 1 to CopyCount do
begin
// ... render the page and send it to the printer ...
Application.ProcessMessages;
if Cancel then
begin
CollateCopyCount:= 0;
ToPage:= 0;
CopyCount:= 0;
end;
end;
Printer.EndDoc; // runs whether or not Cancel fired
Namena deluje dovoljno jasno: ako brojači koji određuju broj grupisanih setova, stranica i kopija padnu na nulu, petlje bi trebalo same da ostanu bez posla i završe se. Printer.EndDoc se zatim bezuslovno izvršava nakon petlje, bez obzira na način završetka, pa se čak i zadatak za koji je korisnik verovao da ga je zaustavio i dalje šalje u red štampanja sa svim stranicama iscrtanim pre nego što je klik primećen
Šta Free Pascal zaključava kada for-petlja počne
Free Pascal izračunava završnu vrednost for petlje tačno jednom, u trenutku njenog pokretanja, i više je ne proverava tokom njenog trajanja. for Page := FromPage to ToPage do čita ToPage samo jednom da bi izračunao broj iteracija, a zatim petlju više ne zanima promenljiva po imenu ToPage, već samo broj iteracija koji je već zapamtila. Postavljanje ToPage := 0 unutar tela petlje menja promenljivu koju pokrenuta petlja više ne koristi, pa je upravo zato Cancel mogao da bude true dok je štampač još primao stranice kroz više narednih iteracija, ponekad kroz sve njih
To je sasvim razumna navika preneta iz C-a ili C++-a, a demonstracija PDFium Component-a za C++Builder dovoljno je slična Pascal verziji da poređenje bude neposredno. Njen for (Copy = 1; Copy <= CopyCount; Copy++) ponovo proverava Copy <= CopyCount protiv trenutne vrednosti CopyCount pri svakom prolazu, pa postavljanje brojača na nulu tamo zaista završava petlju pri sledećoj proveri. Demonstracija za C++Builder sadržala je isti kod za postavljanje brojača na nulu i radila je, zbog čega je ista ideja delovala bezbedno za ponovnu upotrebu u Pascal izgradnji koja se nalazila odmah pored nje u istom repozitorijumu
Zašto Delphi izgradnja nije imala istu grešku?
Delphi demonstracija PDFViewer-a nije zavisila od toga da petlja primeti promenjenu granicu, jer njen put otkazivanja izlazi kroz izuzetak umesto kroz poređenje. Njena najunutrašnja petlja poziva RTL proceduru Abort čim Cancel postane true, čime se podiže tihi EAbort koji prolazi kroz sve tri ugnježdene for petlje do rukovaoca obuhvaćenog celim blokom za štampanje
Printer.BeginDoc;
try
for CollateCopy:= 1 to CollateCopyCount do
for Page:= FromPage to ToPage do
for Copy:= 1 to CopyCount do
begin
// ... render the page and send it to the printer ...
Application.ProcessMessages;
if Cancel then
Abort; // raises EAbort, unwinds all three loops at once
end;
Printer.EndDoc;
except
on E: EAbort do
Printer.Abort;
else
begin
Printer.Abort;
raise;
end;
end;
Izuzetak ne zavisi od toga koliko se for petlji nalazi između mesta na kom je podignut i rukovaoca koji ga hvata, što je upravo svojstvo potrebno ovom problemu. Ta otpornost nije bila svesna zaštita od ponašanja zaključavanja granice opisanog iznad — autor Delphi demonstracije jednostavno je posegnuo za drugim mehanizmom. Izlaz zasnovan na izuzetku ipak vredi istaći kao robusniji obrazac: preživeće dodavanje četvrtog nivoa ugnježđivanja, dok se niz ručno postavljenih naredbi Break mora zapamtiti i ponovo dodati svaki put kada se promeni ugnježđivanje petlji
Ispravka: Break na svakom nivou ugnježđivanja, uz zastavicu PrintSucceeded
Ispravka isporučena u PDFiumPas v2.27.0 zadržava strukturu sa tri petlje u demonstracijama pregledača za Lazarus i C++Builder, ali eksplicitno obrađuje otkazivanje na svakom nivou i razdvaja zaustavljanje petlji od slanja zadatka pomoću zastavice koja se čita tek nakon potpunog završetka petlje
Printer.BeginDoc;
try
Cancel:= False;
for CollateCopy:= 1 to CollateCopyCount do
begin
for Page:= FromPage to ToPage do
begin
for Copy:= 1 to CopyCount do
begin
// ... render the page and send it to the printer ...
Application.ProcessMessages;
if Cancel then
Break;
end;
if Cancel then
Break;
end;
if Cancel then
Break;
end;
PrintSucceeded:= not Cancel;
finally
if PrintSucceeded then
Printer.EndDoc
else
Printer.Abort;
end;
PrintSucceeded se namerno izračunava jednom, neposredno nakon izlaska iz trostruko ugnježdene petlje, samo na osnovu vrednosti not Cancel. Ništa unutar tela petlje ne odlučuje samostalno da li je zadatak uspešan — petlja može da se završi samo na dva načina: tako što potroši sve grupisane setove, stranice i kopije ili tako što naiđe na niz Break naredbi koje pokreće Cancel. PrintSucceeded čita ishod naknadno umesto da ga prati dok petlja radi. Upravo tako izbor između Printer.EndDoc i Printer.Abort u bloku finally ostaje pouzdan: ne izvršava se pre nego što se petlja zaista završi
Provera sopstvenih petlji za štampanje sa otkazivanjem
Tri provere lako se primenjuju i van ove konkretne procedure za štampanje. Nikada nemojte pretpostaviti da će Pascal for petlja primetiti promenu promenljive granice nakon pokretanja; ako petlja treba ranije da se završi, navedite to direktno pomoću Break, na svakom nivou ugnježđivanja kroz koji otkazivanje mora da prođe, a ne samo u najunutrašnjoj petlji. Kada je ugnježđivanje dovoljno duboko da se zaboravljena naredba Break može očekivati, prednost dajte mehanizmu izlaska koji po konstrukciji razmotava stek, kao što je izuzetak — Delphi demonstracija je to dobila bez dodatnog rada zahvaljujući paru Abort/EAbort. Svaki korak potvrde, kao što je EndDoc, uslovite zastavicom izračunatom strogo nakon petlje, nikada unutar nje, da zadatak koji je ranije zaustavljen nikada ne bi mogao da bude proglašen završenim
Ista revizija koja je ispravila ovu petlju dodatno je pooštrila način na koji devet demonstracija pregledača obrađuje tastaturu dok je dugme za otkazivanje vidljivo: sada ignorišu svaki taster osim Esc, pa slučajni Ctrl+P ili Ctrl+F tokom aktivnog štampanja ili pretrage više ne može da pokrene drugi zadatak preko prvog. Ova ispravka ponovnog ulaska jeste druga greška sa drugačijim mehanizmom, ali je proistekla iz iste provere šta Cancel zaista radi usred operacije. Pouka koju vredi preneti iz obe ispravke više je testerska nego programska: proverite otkazivanje na svakom kompajleru za koji se zajednička procedura za štampanje isporučuje, jer ideja poput brisanja brojača i očekivanja da će petlja to primetiti može proći testiranje pod C++Builder-om, dospeti neproverena u Free Pascal izgradnju, izgledati isto u izvornom kodu i otkazati na način koji niko ne vidi sve dok neko dovoljno dugo ne drži Cancel i ne posmatra kako se zadatak ipak završi. Za podešavanje štampanja na kom se ova logika otkazivanja zasniva, uputstvo za štampanje PDF dokumenata pomoću PDFium VCL komponente obuhvata ostatak
Ova ispravka otkazivanja štampanja isporučena je kao deo PDFium Component za Delphi, C++Builder i FPC/Lazarus, koji pri svakom izdanju pokreće isti skup demonstracija na sva tri kompajlera, pa se propusti poput ovog otkrivaju matricom izgradnje, a ne prijavom korisničkoj podršci