Обучающий курс. 25. Работа с файлами и каталогами (часть 1)Источник: delphi
ВведениеНесмотря на то, что предыдущий урок был посвящён записям, но тема не была раскрыта полностью, данный урок будет о другом - о том, как работать с файлами и с каталогами. Эти знания нам понадобятся в дальнейшем при работе с записями, поэтому логично рассказать об этом прямо сейчас. Кроме того, вопросы относительно файлов и папок возникают достаточно часто. Вспомним, о чём, собственно, речь...Для обычного пользователя, который не занимается программированием, знать о файлах и папках много не нужно. Достаточно иметь базовые представления, что это такое, как это используется, и какие возможны операции. Однако при программировании необходимо понимание того, что находится "внутри" и как всё это работает. Текстовая обработка: пути и имена файловДля начала давайте узнаем, где мы вообще находимся. Я имею ввиду полный путь к исполняемому файлу нашей программы. Совершенно логично, что спрашивать нужно у самого приложения, т.е. у объекта Application. Свойство называется ExeName. Не обращайте внимания, что по названию это имя файла - вовсе нет, это самый полный путь к нему. Edit1.Text:=Application.ExeName; В качестве результата будет например следующее: C:\Documents and Settings\Андрей\Мои документы\RAD Studio\Projects\Project1.exe. Уже неплохо, согласитесь? Ориентация в пространстве - одна из важных функций. Есть и другой способ получить ту же самую информацию - функция ParamStr() с параметром 0. Edit1.Text:=ParamStr(0); Если никогда не слышали о командных параметрах - не волнуйтесь. Всему своё время. Просто используйте первый вариант. Идём дальше. Что нам ещё нужно? Да много чего на самом деле! Например, мы хотим узнать-таки имя исполняемого файла нашей программы. Да, Вы знаете, под каким именем сохранён Ваш проект - именно так и называется исполняемый файл, но только расширение другое - exe. Однако кто сказал, что этот файл будет называться именно так всю свою жизнь? Например, в дистрибутив программа может быть включена совершенно под другим именем. Вывод: "жёстко" указывать имя в коде не пойдёт - будем брать его динамически. Откуда его можно взять? Да вот из пути, который мы только что получили! Нам поможет функция, название которой именно это и означает: "извлечь имя файла" - ExtractFileName(). Единственный параметр - путь к файлу. Итак, пробуем: Edit1.Text:=ExtractFileName(Application.ExeName); Ага, Project1.exe. Ловкость рук и никакого мошенничества. А Вы, уже было, приготовились открывать урок про работу со строками, и писать цикл по строке? :-) На самом деле, эта функция так и работает, но код уже написали давным-давно за нас. Следующий логичный вопрос. Имя файла знаем, а как бы нам узнать только путь? Правильно, всё очень просто: "извлечь путь к файлу" - ExtractFilePath(). Параметр - снова путь: Edit1.Text:=ExtractFilePath(Application.ExeName); Получили C:\Documents and Settings\Андрей\Мои документы\RAD Studio\Projects\. Что ещё для счастья нужно? Казалось бы, ничего? Ан-нет, во многих случаях в таком виде путь неприемлем. Слеш в конце лишний. Можно его удалить вручную, а можно использовать другую функцию - ExtractFileDir(). Всё то же самое, только она вернёт нам путь без слеша на конце: Edit1.Text:=ExtractFileDir(Application.ExeName); Результат: C:\Documents and Settings\Андрей\Мои документы\RAD Studio\Projects. У Вас наверняка есть своё рабочее место - часть комнаты дома или в офисе: стол с компьютером, кресло. Есть ещё и виртуальный рабочий стол. Программы в этом плане не обделены - у них тоже есть своё рабочее место, которое называется рабочим каталогом. Что это такое? Это та папка, которую программа считает текущей. Т.е. если никуда не ходить, то мы будем работать прямо в этой папке. Что это даёт? Это позволяет обращаться к этому уголку дискового пространства напрямую: вместо указания полного пути к файлу - только его имя. Хотим перейти в подкаталог - пожалуйста, скажите только его имя. По умолчанию рабочим каталогом программы считается тот, в котором расположен её исполняемый файл. Узнать текущий каталог можно функцией GetCurrentDir() - как всегда, логичное название ("получить текущую директорию"): Edit1.Text:=GetCurrentDir(); В моём случае это C:\Documents and Settings\Андрей\Мои документы\RAD Studio\Projects. К примеру, если в этой папке создать файл new.txt, то к нему можно будет обратиться прямо new.txt, а не писать полный путь C:\Documents and Settings\Андрей\Мои документы\RAD Studio\Projects\new.txt. Если есть подкаталог Data, то зайти в него - просто Data\ вместо длинного C:\Documents and Settings\Андрей\Мои документы\RAD Studio\Projects\Data\. Предчувствую вопрос: а как перейти на уровень выше? Предыдущий уровень обозначается двумя точками, поэтому "..\" - это C:\Documents and Settings\Андрей\Мои документы\RAD Studio. Ну а повторив такой переход несколько раз, придём в корень диска C:. Не лишним будет также сказать, что есть и обозначение текущего каталога - одна точка ("./"). Позже, когда мы попробуем получить список файлов, эти тонкости всплывут. Ну а теперь главное. Запомните, что рабочий каталог программы вовсе не обязан быть именно в папке с exe-файлом. Этот каталог можно легко изменить - если заглянуть в свойства ярлыка к любой программе, там есть поле "Рабочая папка". Изменение этого пути приведёт к смене рабочего каталога. К чему это ведёт, надеюсь понятно: программа начнёт обращаться к несуществующим данным, сохранять всё не там, где нужно - бардак в общем. Это одна из частых ошибок, о которой забывают, а потом не могут понять, откуда столько проблем. Как изменить рабочий каталог? Думаю, сами догадались - функцией SetCurrentDir(). Пример: SetCurrentDir('../../../'); Edit1.Text:=GetCurrentDir(); Здесь мы поднялись на три уровня вверх. Теперь наш рабочий каталог - C:\Documents and Settings\Андрей. Аналог этой функции - ChDir(). Вы всегда можете проверить свои догадки относительно того пути, куда попадёте, в любом файловом менеджере или в оболочке - в том же Проводнике. Так, например, путь C:\Documents and Settings\Андрей\Мои документы\RAD Studio\Projects\..\..\RAD Studio\ вполне законный и ничем не отличается от других. Имена файлов и папок можно обрабатывать и изменять так, как Вам требуется. Однако есть несколько полезных функций, которые сильно облегчают работу. Функция ExtractFileExt() позволяет извлечь расширение файла. Ext - сокращение от англ. extension - расширение. Обратите внимание, что функция возвращает расширение с первым символом точкой: Edit1.Text:=ExtractFileExt(Application.ExeName); Этот код выдаст ".exe". Помимо извлечения расширения часто требуется его изменить. Например, если нужно пересохранить файл в другой формат, изменится именно его расширение, а имя останется прежним. Есть и такая функция - ChangeFileExt(). Первый параметр - строка с именем или путём к файлу, второй - новое расширение. Пример: Edit1.Text:=ChangeFileExt(Application.ExeName,'.txt'); В данном случае получим C:\Documents and Settings\Андрей\Мои документы\RAD Studio\Projects\Project1.txt. Пожалуй, об основных моментах побеседовали. Пока что мы работали только с именами и путями. Теперь перейдём непосредственно к работе с файлами. Есть ли, с чем работать?Пожалуй, одно из самых частых действий - проверка, а есть ли вообще такой файл на диске? Действительно, от этого многое зависти. Если его нет, а требуется записать данные, то файл нужно создать. А если он есть, то надо решить, что делать с его прежним содержимым. Проверка существования файла выполняется функцией FileExists() - "файл существует?". Возвращаемое значение - логическое (Boolean). if FileExists('new.txt') then ShowMessage('Файл существует.') else ShowMessage('Файл отсутствует.'); Путь к файлу может быть как абсолютным (полный путь), так и относительным (от рабочего каталога). Как видите, всё элементарно. А как узнать, есть ли папка? Да всё то же самое, только вместо File - Directory: DirectoryExists(): if DirectoryExists('Data') then ShowMessage('Папка существует.') else ShowMessage('Папки нет.');Файлы и папки - понятия разные, поэтому и функии для проверки разные. Вполне может существовать папка new.txt и файл Data (без расширения). О том, что все так любят делать...Догадались, о чём пойдёт речь? Да-да, именно об удалении. Сломать проще, чем построить. Однако удалять ненужное - благое дело. С удалением всё не так просто, как с проверкой существования. Когда Вы пытаетесь удалить файл, какая-нибудь программа может его использовать. Естественно, файловая система удалить его не позволит. if DeleteFile('text.doc') then ShowMessage('Файл удален.') else ShowMessage('Не удалось удалить файл.'); Для удаления папок используется функция RemoveDir(): if RemoveDir('Data') then ShowMessage('Папка удалена.') else ShowMessage('Не удалось удалить папку.'); Есть и альтернатива - процедура RmDir(), однако об успешности удаления она не сообщает. О том, как создавать папкиПришли к главному - как создавать папки и файлы, как записывать в них информацию и читать её оттуда. Начнём с того, как создавать папки. Здесь всё достаточно просто. Создать папку позволяет функция CreateDir() и процедура MkDir(). Аналогично удалению, первая может сказать о том, получилось ли создать, а вторая лишь молча попробует сделать своё дело. CreateDir('Documents'); Есть и более интересная модификация - функция ForceDirectories(). Она позволяет создать сразу целую цепочку вложенных друг в друга папок. Например, мы хотим создать папку Files, в ней Documents, а в последней - Срочное. Вариант с использованием MkDir(): MkDir('Files'); MkDir('Files\Documents'); MkDir('Files\Documents\Срочное'); Неудобно, согласитесь? И это только 3 папки. А бывают случаи, когда нужны цепочки и длиннее... Функция ForceDirectories() автоматически создаёт всё недостающее по указанному пути. Одна особенность - функция требует на вход полного (абсолютного) пути. Для нашего примера: ForceDirectories(GetCurrentDir()+'\Files\Documents\Срочное');В этом случае получаем C:\Documents and Settings\Андрей\Мои документы\RAD Studio\Projects\Files\Documents\Срочное. О том, как создавать файлы и работать с их содержимымС файлами не всё так просто, как с папками. Это очевидно хотя бы из того, что папка - это просто оболочка с именем. У файла же есть ещё и содержимое. Прежде чем рассказать о механизме работы с файлами, нужно ввести одно важное понятие. Под указателем в программирование понимается некоторая ссылка, которая может однозначно привести нас к какому-либо объекту. Указатели бывают разные. Например, указатель на ячейку памяти, в которой хранятся какие-либо данные. В этом случае указатель хранит адрес этой ячейки. Указатели существуют во многих операциях, без них доступ к данным невозможен. В реальной жизни указатель можно сравнить с адресом дома, с номером комнаты. Если нам известно, куда идти, то мы туда придём и сделаем то, что нужно. Если указатель (адрес) потерян, найти место проблематично, а то и вовсе невозможно. При работе с файлами тоже используются своего рода указатели. Дело в том, что работа с файлом делится на несколько этапов. Сначала файл нужно открыть. Без этого нельзя ни прочитать из него данные, ни записать их в него. Когда файл открыт, с его содержимым можно выполнять требуемые действия. По окочании работы файл нужно закрыть. Железное правило: не забывайте закрывать открытые файлы! Весь процесс можно увидеть и в реальной жизни: открыл дверь, зашёл, что-то сделал, вышел, закрыл дверь. Если файл не закрыть, можно его заблокировать: система будет думать, что он занят программой, однако программа уже завершила работу и потеряла указатель на него. Результат: открыть файл уже не получится. Лечится такая ситуация либо перезагрузкой системы, либо использованием специальных программ типа File Unlocker, которые восстанавливают доступ к занятым файлам. А теперь начнём разбираться, как работать с файлами. 1. Объявление указателя. Чтобы работать с файлом, нам нужен указатель на него. По сути, это обычная переменная, только специального типа данных. Описываются указатели следующим образом: var имя_указателя: File of тип_данных; В качестве типа данных задаётся тот тип, переменную которого можно считать одной записью в файле. Это означает, что Вы изначально подразумеваете файл разбитым на некоторое количество записей. Именно с каждой такой записью будут выполняться все операции: добавление записи в файл или удаление записи из него. Обычный текстовый файл - это набор байтов: каждый символ кодируется один байтом (не принимая в расчёт файлы, содержащие текст в формате Юникода), поэтому текстовый файл описывается так: var имя_указателя: File of Byte; Вместо Byte можно написать Char. С последним работать удобнее, если в файле записан текст. Поскольку чаще всего приходится работать именно с текстовыми файлами, был введён отдельный тип данных - TextFile: var F: TextFile; Указатели чаще всего называют буквой F (file). Итак, указатель есть. Переходим к следующему шагу. 2. Установка связи с файлом. Этот этап можно сравнить с поиском двери нужного Вам номера. Связь устанавливается процедурой AssignFile(): AssignFile(указатель, 'путь к файлу'); Первый параметр - указатель, который мы объявили в п.1. Путь к файлу может как абсолютным, так и относительным. После этой операции мы готовы к открытию файла. 3. Открытие файла. Открытие - не такая простая операция. Она может выполняться в трёх разных режимах. Rewrite - перезапись файла (в т.ч. создание несуществующего файла). Подразумевает очистку содержимого файла и установку текущей позиции (её тоже называют указателем) в начало файла. Reset - открытие файла только для чтения. Указатель устанавливается в начало файла. Append - добавление. Используется для записи данных в конец файла, не затрагивая существующие данные. Указатель ставится в конец. Что за указатель тут используется? Это не тот, который даёт нам доступ к файлу. Представьте внутреннее содержимое файла как текст. В текстовом редакторе есть курсор, который находится в какой-то конкретной позиции. С файлом то же самое: есть невидимый курсор, посредством которого можно работать с содержимым. Если требуется записать данные в файл, курсор ставится в нужную позицию и далее выполняется запись. Если нужно прочитать - перемещаемся в нужное место и, шаг за шагом, считываем нужные данные. Указатель при этом каждый раз движется вперёд. Рано или поздно он достигнет конца файла. Функции вроде OpenFile нет - вместо неё есть 3 функции с названиями, приведёнными выше: Rewrite(), Reset(), Append(). Единственный обязательный параметр всех этих функций - указатель на наш файл, причём тот, с помощью которого уже установлена связь с помощью AssignFile(). 4. Закрытие файла. Закрытие выполняется помощью CloseFile() передачей переменной-указателя на файл. Вот и основные сведения о том, как работать с файлами. При работе следует заботиться обо всём, что может произойти. Так, например, если попытаться открыть файл для чтения функцией Reset(), которого не существует, произойдёт ошибка. Пример 1. Создадим пустой текстовый файл. var F: TextFile; AssignFile(F,'new.txt'); Rewrite(F); CloseFile(F);Мы объявили переменную F типа "текстовый файл", далее привязали её к файлу new.txt, сделали перезапись файла и закрыли его. В итоге получен пустой файл. Набор из трёх команд необходимо выполнять при работе с любым файлом. Чтение и записьДля чтения и записи существуют процедуры с понятными именами Read() и Write(). Процедуры необычые: у них неограниченное число параметров. У обеих первым параметров передаётся указатель на файл, а дальше - список переменных (или значений), с которыми нужно выполнить данную операцию. Команда Read() читает одну запись из файла и заносит её в переменную, которая указана вторым параметром. При этом указатель сдвигается на одну позицию. Если указать несколько переменных - в них будут считаны значения по порядку. Функция автоматически распознаёт разделители значений - пробелы, что позволяет с лёгкостью считывать несколько значений из файла. Запись работает аналогично: все переданные значения последовательно записываются в файл. Помните, что пока Вы не закрыли файл, внесённые изменения в нём не сохраняются! Пример 2. Запись в файл. var F: TextFile; a,b: Byte; AssignFile(F,'new.txt'); Rewrite(F); a:=2; b:=10; Write(F,'1',a,'3 ',b); CloseFile(F); В данном примере показано использование Write(). В результате в файле будет строка "123 10". Были записаны как явно заданные строки, так и значения переменных. Пример 3. Считывание из файла двух чисел. var F: TextFile; a,b: Byte; AssignFile(F,'new.txt'); Reset(F); Read(F,a,b); CloseFile(F); ShowMessage(IntToStr(a)+' ; '+IntToStr(b)); В данном примере файл открывается для чтения (Reset) и из него считываются записанные там значения в переменные a и b. На экран будет выведено сообщение "123 ; 10". Процедуры Read() и Write() не проверяют типов данных, которые используются при работе с файлом. Так что можно напрямую записывать числа, не преобразуя их в строки, и обратно из строк в числа при чтении. Итак, записывать и читать данные научились. Но хватит ли этого для решения любых задач? Нет. Гораздо чаще требуется работать с файлами построчно, т.е. считывать и записывать целые строки. Реакция функций на пробелы-разделители будет только вставлять палки в колёса. На помощь приходят функции ReadLn() и WriteLn(). Ln - сокращение от line (строка). ReadLn() читает целую строку из файла, а WriteLn() записывает данные и вставляет после них признаки конца текущей строки и начала новой. В остальном работа этих функций точно такая же. Когда выполняется чтение файла, указазатель ("курсор") перемещается автоматически. Понятно, что в какой-то момент мы придём к концу файла и дальше читать уже будет нечего. Чтобы отследить это состояние, используется функция EOF() - сокращение от end of file (конец файла). Передаём указатель на файл и узнаём, дошли ли до конца. Что может быть проще? Таким образом, чтение файла легко записывается в цикл. Пример 4. Считывание всего файла в ListBox. var F: TextFile; S: String; ListBox1.Clear; AssignFile(F,'new.txt'); Reset(F); while not EOF(F) do begin ReadLn(F,S); ListBox1.Items.Add(S) end; CloseFile(F); Разберём пример построчно:
Механизм простой, и, надеюсь понятный. Пример 5. Сохранение всех строк из поля Memo в текстовый файл. var F: TextFile; I: Integer; AssignFile(F,'test.txt'); Rewrite(F); for I := 0 to Memo1.Lines.Count-1 do WriteLn(F,Memo1.Lines[I]); CloseFile(F);
Считаю необходимым напомнить, что операции сохранения и загрузки из файла в Memo и ListBox предусмотрены. Эквиваленты для данных примеров: ListBox1.Items.LoadFromFile('new.txt'); Memo1.Lines.SaveToFile('test.txt'); ЗаключениеМы рассмотрели работу с файлами и каталогами. Научились работать с путями и именами файлов, а также узнали, как записывать информацию в файлы, и как считывать её оттуда. Конечно, это далеко не всё. Ещё множество вопросов осталось за кадром. Как определить объём файла? Как скопировать, переместить, переименовать файл? Речь об этом пойдёт во второй части данной темы, однако в следующем уроке мы вернёмся к работе с записями. Домашнее заданиеДля усвоения пройденного предлагаю создать следующую программу. На форме два поля для ввода текста. В первое вводится путь к некоторой папке, а во второе - текстовая строка, содержащая символ "*" (звёздочка). Дополнительно (с помощью каких-либо компонентов) вводятся два числа. Требуется в указанной папке создать текстовые файлы с именами из второго поля, заменив звёздочку на числа из заданного диапазона. При этом, если такой файл уже был на диске, нужно дописать в него текущее время, а если его не было - то и дату, и время. Подсказка: текущее время в виде строки можно получить как TimeToStr(Now), а дату - DateToStr(Now). Примерный вид окна программы приведён на рисунке справа. Для указанных исходных данных на диске C:\ будут созданы файлы Test1.txt, Test2.txt, ..., Test5.txt. |