При натискане на Cancel по време на печат в прегледник на PDFium Component понякога нищо не се случва: цикълът продължава да визуализира всички оставащи страници и копия, а заданието стига до принтера. Причината е for цикълът на Free Pascal, който фиксира горната си граница еднократно при стартирането, така че зануляването на променливата зад CopyCount или ToPage по средата на цикъла не променя вече започналото изпълнение. Грешката с границите на for цикъла е петият проблем, открит в същия одит за съвместимост между Delphi и FPC на PDFiumPas, довел до четири други несъвместимости между компилатори, и за разлика от тях се намира изцяло в процедурата за печат на прегледника — тестер я забелязва едва когато задържи Cancel по време на дълго задание и види, че принтерът не спира
Защо заданието за печат продължава след натискане на Cancel
Заданието за печат продължава, защото проверката за отмяна занулява само променливите, които подават границите на циклите, а не самите цикли, чиито граници Free Pascal вече е фиксирал при стартирането им. Обработчикът SpeedButtonPrintClick зад бутона Print в демонстрациите PDFViewer и MultiPageViewer изгражда всяко задание от три вложени цикъла: външен цикъл за групите от комплектувани копия, среден цикъл за страниците и вътрешен цикъл за некомплектуваните копия на същата страница. PrintDialog.Collate определя кой брояч — CollateCopyCount или CopyCount — съдържа реално заявения брой копия, докато другият остава единица. При отмяна сигналът трябва да достигне едновременно и трите нива, а първата версия на този код се опитва да постигне това, като занулява променливите за границите в момента, в който Cancel стане истина
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
Замисълът изглежда достатъчно ясен: ако броячите, които определят броя на оставащите групи, страници и копия, паднат до нула, циклите би трябвало сами да изчерпят работата и да приключат. Printer.EndDoc обаче се изпълнява безусловно след цикъла, независимо как е завършил той, така че дори задание, което потребителят смята за спряно, пак се подава към спулера с всички страници, визуализирани преди проверката да забележи натискането
Какво фиксира Free Pascal при стартиране на for цикъл
Free Pascal изчислява крайната стойност на for цикъла точно веднъж, в момента на стартирането му, и повече не я преизчислява през целия живот на този цикъл. for Page := FromPage to ToPage do прочита ToPage само веднъж, за да изчисли броя на итерациите, след което изпълняващият се цикъл вече не се интересува от променлива с име ToPage, а само от предварително запазения брой повторения. Задаването на ToPage := 0 вътре в тялото променя стойност, която текущият цикъл вече не проверява, и точно затова Cancel може да е истина, докато принтерът продължава да получава страници още няколко итерации, а понякога и всички тях
Този модел е напълно разумен навик, пренесен от C или C++, а демонстрацията на прегледника на PDFium Component за C++Builder е достатъчно близка до Pascal варианта, за да направи сравнението директно. Нейният for (Copy = 1; Copy <= CopyCount; Copy++) проверява отново Copy <= CopyCount спрямо текущата стойност на CopyCount при всяко преминаване, така че зануляването на брояча наистина приключва цикъла при следващата проверка. Демонстрацията за C++Builder съдържа същия код за зануляване на броячите и той работи, което прави идеята да изглежда безопасна за повторна употреба в Pascal компилацията, разположена непосредствено до нея в същото хранилище
Защо компилацията с Delphi не срещна същата грешка
Демонстрацията PDFViewer за Delphi никога не разчита на това цикълът да забележи променена граница, защото пътят за отмяна се разгъва чрез изключение, а не чрез сравнение. Вътрешният цикъл извиква процедурата Abort от RTL в момента, в който Cancel стане истина, което повдига безшумно EAbort и го разпространява през всичките три вложени for цикъла до обработчик, обхващащ целия блок за печат
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;
Изключението не се интересува колко for цикъла отделят мястото на повдигането му от обработчика, който го улавя, и точно това свойство е необходимо тук. Тази устойчивост не е умишлена защита срещу описаното по-горе поведение на фиксираната граница — авторът на демонстрацията за Delphi просто е избрал друг инструмент. Изходът чрез изключение все пак е по-надеждният модел: той остава работещ и при добавяне на четвърто ниво на влагане, докато поредица от ръчно поставени оператори Break трябва да се помни и добавя отново при всяка промяна на влагането
Поправката: Break на всяко ниво на влагане с флаг PrintSucceeded
Поправката, включена в PDFiumPas v2.27.0, запазва структурата с три цикъла в демонстрациите за прегледници на Lazarus и C++Builder, но прави отмяната явна на всяко ниво и отделя спирането на циклите от подаването на заданието чрез флаг, който се прочита едва след пълното приключване на цикъла
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 се изчислява умишлено веднъж, непосредствено след излизането от тройно вложения цикъл, и зависи само от not Cancel. Нищо в тялото на цикъла не може самостоятелно да реши дали заданието е успешно — цикълът може да приключи по два начина: да изчерпи групите, страниците и копията или да достигне веригата от Break, задействана от Cancel, а PrintSucceeded отчита резултата след края, вместо да го проследява, докато цикълът върви. Именно това прави надежден избора между Printer.EndDoc и Printer.Abort в блока finally: той никога не се задейства, преди цикълът действително да е приключил
Проверка на собствени цикли за печат с отмяна
Три проверки са приложими далеч отвъд тази процедура за печат. Никога не приемайте, че Pascal for цикъл ще забележи промяна на променлива за граница след стартирането си; ако цикълът трябва да приключи по-рано, заявете го директно с Break на всяко ниво, през което трябва да премине отмяната, а не само във вътрешния цикъл. Когато влагането е достатъчно дълбоко и пропуснат Break е реална опасност, предпочитайте механизъм за изход, който по конструкция разгъва изпълнението, например изключение — демонстрацията за Delphi получава това естествено чрез двойката Abort/EAbort. Всеки етап за потвърждение като EndDoc трябва да зависи от флаг, изчислен строго след цикъла, никога вътре в него, за да не бъде погрешно прието задание, спряно по средата, за напълно завършило
Същият преглед, който поправи този цикъл, затегна и обработката на клавиатурата в деветте демонстрации на прегледника, когато бутонът за отмяна е видим: те вече поглъщат всеки клавиш освен Esc, така че случайно Ctrl+P или Ctrl+F по време на активен печат или търсене не може да стартира второ действие върху първото. Тази поправка за повторно влизане е различна грешка с различен механизъм, но също произлезе от прегледа на реалното поведение на Cancel по средата на операцията. Полезният извод и от двете поправки е по-скоро навик за тестване, отколкото правило за кодиране: проверявайте отмяната с всеки компилатор, за който действително се доставя общата процедура за печат, защото идея като зануляване на броячите и очакване цикълът да забележи това може да премине тестовете с C++Builder, да стигне непроверена до компилация с Free Pascal, да изглежда идентично в изходния код и да се провали така, че никой да не го види, докато не задържи Cancel достатъчно дълго, за да наблюдава как заданието все пак завършва. За настройката на печата, върху която стъпва тази логика за отмяна, ръководството за печат на PDF документи с VCL компонента PDFium разглежда останалата част
Тази поправка при отмяна на печатането е доставена като част от PDFium Component за Delphi, C++Builder и FPC/Lazarus, който изпълнява един и същ набор от демонстрации с трите компилатора при всяко издание, така че пропуски като този да бъдат откривани от матрица на компилациите, а не от заявка към поддръжката