Моя первая программа. Еще немного работы

Моя первая программа. Еще немного работы Продолжение. Начало в КГ №11 (352)
Итак, в прошлый раз мы остановились на том, что обнаружили в написанной нами программе существенный недочет: не предусмотрена остановка процедуры поиска. Но прежде, чем бросаться исправлять данную ошибку, стоит подумать, а нужна ли вообще этой программе возможность прервать работу процедуры обхода всех папок. На первый взгляд может показаться, что не нужна вовсе: сам алгоритм программы предполагает перебор ВСЕХ папок и файлов в пределах заданного пользователем уровня вложенности. Но такой подход со стороны разработчика, мягко говоря, не совсем верен. Пользователь должен иметь возможность остановить или завершить работу программы в ЛЮБОЙ момент времени. Написанная же нами программа в том виде, в котором она существует на данный момент, такой возможности не предоставляет. До конца работы алгоритма окно программы не будет отвечать ни на какие действия пользователя, так что ему не удастся ни свернуть ее на панель задач, ни закрыть. Более того, Диспетчер задач Windows будет детектировать эту программу как Not responding (не отвечающую). Итого, единственное, что останется пользователю в данной ситуации, это аварийно завершить программу при помощи того же Диспетчера задач. Думаю, каждый сталкивался с проблемой аварийного снятия задач, и не мне вам рассказывать, насколько это неприятно. Рассчитывать на то, что пользователи будут продолжать пользоваться этой программой после такого недружелюбного отношения к ним с ее стороны, было бы очень оптимистично. Здесь придирчивый читатель может поправить меня, напомнив, что эту программу мы писали не для распространения, а для самих себя, поэтому и не особо утруждали себя "дружественностью" ее интерфейса. А мы, зная об этом ее недостатке, а также о том, что программа не "висит", а всего лишь трудится в поте лица, как-нибудь переживем невозможность "нормальной" остановки ее выполнения. Что же, в этом есть доля правды, но все же, во-первых, сразу также было оговорено, что программой могут захотеть воспользоваться и наши друзья (а так, чаще всего, и случается), и вместо того, чтобы каждому из них читать лекцию о ее "скрытых особенностях", проще будет несколько подправить наш код. Во-вторых, даже сам автор программы, знающий об этих самых особенностях, не будет пребывать в восторге, если ему вдруг придется завершать выполнение программы аварийно. А в-третьих, к совершенству следует стремиться всегда, равно как всегда имеет смысл писать корректно работающие программы, вне зависимости от того, для каких нужд они предназначены и кто ими будет пользоваться.
Учитывая все, сказанное выше, пора приниматься за дело. Все, что нам необходимо, — это добавить на форме кнопку, повесить обработчик на ее нажатие, а в этом обработчике поместить код, останавливающий выполнение процедуры FindFiles. Итак, помещаем на форму еще один компонент TButton и, по договоренности, которую мы приняли в первой статье, даем ему имя btnStop. Теперь разумно предположить, что нам понадобится добавить в описание класса TMainForm одну булеву переменную ShouldStillExecute и при создании класса присваивать ей значение true. При нажатии на кнопку менять значение этой переменной на false. А в процедуре FindFiles добавить лишь строчку проверки:
if(ShouldStillExecute) then
exit;
Соответственно, при очередном рекурсивном входе в подпрограмму, ее выполнение будет прервано инструкцией exit. Такой способ мог бы сработать, если бы не одно весьма существенное "но". Как уже упоминалось, главная форма программы просто не получит управления до тех пор, пока процедура FindFiles не отработает полностью. Это значит, что сообщение, возникающее при нажатии на кнопку, не будет обработано, и, следовательно, переменная ShouldStillExecute так никогда и не примет значения false.
Довольно остро встает вполне разумный вопрос: "Что же тогда делать?". Как оказывается, все совсем не так плохо, как может показаться. Стоит только вспомнить о том, что, начиная с Windows 95, все версии этой операционной системы поддерживают многозадачность и многопоточность. В данном случае нас интересует последнее. Самым точным определением потока мне представляется определение, данное в книге Стива Тейксейры и Ксавье Пачеко "Borland Delphi. Руководство разработчика": Поток — это объект операционной системы, который представляет путь выполнения программы внутри отдельного процесса. Каждое 32-разрядное Windows-приложение имеет по крайней мере один поток, называемый первичным или основным. Он отвечает за создание дочерних окон и обработку сообщений. Для других задач (таких как выполнение математических вычислений и загрузка больших файлов) приложения в праве создавать дополнительные потоки. При этом потоки будут выполняться как бы "одновременно". Хотя на однопроцессорных компьютерах это физически невозможно, процессорное время будет делиться между потоками, согласно установленным им приоритетам, и будет создаваться ощущение, что потоки работают одновременно.
Думаю, из описания потока следует, что он будет как раз к месту в разрабатываемом нами приложении. Таким образом, теперь нам предстоит выделить процедуру FindFiles в отдельный поток. В Delphi для работы с потоками существует класс TThread. Этот класс инкапсулирует в себе большинство функций WinAPI для работы с потоками. TThread является абстрактным классом; это значит, что не может быть создано объектов данного класса. Данный класс может являться предком некоторого класса при наследовании, и, соответственно, программисту дозволено создавать экземпляры потомков класса TThread, при условии, что в них определена процедура Execute, объявленная в TThread и являющаяся абстрактной.
В метод Execute помещается код той процедуры, которая должна выполнятся в отдельном потоке. Экземпляр потока создается при помощи конструктора Create (CreateSuspended: boolean). Параметр CreateSuspended отвечает за то, будет ли поток создан в приостановленном состоянии. Значит, для того чтобы выделить обработку папок в отдельный поток, нам достаточно лишь создать класс-наследник TThread и определить метод Execute, поместив в него вызов процедуры FindFiles.
Существенным недостатком Delphi по сравнению с C++ является отсутствие множественного наследования (когда класс-потомок наследуется от двух и более родительских классов). В нашем же случае private-метод FindFiles класса TMainForm необходимо вызвать из класса, унаследованного от TThread. Это ограничение можно обойти при помощи интерфейсов, но все же в данном случае мы поступим несколько иначе.
Добавим к проекту новый модуль (Unit) и сохраним его, дав ему имя ThreadRoutines. Теперь в разделе Uses можно перечислить модули, которые необходимо включить в ThreadRoutines, а также описать класс TSearchThread, который и будет отвечать за поиск в отдельном потоке.
unit ThreadRoutines;

interface

uses Classes, StdCtrls, SysUtils;

type
TSearchThread = class(TThread)
private
memDump: TMemo;
OperationsNum: integer;
StartPath: String;

procedure FindFiles(Path: String; OperationsNum: integer);
function GetDirectoryName(Dir: String): String;
function LeaveJustName(Path: String): String;
function DeleteExtention(Path: String): String;

protected
procedure Execute; override;
public
constructor Create(OpNum:integer;const Path:string);
destructor Destroy; override;
end;
Подпрограммы FindFiles, GetDirectoryName, LeaveJustName, DeleteExtention (как их объявления, так и реализация) без каких-либо изменений переносятся из класса TMainForm в класс TSearchThread. В результате, все, что остается сделать, это написать конструктор и деструктор класса, а также тело процедуры Execute:
constructor TSearchThread.Create (OpNum:integer;const Path:string);
begin
{Конструктор вызывается с двумя параметрами:
OpNum — количество уровней вложенности,
Path — путь, с которого необходимо начать обход}
OperationsNum:=OpNum;
StartPath:=Path;
MemDump:=MainForm.memDump;
inherited Create(false);
end;

destructor TSearchThread.Destroy;
begin
inherited Destroy;
end;

procedure TSearchThread.Execute;
begin
FreeOnTerminate:=true;
//По окончании выполнения метода Execute поток будет удален неявно
Priority:=tpNormal;
//Приоритет потока относительно других потоков приложения
FindFiles(StartPath,OperationsNum);
//Выполняем процедуру поиска
end;
Также необходимо внести некоторые изменения в главный модуль проекта. В первую очередь в разделе Uses добавим ThreadRoutines. В private-секцию класса TMainForm добавляем переменную SearchThread: TSearchThread. Из процедуры TMainForm.btnStart Click следует удалить вызов FindFiles и поместить туда вызов конструктора класса TSearchThread. Кроме того, за ненадобностью можно удалить строчки
Screen.Cursor:=crHourGlass;
try
и
finally
Screen.Cursor:=crDefault;
end;
так как при выделении функций просмотра дерева каталогов в отдельный поток нет никакого смысла заменять стандартный курсор мыши "песочными часами". Теперь процедура должна выглядеть так:
procedure TMainForm.btnStartClick (Sender: TObject);
begin
memDump.Clear;
//Очищаем memDump
SearchThread:=TSearchThread.Create(
speLevelsNumber.Value+1, dolFolders.Directory,
memDump);
{И создаем поток, вызывающий процедуру поиска. Параметрами ему передаем число уровней вложения из speLevelsNumber и текущую папку, установленную в dolFolders }
end;
Но для того чтобы использовать все, что было сделано, необходимо где-то останавливать действие потока. Для этой цели у нас уже есть кнопка btnStop. Создадим обработчик нажатия этой кнопки, записав туда всего одну строку кода:
procedure TMainForm.btnStopClick (Sender: TObject);
begin
SearchThread.Terminate;
end;
Метод Terminate не завершает поток непосредственно, а лишь устанавливает в True свойство Terminated. Для того чтобы поток завершился, метод Execute должен постоянно проверять свойство Terminated и завершить свое выполнение, как только то примет значение True. Так как рекурсивный вызов происходит в FindFiles, то именно там и предстоит проводить проверку на равенство истине свойства Terminated. Поэтому в самом начале процедуры (сразу после оператора begin) нужно вставить строчки:
if Terminated then
exit;
В данном случае вновь будет заметно, что структура программы продумана не самым удачным образом: дело в том, что вызов процедуры Exit хоть и прерывает действие подпрограммы, но в случае использования конструкции try..finally выполнение блока try будет прервано, а управление будет передано блоку finally. Соответственно, в этом случае нам придется повторить проверку в самом начале блока finally (т.к. именно из этого блока происходит рекурсивный вызов). Не самая, согласитесь, удачная мысль, производить каждую проверку дважды. Ввиду этого мы просто поместим ключевое слово finally непосредственно перед вызовом FindClose (FSearchRec). Проделав все это, мы получим следующий код тела процедуры FindFiles:
begin
if Terminated then
exit;

Path:=GetDirectoryName(Path);
SubPath:=LeaveJustName(Path);
SubPath:=UpperCase(SubPath);
memDump.Lines.Add(SubPath);

FindResult:=FindFirst(Path+'*.*', faDirectory, DSearchRec);
try
dec(OperationsNum);
if(OperationsNum<=0) then exit;

while FindResult = 0 do
begin
if (((DSearchRec.Attr and faDirectory) = faDirectory)
and not IsProperFolderName (DSearchRec.Name)) then
begin
FindFiles(Path+DSearchRec. Name,OperationsNum);
end;
FindResult:=FindNext (DSearchRec);
end;
FindResult:=FindFirst(Path+ '*.*',faDirectory,DSearchRec);
while FindResult = 0 do
begin
if (((DSearchRec.Attr and faDirectory) = faDirectory)
and (not IsProperFolderName(DSearchRec.Name)) and (Operationsnum=0)) then
begin
SubPath:=DSearchRec.Name;
SubPath:=UpperCase(SubPath);
memDump.Lines.Add(SubPath);
end;
FindResult:=FindNext (DSearchRec);
end;

FindResult:=FindFirst(Path+' *.*',faAnyFile+faHidden+
faSysFile+faReadOnly,FSearch Rec);
while FindResult = 0 do
begin
SubPath:=DeleteExtention (FSearchRec.Name);
memDump.Lines.Add(SubPath);
FindResult:=FindNext (FSearchRec);
end;
memDump.Lines.Add(#10#13);
finally
FindClose(FSearchRec);
FindClose(DSearchRec);
end;
end;
Что же, цель вновь достигнута: теперь приложение можно корректно завершить даже в том случае, когда оно будет поглощено сканированием дерева каталогов. Но при этом в нашей программе осталось три очевидных и довольно грубых ошибки: во-первых, если пользователь случайно нажмет на кнопку btnStart больше одного раза, то с каждым нажатием на выполнение будет запущен еще один поток. Самому приложению это не угрожает: Windows поддерживает отдельный стек для каждого потока, поэтому потоки не смогут "испортить" друг другу данные.
Но проблема состоит в том, что для вывода оба потока будут использовать один и тот же объект TMemo, расположенный на главной форме, так что составленный таким образом список будет безнадежно испорчен. Вторая ошибка состоит в том, что пока дополнительный поток не запущен, любое нажатие на кнопку btnStop будет вызывать ошибку. Все дело в том, что в теле обработчика этой кнопки мы пытаемся присвоить значение свойству несуществующего объекта. И, наконец, третья ошибка заключается в том, что при попытке закрыть главную форму во время работы процедуры FindFiles приложение будет завершено с ошибкой, т.к. эта процедура будет пытаться изменять свойства уже недоступных объектов.
В связи с этим придется принять еще несколько мер. Сначала при помощи Object Inspector установим свойство Enabled кнопки btnStop в False и создадим в классе TMainForm булеву переменную ThreadExecuteing, а также процедуру OnThreadFinish (Sender: TObject).
В процедуру TMainForm.btnStart Click добавим инструкции
ThreadExecuteing:=true;
btnStart.Enabled:=false;
btnStop.Enabled:=true;
а также после создания потока SearchThread строку
SearchThread.OnTerminate:= OnThreadFinish;
В теле OnThreadFinish запишем
procedure TMainForm.OnThreadFinish(Sender: TObject);
begin
ThreadExecuteing:=false;
btnStart.Enabled:=true;
btnStop.Enabled:=false;
end;
Помимо этого, создадим обработчики событий OnCloseQuery и OnCreate главной формы, записав в их тела следующие инструкции:
procedure TMainForm.FormCloseQuery(Sender: TObject; var CanClose: Boolean);
begin
{Если дополнительный поток выполняется, игнорируем попытку закрыть форму}
if(ThreadExecuteing) then CanClose:=false
else CanClose:=true;
end;

procedure TMainForm.FormCreate (Sender: TObject);
begin
ThreadExecuteing:=false;
end;
Тем самым мы добились главного: у нас теперь есть рабочее ядро программы, которое выполняет основное возложенное на нее действие. Дальше все станет гораздо легче, так как останется лишь добавить в программу возможность настраивать ее функциональность.

masm
kgmail@mail.ru



Компьютерная газета. Статья была опубликована в номере 13 за 2002 год в рубрике программирование :: разное

©1997-2024 Компьютерная газета