Моя первая программа

Моя первая программа Уже достаточно давно на страницах "Компьютерной газеты" прижились учебные рубрики о программировании на Delphi. По своей сути, эти публикации представляют собой целый учебник, в котором подробно, с примерами использования описываются все возможности языка Object Pascal и среды разработки Delphi. Учитывая, что рубрика о Delphi весьма популярна среди наших читателей, надеюсь, что этот материал также будет для них небезынтересным. Ну, что же, предположим, что статьи о языке Pascal уже изучены. Теперь стоит приложить полученные знания к чему-то по-настоящему нужному. Настало время написать свою Первую программу.

Можно ожидать, что все, о чем пойдет речь в этой статье, будет интересно прежде всего тем читателям, кто только начинает осваивать программирование в среде Delphi. То, что я предлагаю сделать, может, и не является чем-то сверхъестественным для программистов со стажем, но может быть весьма полезным для новичков. Не секрет, что у начинающих программистов часто возникает проблема применения имеющихся знаний на практике. Все основные типы данных и библиотечные функции уже изучены, а связать их с бегущим монстром из Quake или с мчащейся по трассе машиной из Need For Speed никак не получается. К тому же, если не закреплять полученные знания на практике, то со временем они забываются.
Сегодня мы попытаемся заняться этой самой практикой. Конечно, игрушек писать мы не станем, по крайней мере, пока. Давайте лучше зададимся целью создать какую-нибудь по-настоящему полезную программу, чтобы та к тому же не имела очень известных аналогов, иначе какой вообще тогда смысл садиться за ее создание? Программу будем писать "под себя", но не будем также забывать о том, что, возможно, ею захотят воспользоваться и другие, например друзья.
То, что программу мы пишем для себя и тем более то, что эта программа у нас первая, накладывает определенный отпечаток сумбурности на процесс ее написания. Первое обстоятельство заставит нас поступиться качеством разработки пользовательского интерфейса и структуры программы в угоду скорейшего получения результата, а второе не позволит нам все сделать правильно с самого начала. Что же, тогда будем создавать и переделывать по ходу, учась на собственных ошибках. Соглашусь, это несколько не по-газетному — писать программы, а потом их модифицировать, но, с другой стороны, так довольно часто бывает и в жизни. Итак, в путь!

Идея
Допустим, что мы приобрели CD-RW-привод. Замечу, что наличие у читателя пишущего CD-привода ни в коем случае не является необходимым условием для понимания тех вещей, о которых пойдет речь в этой статье. Итак, день за днем, мегабайт за мегабайтом, мы начинаем скидывать содержимое своего жесткого диска на матрицы. Но для порядка было бы совсем неплохо оснастить каждый диск распечаткой с информацией о его содержимом. Скажем, чтобы для диска с софтом имелся список всех программ с номерами их версий и желательно чтобы при этом все программы были тематически разделены на группы (например, антивирусы, архиваторы и т.д.). Для дисков с музыкой MP3 нужно, чтобы структура распечатки была представлена в виде: Группа/Альбом/Композиция. Ничего особенного нам не нужно: для составления распечатки вполне подойдет и MS Word. Только неужели придется самому вводить с клавиатуры названия всех программ, разбивать их на группы, тем более, что вся необходимая информация, как правило, уже содержится в именах файлов и папок. Таким образом, мы ставим перед собой задачу написания программы, которая позволяла бы сканировать файловую систему с заданным уровнем вложенности и выводить информацию о найденных файлах и папках в текстовый файл, которым мы будем вольны распорядиться по собственному усмотрению в дальнейшем.

Форма
Для начала запустим Delphi. Среда автоматически создаст новый проект. Очевидно, что выбор папки, из которой следует начинать поиск, нужно предоставить пользователю. Таким образом, нам понадобится какое-то средство навигации по файловой системе. Для этих нужд вполне подойдут компоненты TDriveComboBox и TDirectoryListBox с закладки Win 3.1 палитры компонент. Они, конечно, не самые красивые, но работать будут. Недолго раздумывая, тащим их на форму. Также для начала нам понадобится компонента TMemo, одна метка TLabel, три кнопки TButton, диалог сохранения файла TSaveDialog и компонента TSpinEdit с закладки Samples. С этим арсеналом уже вполне можно начинать сражаться. Проделав все операции по перетаскиванию компонентов на форму, сохраним проект, присвоив ему какое-нибудь осмысленное имя, например, CD-R.
Попробуем сразу навести порядок во всем нашем хозяйстве. Раз уж мы решили писать настоящую программу, условимся, что мы больше не будем пользоваться стандартными именами, вроде Edit1, а заменим их таким образом, чтобы имя говорило о типе объекта и о его функциональной нагрузке в программе. Заменим имя формы новым именем MainForm, Memo1 поменяем на memDump, Label — на lblLeves, кнопки назовем btnStart, btnSaveToFile и btnSaveToBuffer. В данном случае для присвоения имен элементам формы применяется негласное правило, чем-то напоминающее "венгерскую" нотацию, широко известную благодаря стараниям корпорации Microsoft. Сначала в имени идет префикс, указывающий на тип используемого компонента, затем следует имя переменной, объясняющее ее назначение, причем каждое слово начинается с большой буквы. Как известно, Delphi, в отличие от C++, не различает регистров написания, однако для лучшей читабельности кода этих рекомендаций следует придерживаться. Теперь из имени SaveDialog1 удалим единичку, DriveComboBox1 и DirectoryListBox1 назовем dcbDrives и dolFolders соответственно. Именем для SpinEdit1 сделаем speLevelsNumber. Теперь можно быть уверенным, что код программы будет читабельным в любом месте, к тому же не придется постоянно лазить в секцию объявления переменных, чтобы узнать их тип.
Все уже почти готово, но до того, как мы напишем первую строчку кода, еще несколько штрихов: в меню Project\Options в графе имени приложения вводим CD-R Cover Helper. Также при помощи Object Inspector очищаем содержимое memDump и устанавливаем свойство Align в alBottom, свойство ReadOnly — в true, а свойство ScrollBars — в ssVertical; отключаем у главной формы кнопку "развернуть": BorderIcons.biMaximize:=false. У компоненты SaveDialog свойство FileName установим равным CD-R, и, наконец, свойству DefaultExt присвоим значение .txt. К тому же установим в true опцию ofOwerwritePrompt. Назначение этих свойств следует из их названий и в дополнительных пояснениях не нуждается. По окончании всех этих манипуляций должно получиться нечто похожее на рис. 1.

Код приложения
Создаем обработчик события OnChange для dcbDrives со следующим кодом:

procedure TMainForm.dcbDrives Change(Sender: TObject);
begin
dolFolders.Drive:=dcbDrives.Drive;
end;

Данная процедура содержит лишь одну инструкцию, которая меняет свойство Drive у объекта dolFolders. Далее создаем обработчики на нажатие кнопок. Все инструкции подробно описаны в комментариях.

procedure TMainForm.btnStart Click(Sender: TObject);
//Эта процедура непосредственно запускает процедуру обхода дерева файлов
begin
Screen.Cursor:=crHourGlass;
//На время выполнения заменяем значок курсора значком Busy
try
memDump.Clear;
//Очищаем memDump
FindFiles(dolFolders.Directory,speLevelsNumber.Value);
{И запускаем процедуру поиска. Параметрами ей передаем текущую папку, установленную в dolFolders, и число уровней вложения из speLevelsNumber}
finally
Screen.Cursor:=crDefault;
//Возвращаем состояние курсора в NormalSelect
end;
end;
procedure TMainForm.btnSaveToFile Click(Sender: TObject);
//Сохраняем содержимое memDump в файл
var
FileName: TextFile;
i: integer;
begin
//Если SaveDialog вернул имя файла
if (SaveDialog.Execute) then
begin
AssignFile(FileName, SaveDialog.FileName);
//Переписываем его содержимое
Rewrite(FileName);
for i:=1 to memDump.Lines.Count do
//Построчно записываем в файл содержимое memDump
Writeln(FileName, memDump.Lines[i]);
//И закрываем файл
CloseFile(FileName);
end;
end;

procedure TMainForm.btnSaveToBufferClick(Sender: TObject);
//Копируем содержимое memDump в буфер обмена
begin
//Выделяем содержимое memDump
memDump.SelectAll;
//И копируем его в буфер
memDump.CopyToClipboard;
end;

Теперь нам осталось лишь описать процедуру поиска FindFiles со всеми необходимыми для ее работы функциями — и поставленная цель будет достигнута. В private-секцию описания класса TMainForm добавляем прототипы следующих функций:

private
{ Private declarations }
procedure FindFiles(Path: String; OperationsNum: integer);
function GetDirectoryName(Dir: String): String;
function LeaveJustName(Path: String): String;
function DeleteExtention(Path: String): String;
И, наконец, пишем тела этих функций.
function TMainForm.GetDirectoryName(Dir: String): String;
//Составляем валидное имя для директории
//теперь оно заканчивается бэк-
слешем (\)
begin
if (Dir[Length(Dir)]<> '\') then
Result:=Dir+'\'
else
Result:=Dir;
end;

function TMainForm.LeaveJustName(Path: String): String;
//Убираем полный путь
//оставляем только имя файла или папки
var
no: integer;
begin
if (Path[Length(Path)]= '\') then
Path:=Copy(Path,1,Length(path)-1);

no:=Pos('\',Path);
while(no<> 0) do
begin
Path:=Copy(Path,no+1,Length(Path)-no);
no:=Pos('\',Path);
end;
Result:=Path;
end;

function TMainForm.DeleteExtention(Path: String): String;
//Удаляем расширение
//оставляем только имя файла
var
SubPath: String;
SubPath2: String;
no,no1: integer;
begin
no:=Pos('.',Path);
no1:=no;
if (no=0) then
Result:=Path
else
begin
SubPath2:=Path;
while(no<> 0) do
begin
SubPath:=Copy(Path,1,no1-1);
SubPath2:=Copy(SubPath2, no+1,Length(Path)-no);
no:=Pos('.',SubPath2);
no1:=no1+no;
end;
Result:=SubPath;
end
end;

procedure TMainForm.FindFiles (Path: String; OperationsNum: integer);
//Рекурсивная процедура поиска всех файлов и папок
var
FSearchRec,
DSearchRec: TSearchRec;
{Тип TSearchRec представляет собой структуру, заполняемую функциями поиска файлов FindFirst и FindNex}
FindResult: integer;
SubPath: String;

function IsProperFolderName (FolderName: String): Boolean;
{Вложенная функция, проверяющая имя папки на равенство '.' или '..'}
begin
Result:=(FolderName = '.') or (FolderName = '..');
end;

begin
{Получаем имя папки, из которой начинается поиск, и записываем его в memDump}
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;

{Для каждой найденной подпапки рекурсивно вызываем процедуру FindFiles и продолжаем поиск}
while FindResult = 0 do
begin
if (((DSearchRec.Attr and faDirectory) = faDirectory)
and not IsProperFolderName (DSearchRec.Name)) then
// Здесь происходит рекурсивный вызов
FindFiles(Path+DSearch Rec.Name,OperationsNum);
FindResult:=FindNext (DSearchRec);
end;
finally
{На выходе из рекурсии добавляем в memDump сначала имена всех подпапок, входящих в данную папку, а затем и имена всех файлов}
if (OperationsNum> =0) then
begin
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, FSearchRec);
while FindResult = 0 do
begin
SubPath:=DeleteExtention (FSearchRec.Name);
memDump.Lines.Add(SubPath);
FindResult:=FindNext (FSearchRec);
end;
memDump.Lines.Add(#10#13);
end;{if (OperationsNum> =0)}
//Освобождаем ресурсы, зарезервированные вызовом FindFirst
FindClose(FSearchRec);
FindClose(DSearchRec);
end;{finally}
end;

Все, основной алгоритм на этом готов. Но для порядка добавим еще обработчик на событие OnExit объекта speLevelsNumber. Эта процедура будет информировать пользователя о неверных значениях, присвоенных объекту.

procedure TMainForm.speLevelsNumberExit(Sender: TObject);
begin
if((speLevelsNumber.Value> 99) or (speLevelsNumber.Value<1)) then
ShowMessage('Неверное значение параметра');
end;


Победа?
Теперь осталось запустить программу на выполнение и убедиться в ее работоспособности. То, что получилось у меня, можно видеть на втором рисунке. Применив CD-R Cover Helper к одному из дисков Домашней коллекции, я получил полную распечатку исполнителей, альбомов и песен, записанных на этом диске.
Что же, можно праздновать победу: мы поставили перед собой задачу и добились своего. У нас теперь есть программа, удовлетворяющая всем первоначальным требованиям. Но окончательная ли это победа? Не совсем. Дело в том, что для современных приложений, помимо функциональности, важны также настраиваемость, удобство и дружественность интерфейса, стабильность, доступная и продуманная справочная система и многое-многое другое. И если над внешним видом компонентов можно потрудиться при помощи ObjectInspector, то для добавления настроек уже придется писать дополнительный код. Но, что еще более важно, мы совсем упустили из виду один существенный момент.
Попробуйте применить нашу программу, например, к корневому каталогу диска C:, указав в поле для ввода уровней вложения число 10. В зависимости от числа файлов на диске C: программа будет работать либо долго, либо очень долго. Хотелось бы остановить поиск без аварийного завершения программы при помощи Менеджера задач, но это неосуществимо, т.к. мы не предусмотрели возможности остановки действия функции поиска. Кто же согласится пользоваться такой программой? Что же, значит, работа разработчика еще не закончена, по сути, она, наоборот, только начинается.
В следующий раз мы попробуем решить хотя бы некоторые из вышеназванных проблем, делая еще один шаг в сторону создания нашего первого настоящего приложения.

masm, kgmail@mail.ru



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

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