TextFile — Тип Delphi


TextFile — Тип Delphi

Технология работы с файлами в системе Delphi требует определённого порядка действий:

  1. Прежде всего файл должен быть открыт. Система следит, чтобы другие приложения не мешали работе с файлом. При этом определяется, в каком режиме открывается файл — для изменения или только считывания информации. После открытия файла в программу возвращается его идентификатор, который будет использоваться для указания на этот файл во всех процедурах обработки.
  2. Начинается работа с файлом. Это могут быть запись, считывание, поиск и другие операции.
  3. Файл закрывается. Теперь он опять доступен другим приложениям без ограничений. Закрытие файла гарантирует, что все внесённые изменения будут сохранены, так как для увеличения скорости работы изменения предварительно сохраняются в специальных буферах операционной системы.

В Delphi реализовано несколько способов работы с файлами. Познакомимся со классическим способом, связанным с использованием файловых переменных. Файловая переменная вводится для указания на файл. Делается это с помощью ключевого слова File :

Описанная таким образом файловая переменная считается нетипизированной, и позволяет работать с файлами с неизвестной структурой. Данные считываются и записываются побайтно блоками, размер которых указывается при открытии файла, вплоть от 1 байт.

Но чаще используются файлы, состоящие из последовательности одинаковых записей. Для описания такого файла к предыдущему описанию добавляется указание типа записи:

В качестве типа могут использоваться базовае типы, или создаваться свои. Важно только, чтобы для типа был точно известен фиксированный размер в байтах, поэтому, например, тип String в чистом виде применяться не может, а только в виде String[N], как указывалось в уроке Delphi 5.

Данные, считанные из файла или записываемые в файл, содержатся в обычной переменной, которая должна быть того же типа, что и файловая. Поэтому сначала в программе лично я описываю нужный тип, а затем ввожу две переменные этого типа — файловую и обычную:

Для текстовых файлов отдельно укажу, что тип файловой переменной в этом случае TextFile, а тип обычной — String.

Для открытия файла нужно указать, где он расположен. Для этого файловая переменная должна быть ассоциирована с нужным файлом, который определяется его адресом. Адрес файла может быть абсолютным, с указанием диска и каталогов (‘C:\Мои документы\Мои рисунки\FileName.ini’), или относительным, тогда он создаётся в папке с .exe файлом программы. Для задания относительного адреса достаточно указать имя файла с нужным расширением. Делается это оператором AssignFile :

AssignFile(SaveF, ‘C:\Мои документы\Мои рисунки\FileName.ini’);
AssignFile(SaveF, ‘FileName.ini’);

Теперь файл должен быть открыт.
Открытие файла оператором Rewrite приведёт воссозданию файла заново, т.е. существующий файл будет без предупреждения уничтожен, и на его месте будет создан новый пустой файл заданного типа, готовый к записи данных. Если же файла не было, то он будет создан.
Открытие файла оператором Reset откроет существующий файл к считыванию или записи данных, и его указатель будет установлен на начало файла :

Каждый из этих операторов может иметь второй необязательный параметр, имеющий смысл для нетипизированных файлов, и указывающий длину записи нетипизированного файла в байтах:

Rewrite(SaveF, 1);
Reset(SaveF, 1);

Чтение файла производится оператором Read :

Запись в файл производится оператором Write :

При этом чтение и запись производится с текущей позиции указателя, затем указатель устанавливается на следующую запись. Можно проверить, существует ли нужный файл, оператором FileExists :

if FileExists(‘FileName.ini’)
then Read(SaveF, SaveV);

Принудительно установить указатель на нужную запись можно оператором Seek(SaveF, N), где N — номер нужной записи, который, как и почти всё в программировании, отсчитывается от нуля:

Seek(SaveF, 49); — установка указателя на 50-ю запись.

При последовательном чтении из файла рано или поздно будет достигнут конец файла, и при дальнейшем чтении произойдёт ошибка. Проверить, не достигнут ли конец файла, можно оператором EOF (аббревиатура End Of File), который равен true, если прочитана последняя запись и указатель находится в конце файла:

while (not EOF(SaveF)) do
Read(SaveF, SaveV);

Для текстовых файлов вместо Read и Write используются операторы Readln и Writeln, умеющие определять конец строки. приведена процедура чтения текстового файла.

Оператор Truncate(SaveF) позволяет отсечь (стереть или, если хотите, удалить!) все записи файла, начиная от текущей позиции указателя, и до конца файла.

В конце работы с файлом его необходимо закрыть. Это делается оператором CloseFile(SaveF) ;

Теперь можно изменить программу из первой части урока, запоминающую своё положение на экране. Описание переменных опускаю, оно приведено на рисунке выше.

Создаём обработчик события Формы OnCreate со следующим содержимым:

procedure TForm1.FormCreate(Sender: TObject) ;
begin
AssignFile(SaveF, ‘Init.ini’) ;
if FileExists(‘Init.ini’) then
begin
Reset(SaveF) ;
Read(SaveF, SaveV) ;
Form1.Left := SaveV.X ;
Form1.Top := SaveV.Y ;
Form1.Caption:=SaveV.Caption ; //Наши переменные дополнительно сохраняют заголовок Формы!
end ;
end ;

Теперь необходимо создать обработчик события OnClose :

procedure TForm1.FormClose(Sender: TObject; var Action: TCloseAction) ;
begin
Rewrite(SaveF) ; //Нет необходимости проверять наличие файла, создадим его заново!
SaveV.X := Form1.Left ;
SaveV.Y := Form1.Top ;
SaveV.Caption := Form1.Caption ;
Write(SaveF, SaveV) ;
CloseFile(SaveF) ;
end ;

В данном случае файл считывается и записывается туда, куда мы ему указали. Но необходимо также уметь выбрать нужный файл в работающей программе. Описание этого — в заключительной части урока.

TextFile — Тип Delphi

Размер в байтах или количество строк? Для первого существует функция FileSize (подробнее см. help файл Delphi), для второго можно организовать цикл по while not(EOF(F)) (F — файловая переменная) с использованием ReadLn и переменной-счетчиком.

А можно просто
FileSize(f);
независимо от типа файла

[2], [3]
FileSize — для Текстового вернёт не в байтах не в строках а в кусках по 128.

2 GuAV © (25.05.04 21:35) [4]
FileSize — для Текстового вернёт не в байтах не в строках а в кусках по 128.

А это смотря как открывать сей файл!
см. Хэлп —
Note: FileSize can»t be used on a text file.


> А это смотря как открывать сей файл!

По условию textfile. Или вы о другом?

> Note: FileSize can»t be used on a text file.

Ну да, FileSize не работает, а так вроде работает:

> GetFileSize(TTextRec(. ).Handle, . )

Забудьте вы вообще о текстовых файлах. Это атавизм паскаля. Делается так.

var Str:TStrings;
begin
Str:=TStringList.Create;
Str.LoadFromFile(«. «);
.
//доступ ко всему файлу в виде одной строки
//(не бойтесь, динамическая строка — до 2 ГБ)
. =Str.Text;
//или по строкам
. =Str[i];
.
Str.Free;

сказать, что файл не текстовый и
> FileSize

TextFile — Тип Delphi

Текстовый файл отличается тем что он разбит на разные по длине строки, отделенные символами #13#10. Есть 2 основных метода работы с текстовыми файлами — старый паскалевский способ и через файловые потоки. У обоих есть преимущества и недостатки. Через потоки способ проще поэтому начнем с него.

Итак у всех потомков класса TStrings (TStringList, memo.Lines и т.п. ) есть методы записи и чтения в файл — SaveToFile, LoadFromFile. Преимущество — простота использования и довольно высокая скорость, недостаток — читать и писать файл можно только целиком.

1. Загрузка текста из файла в Memo:

2. Сохранение в файл:

3. А вот так можно прочитать весь файл в строку:

Паскалевский метод доступа

Для более тонких операций над текстовыми файлами прийдется освоить очень древний паскалевский способ.

Итак, для доступа к текстовым файлам используется переменная типа TextFile. До сих пор не совсем понимаю что это такое физически — что-то типа «внутреннего» паскалевского Handle на файл.

Итак чтобы ассоциировать файл на диске с переменной надо проделать следующие опрерации:

1) Определяем файловую переменную:

2) Ассоциируем ее:

3) Теперь надо этот файл открыть, есть 3 варианта:

  1. файла нет или он должен быть перезаписан, открытие для записи: Rewrite(f)
  2. файл есть и его надо открыть для чтения (с первой строки): Reset(f)
  3. файл есть и его надо открыть для дописования строк в конец: Append(f)

Как видите не хватает очень полезных функций таких как открытия файла для чтения с произвольной строки и для записи в файл произвольной строки. Но надо учесть, что так как длины строк разные, не существует никакого способа узнать физическое место начала например 1000 строки, не прочитав всю тысячу строк. Для записи ситуация еще сложнее — вставить строку означает перезаписать всю информацию после этой строки заново. Таким образом варианты только следующие:


  • Перезаписать весть файл
  • Читать с первой строки
  • Дописать что-то в конец
  • Читать и писать файл целиком (см. выше работу через TStrings)

В конце работы открытый файл нужно закрыть:

Теперь пусть у нас есть строковая переменная s для чтения строки из файла

Чтение предварительно открытого файла:

ReadLn(f, s) — будет прочитанна текущая строка и позиция чтения переведена на следующую позицию.

А как прочитать весь файл?

Хорошо, а если файл несколько метров есть ли способ поставить какой-нибудь ProgressBar или Gauge чтобы показывал сколько считанно? Есть, но не совсем прямой — не забыли, сколько строк в файле заранее мы не знаем, узнать можно только прочитав его весь, но показометер мы все-таки сделаем:

Теперь комментарии к коду.

  1. Функию GetFileSize я рсссмотрю после, она немного по другому подходит к чтению файла (кстати я знаю еще по крайней мере 3 способа ее реализации, поэтому не нужно указывать что это можно сделать легче, быстрее или просто по другому — просто давайте разберем это позже)
  2. Переменная i — все время указывает на количество байт которое мы считали — мы определяем длину каждой строки и прибавляем 2 (символы конца строки). Зная длину файла в байтах и сколько байт прочитано можно оценить и прогресс, но
  3. Если ставить изменение прогресса после каждой строки, то это очень сильно тормознет процесс. Поэтому вводим переменную j и обновляем прогресс например 1 раз на 1000 прочитанных строк
  4. Переменная Canceled — глобальная переменная. Поставьте на форму кнопку, в обработчике нажатия поставьте Canceled:=True; и нажатие кнопки прервет чтение файла.

Теперь как писать в текстовый файл:

Запись целой строки:

Запись кусочка строки(те следующая операция записи будет произведена в ту же строку):

Если переменная s содержит больше 255 символов (т.е. является длинной строкой), то таким способом ни фига не запишится, в файл вместо строки попадут 4 байта указателя на нее. Надо делать так:

Работа через WinAPI

Раздел написан Podval (примеры к сожалению на С++)

Любителям WinAPI посвящается. Функции FileOpen, FileSeek, FileRead. Возьмем форму, положим на нее кнопку, грид и Опен диалог бокс. Это для Билдера, но какая нам в данном случае разница?

Потренируемся еще. Функции FileExists, RenameFile, FileCreate, FileWrite, FileClose. Бросим на форму Save dialog box.

(с) Оба примера взяты из хелпа по Borland C++ Builder 5.

Первоисточник тот же.

Может быть, такая функция полезна будет, хотя о программном поиске файлов и так много наговорено. Но на всякий случай, как говорится.

В этом примере идет поиск в текущем каталоге и каталоге Windows

В дополнение к Дате/Времени

Для конвертации возвращаемого значения в TDateTime:

Теперь разберем типизированные файлы. Типизированный файл — это файл в котором записанны идентичные структуры. Например любой файл можно считать файлом байтов — т.е. можно его читать байт за байтом, можно перейти сразу к любому байту по его номеру, можно сразу узнать сколько байт в файле, можно заменить любой байт на другой не перезаписывая файл. Теперь все это в примерах:

Объявляем файл байтов:

Ассоциируем файловую переменную с физическим файлом:

Теперь мы можем либо перезаписать/создать файл:

Либо открыть существующий для чтения и записи:

Обратите внимание, что функция Reset хотя и имеет такой же формат как и для текстовых файлов, но открытый ей файл можно и читать и писать, в отличие от текстовых.

Теперь функции работы с файлом:

Все эти функции не работают с файлами большими 2 Gb.

После работы файл надо закрыть:

Приведенные выше механизмы будут работать с любым файлом, так как любой файл можно считать файлом байтов. Теперь где это можно использовать? В принципе везде, но в подавляющем большинстве случаев это будет очень неудобно, ведь скорость считывания при чтении по байтам будет на порядки более низкой чем другими способами. Однако в некоторых случаях этот способ может быть очень полезен. Например в программе вам надо заменить 100й байт файла на другой, или прочитать 100й байт файла, например во всяких читерских программах, при взломе и т.п. Здесь такой доступ будет весьма удобен. Гораздо более интересным представляется дальнейшее развитие технологии типизированных файлов (их еще лет 15 назад называли «Файлы прямого доступа»). Представим себе, что файл состоит не из байт а из более сложных структур. Например мы имеем некоторую информацию в виде:

Обратите внимание, что все элементы записи точно типизированны, нет ни длинных строк, ни открытых массивов, ни объектов, другими словами, заранее точно известно сколько именно байт будет занимать переменная этого типа. Объявим переменную этого типа:

и файл этого типа:

Теперь мы можем читать и писать сразу целую структуру, абсолютно так же как и если бы это был один байт:

Все остальные функции приведенные в предыдущей статье будут работать так же, только одно отличие — Seek и Size оперируют не с количеством байт, а с количеством записей.

Идем дальше. Есть такое понятие как нетипизированный файл. Это такой файл который содержит разнородные элементы. Например файл EXE — вначале он имеет заголовок, затем двоичный код, в конце какие-то ресурсы. Все части файла имеют разную длину и разную структуру. Тут уже обратится к произвольному элементу сложно, обычно надо вначале узнать где этот элемент находится, подчас это записано в предыдущем куске информации. Работа с такими файлами достаточно сложна и требует вручную разработки алгоритмов его чтения, но в связи гибкостью структуры и компактностью такие файлы составляют большинство. Для работы с нетипизированными файлами используют процедуры BlockRead и BlockWrite, которые позволяют читать/писать произвольное количество байт. Привожу пример пользования этими функциями из справки по Дельфи:

Этот код копирует из одного файла в другой. Замечания по поводу этого метода работы с файлами — плюсы — очень высокая скорость, особенно если размер буффера увеличить до 64kb-512kb, что позволит считывать файл достаточно большими кусками, чтобы обеспечить отсутствие простоев винчестера, к тому же обеспечивается очень высокая гибкость в работе. Минусы — сложность разработки, необходимость вручную писать все детали механизма чтения/записи и интерпретации данных.

Пожалуй на этом можно было бы и завершить описание работы с файлами средствами Паскаля и файловых переменных, но заглянув в Help Дельфей я обнаружил еще несколько функций достойных упоминания.

Теперь разберем возможности работы потомка TStream — TFileStream — файловый поток. Этот класс был специально введен для работы с файлами. Для работы с файловым потоком Вам надо записать в Uses модули classes, Sysutils (classes — включает в себя собственно определение класса, Sysutils — некоторые константы необходимые для работы).

Вот пример записи/перезаписи файла:

Теперь небольшой разбор:

TFileStream.create — конструктор класса, его вызов требует указания имени файла и опций его открытия, следующие опции определены:

Теперь метод Write — этим методом в файл пишется любая информация из буфера любого типа, Вам надо указать только буффер и количество записываемых байтов. В данном случае используется переменная типа String в качестве буффера, но так как для длинных строк она представляет собой лишь указатель, то конструкция «pointer(s)^» заставляет обращаться именно к ее содержимому.

А вот этот код демонстрирует чтение файла с использованием файлового потока:

И пояснения к коду:

  1. Никаких проверок длину файла и его наличие здесь не делается — это демонстрационный код, а не готовая процедура чтения.
  2. Файл мы считываем в буффер типа PChar (с тем же успехом можно использовать массив или любой другой контейнер). Для тех кто не помнит — процедуры GetMem(p, 255) и FreeMem(p) — распределение памяти для строки и освобождение памяти.
  3. Метод потока Seek позволяет установить текущую позицию считывания/записи файла. Первый параметер — номер байта, второй — это от чего считать этот байт (у нас считать от начала файла), возможны варианты:
    • soFromBeginning — от начала файла
    • soFromCurrent — от текущей позиции считывания
    • soFromEnd — от конца файла (в этом случае номер байта должен быть отрицательным или равным нулю)
  4. Собственно считывание из потока осуществляется методом read, в котором указывается в качестве параметров буфер в который мы читаем и желаемое количество байт для чтения. Метод read является функцией, которая возвращает количество байт реально прочитанных из потока.

Заканчивая о файловых потоках хочу упомянуть о методе

CopyFrom который позволяет перекачивать информацию из одного потока в другой и о свойствах:

Size — размер файла
Position — текущая позиция чтения/записи потока

Работа с файловыми потоками весьма быстра, этот класс, являсь классом VCL, в то же время базируется на низкоуровневых функциях Windows, что обеспечивает очень высокую скорость работы и стабильность операций. К тому же многие компоненты и классы VCL поддерживаю прямое чтение и запись с файловыми потоками, что занчительно упрощает работу — например TStringList, TBlobField, TMemoField и другие.

Файловые потоки могут быть рекомендованы к использованию в большинстве случаев для чтения и записи файлов (за исключением специфических ситуаций, требующих каких-то других подходов), другими словами если вам надо просто записать или считать файл, используйте файловые потоки.


Работа через Handle

Еще один способ работы с файлами — это открытие Handle на файл и работу через него. Тут есть 2 варианта — можно использовать функции Дельфи или использовать WinAPI напрямую.

При использовании функций Дельфи можно применять следующие функции:

FileOpen(FileName, fmOpenWrite or fmShareDenyNone) — функция открывает файл и возвращает целое цисло — Handle на файл. Параметры функции — имя файла и тип доступа (все типы доступа я перечислил ранее). Если файл успешно открыт то Handle должен быть положительным цислом, отрицательное число — это код ошибки.

Во всех остальных функциях используется именно значение Handle, возвращаемое этой функцией.

FileClose(Handle: Integer) — закрывает файл

FileRead(Handle: Integer; var Buffer; Count: Integer): Integer;
FileWrite(Handle: Integer; const Buffer; Count: Integer): Integer;

Эти функции для чтения/записи файла, где Buffer любая переменная достаточного размера для чтения/записи куска информации (обычно типа PChar или массив), Count-количество байт, которое Вы желаете записать/прочитать. Функции возвращают количество байт которые реально были прочитанны или записаны.

Этот тип доступа к файлам применяется весьма редко. Дело в том что он практически дублирует соответствующие функции WinAPI и к тому же обычно работает несколько медленнее, чем например потоки. И все же использование функций FileOpen и FileClose не лишено привлекательности. Наряду с тем что эти функции намного легче в использовании соответствующих функций WinAPI (можете сравнить — FileOpen имеет 2 параметра, cooтветствующая функция WinAPI — CreateFile имеет 7 параметров, большая часть из которых реально требуется лишь в ограниченном числе случаев) этот путь доступа открывает возможность прямого использования всех функций WinAPI про работе с файлами, которые требуют Handle на открытый файл.

Дельфи предоставляет довольно широкие возможности по файловым операциям без использования механизмов открытия/закрытия файлов.

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

ChDir(NewCurrentPath: string); — изменяет текущий каталог (в среде Windows сие конечно не так актуально как в ДОС, но все же), прочитать же текущий каталог можно функцией GetCurrentDir, а текущий каталог для определенного драйва — GetDir.

CreateDir(const Dir: string): Boolean; — создает каталог. При этом предыдущий уровень должен присутствовать. Если вы хотите сразу создать всю вложенность каталогов используйте функцию ForceDirectories(Dir: string): Boolean; Обе функции возвращают True если каталог создан

DiskFree(Drive: Byte): Int64; — дает свободное место на диске. Параметер — номер диска 0 = текущий, 1 = A, 2 = B, и так далее

DiskSize(Drive: Byte): Int64; — размер винта. Обратите внимание на то что для результата этой и предыдущей функций абсолютно необходимо использовать переменную типа Int64, иначе макимум того что вы сможете прочитать правильно будет ограничен 2Gb

FileExists(const FileName: string) — применяется для проверки наличия файла

FileGetAttr(const FileName: string): Integer;
FileSetAttr(const FileName: string; Attr: Integer): Integer; — функции для работы с атрибутами файлов. Вот список возможных атрибутов:

RemoveDir(const Dir: string): Boolean; — удаляет папку(пустую)
DeleteFile(const FileName: string): Boolean; — удаляет файл
RenameFile(const OldName, NewName: string) — переименовывает файл

Информация о файле

Привожу пример функции которая собирает довольно большое количество информации о выбранном файле:

Скорее всего эта функция как есть вряд ли понадобится, так как наверняка бОльшее количество определяемых параметров избыточно, тем ни менее может кому пригодится как пример выяснения информации о файле.

Теперь поговорим о поиске файлов. Для этой цели могут использоваться процедуры FindFirst, FindNext, FindClose, при участии переменной типа TSearchRec которая хранит информацию о текущем статусе поиска и характеристики последнего найденного файла.

Пример иллюстрирующий поиск всех файлов и каталогов в определенном каталоге:

TextFile — Тип Delphi

на write ошибка IO error 105.

Добавлено 14.11.17, 12:52
Нашел такое решение:

Кстати такой ещё вопрос.

Если я хочу через ShellExecute передать русские символы, какие строки я должен применять?

Параметр, включающий файловый путь (длинный, с пробелами, с русскими символами), заключен в двойные кавычки (это требование Windows)

leo
Спасибо за полный ответ, а такие вещи как ReadFile и WriteFile это какие-то чисто апишные функции? Их использование когда бывает оправдано?

MBo
сейчас попробую

Просто String и просто PChar.
Каждая версия дельфи сама разруливает, какой вариант строк используется по умолчанию и соотв-но какой вариант функции ShellExecute использовать (А или W).

Добавлено 15.11.17, 06:20
В двойные кавычки нужно заключать строки, содержащие пробелы, т.к. пробел по умолчанию является разделителем параметров командной строки. При этом не важно какие символы используются в строке — русские или не русские.

Добавлено 15.11.17, 06:38

Да, чисто апишные. Их обертками в дельфи являются FileRead и FileWrite.
По сути BlockRead и BlockWrite тоже используют эти функции. Поэтому принципиальной разницы между ними нет.
Разница лишь в том, что Read\WriteFile и FileRead\Write используют хэндл файла, получаемый апишной ф-ей CreateFile или дельфийской FileOpen. А в переменной типа File этот хэндл скрыт в структуре TFileRec(f).Handle. Поэтому «оправданность» может заключаться не в самих функциях чтения\записи, а использовании более продвинутой функции CreateFilе с возможностью задания дополнительных опций\флагов открытия файла.

Добавлено 15.11.17, 06:50
PS: Или даже не продвинутых, а более понятных, например разрешений чтения записи файла другими процессами FILE_SHARE_READ\WRITE, вместо малопонятной дельфийской FileMod, не разрешающей, а запрещающей чтение\запись (например fmShareDenyWrite = FILE_SHARE_READ, а fmShareDenyRead = FILE_SHARE_WRIE).

Работа с файлами в Delphi

Дата добавления: 2015-06-12 ; просмотров: 7257 ; Нарушение авторских прав

Поименованная область памяти на внешнем носителе (диске) называется файлом. У любого файла есть имя. Информация, которая хранится в файле, представляет собой множество элементов одного типа. Размер файла, как правило, ограничен только емкостью устройства, на котором он хранится, что позволяет обрабатывать большие объемы информации.

Способ объявления файла зависит от его типа. Различают три типа файлов:

Текстовый файл, состоит из последовательности любых символов. Символы образуют строки, каждая из которых заканчивается символом «конец строки». Конец самого файла обозначается символом «конец файла». При записи информации в текстовый файл, все данные преобразуются к символьному типу и хранятся в таком виде. Просмотреть текстовый файл можно с помощью любого текстового редактора.

Описание текстового файла:

var имя_переменной: TextFile;

Типизированный файл состоит из элементов одного типа, число которых заранее не определено и может быть любым. Он так же заканчивается символом «конец файла». Данные в типизированном файле хранятся в двоичных кодах и не просматриваются текстовыми редакторами. Объявление таких файлов имеет вид:

имя_переменной: file of тип;

В нетипизированных файлах информация считывается и записывается в виде блоков определенного размера, в них могут храниться данные любого вида и структуры. Описание нетипизированного файла:

var имя_переменной: file;

Для доступа к файлам используют специальную файловую переменную.

c:file of integer;

Для связи файловой переменной с файлом на диске примняют процедуру:

здесь переменная – имя переменной любого файлового типа, имя_файла – строка символов, содержащая полное имя файла (если путь к файлу не указан, подразумевается, что он находится в текущем каталоге).

Для открытия существующего файла используют процедуру:

После открытия файла доступен его первый компонент.

Для создания нового файла или удаления информации из существующеего применяют процедуру:

После выполнения различных операций файл должен быть закрыт. Для этого применяют процедуру

Запись данных в текстовый файл осуществляется процедурами


где список_выражений – выражения символьного, целочисленного, вещественного, строкового или логического типов, которые будут записаны в файл, связанный с файловой переменной.

Процедура Writeln в конце каждой строки добавляет символ «окончание строки».

ПРИМЕР. Подпрограмма, которая создает на диске D файл abc.txt и записывает в него таблицу умножения.

procedure TForm1.Button1Click(Sender: TObject);

var f:TextFile; i,j:byte;

for j := 2 to 9 do

Чтение данных из файла осуществляется последовательно от начала файла процедурой

здесь в списоке_переменных перечисляются переменные, в которые будут записаны данные прочитанные из файла, связанного с файловой переменной.

Процедура Readln после чтения из файла очередного элемента переходит к следующей строке.

ПРИМЕР. Разместим на форме кнопку «Читать из файла» и компонент Memo1.

Обращение к следующей подпрограмме приведет к тому, что на форме в окне редактирования появится текст из файла D:\abc.txt:

procedure TForm1.Button1Click(Sender: TObject);

Для типизированных файлов так же применимы процедуры чтения и записи

только типы выражений при записи и чтении должны соответствовать объявленному типу файла. Процедуры Writeln и Readln к типизированным файлам не применяют.

Для перемещения по типизированному файлу используют процедуру:

Эта процедура позволяет перейти к элементу с указанным номером, то есть осуществляет прямой доступ к компонентам файла. При прямом доступе компоненты файла нумеруются от 0 до n-1, где n – число компонентов в файле.

Так же для прямого доступа к компонентам типизированного файла можно применять функции:

filesize(переменная) – возвращает число реальных компонентов в открытом файле, связанном с файловой_переменной (для пустого файла функция вернет 0);

filepos(переменная) – возвращает значение текущей позиции в открытом файле, связанном с файловой переменной (если файл только что открылся, то функция вернет ноль, после прочтения последнего компонента из файла значение функции совпадает со значением filesize, что указывает на достижение конца файла).

Процедура Seek и функции filesize и filepos с текстовыми файлами не работают. Следующие подпрограммы работают как с типизированными, так и с текстовыми файлами:

переименовывает закрытый файл, связанный с файловой переменной, в соответствии с именем указанным в строке имя файла;

Erase(переменная) – удаляет закрытый файла, связанный с файловой переменной;

Eof(переменная) принимает значение истина (true), если достигнут конец файла, связанного с файловой_переменной, и ложь (false) в противном случае;

ПРИМЕР. Подпрограмма, приведенная ниже, работает так. Открывается вещественный файл d:\abc.dat и в него записывается определенное количество вещественных чисел, вычисленных по формуле. Просмотреть созданный файл не возможно, так как информация в нем представлена в двоичных кодах.

Илон Маск рекомендует:  Что такое код searchpath

procedure TForm1.Button1Click(Sender: TObject);

How to save string into text files in Delphi?

What is the easiest way to create and save string into .txt files?

6 Answers 6

Also SaveToFile and LoadFromFile can take an additional Encoding in Delphi 2009 and newer to set the text encoding (Ansi, UTF-8, UTF-16, UTF-16 big endian).

Actually, I prefer this:

I consider this the easiest way, since you don’t need the classes or any other unit for this code. And it works for all Delphi versions including -if I’m not mistaken- all .NET versions of Delphi.

I’ve added a call to SetTextBuf() to this example, which is a good trick to speed up textfiles in Delphi considerably. Normally, textfiles have a buffer of only 128 bytes. I tend to increase this buffer to a multiple of 4096 bytes. In several cases, I’va also implemented my own TextFile types, allowing me to use these «console» functions to write text to memo fields or even to another, external application! At this location is some example code (ZIP) I wrote in 2000 and just modified to make sure it compiles with Delphi 2007. Not sure about newer Delphi versions, though. Then again, this code is 10 years old already.
These console functions have been a standard of the Pascal language since it’s beginning so I don’t expect them to disappear anytime soon. The TtextRec type might be modified in the future, though, so I can’t predict if this code will work in the future. Some explanations:

  • WA_TextCustomEdit.AssignCustomEdit allows text to be written to CustomEdit-based objects like TMemo.
  • WA_TextDevice.TWATextDevice is a class that can be dropped on a form, which contains events where you can do something with the data written.
  • WA_TextLog.AssignLog is used by me to add timestamps to every line of text.
  • WA_TextNull.AssignNull is basically a dummy text device. It just discards anything you write to it.
  • WA_TextStream.AssignStream writes text to any TStream object, including memory streams, file streams, TCP/IP streams and whatever else you have.

Code in link is hereby licensed as CC-BY

Oh, the server with the ZIP file isn’t very powerful, so it tends to be down a few times every day. Sorry about that.

Работа с текстовыми файлами в среде программирования Делфи.

Рассматриваются основные вопросы, связанные с работой с текстовыми файлами в среде программирования Делфи. Пособие может быть использовано студентами для самостоятельной подготовки в занятию по данной теме.

Скачать:

Вложение Размер
rabota_s_tekstovymi_faylami_v_delfi.docx 48.59 КБ

Предварительный просмотр:

Работа с текстовыми файлами в среде программирования Делфи.

Текстовый файл отличается тем что он разбит на разные по длине строки, отделенные символами #13#10. Есть 2 основных метода работы с текстовыми файлами — старый паскалевский способ и через файловые потоки. У обоих есть преимущества и недостатки. Через потоки способ проще поэтому начнем с него.

Итак у всех потомков класса TStrings (TStringList, memo.Lines и т.п. ) есть методы записи и чтения в файл — SaveToFile, LoadFromFile. Преимущество — простота использования и довольно высокая скорость, недостаток — читать и писать файл можно только целиком.

1. Загрузка текста из файла в Memo:

2. Сохранение в файл:

3. А вот так можно прочитать весь файл в строку:

function ReadFromFile(FileName: string ): string ;
begin
with TStringList.Create do
try
LoadFromFile(FileName);
result := text;
finally
Free;
end ;
end ;

Паскалевский метод доступа

Для более тонких операций над текстовыми файлами прийдется освоить очень древний паскалевский способ.

Итак, для доступа к текстовым файлам используется переменная типа TextFile . До сих пор не совсем понимаю что это такое физически — что-то типа «внутреннего» паскалевского Handle на файл.

Итак чтобы ассоциировать файл на диске с переменной надо проделать следующие опрерации:

1) Определяем файловую переменную:

2) Ассоциируем ее:

3) Теперь надо этот файл открыть, есть 3 варианта:

  • файла нет или он должен быть перезаписан, открытие для записи: Rewrite(f)
  • файл есть и его надо открыть для чтения (с первой строки): Reset(f)
  • файл есть и его надо открыть для дописования строк в конец: Append(f)


Как видите не хватает очень полезных функций таких как открытия файла для чтения с произвольной строки и для записи в файл произвольной строки. Но надо учесть, что так как длины строк разные, не существует никакого способа узнать физическое место начала например 1000 строки, не прочитав всю тысячу строк. Для записи ситуация еще сложнее — вставить строку означает перезаписать всю информацию после этой строки заново. Таким образом варианты только следующие:

  • Перезаписать весть файл
  • Читать с первой строки
  • Дописать что-то в конец
  • Читать и писать файл целиком (см. выше работу через TStrings)
  • В конце работы открытый файл нужно закрыть:

Теперь пусть у нас есть строковая переменная s для чтения строки из файла

Чтение предварительно открытого файла:

ReadLn(f, s) — будет прочитанна текущая строка и позиция чтения переведена на следующую позицию.

А как прочитать весь файл?

Хорошо, а если файл очень большого размера, есть ли способ поставить какой-нибудь ProgressBar или Gauge чтобы показывал сколько считанно? Есть, но не совсем прямой — не забыли, сколько строк в файле заранее мы не знаем, узнать можно только прочитав его весь, но показометер мы все-таки сделаем:

var
Canceled: Boolean;

function GetFileSize(FIleName: string ): integer;
var
f: file of Byte;
begin
try
AssignFile(f, FileName);
Reset(f);
result := filesize(F);
CloseFile(f);
except
result := -1;
end ;
end ;

procedure ReadMyFile;
var
i, j: integer;
begin
ProgressBar1.Max := GetFileSize(‘c:\MyFile.txt’);
ProgressBar1.position := 0;
AssignFile(f, ‘c:\MyFile.txt’);
Canceled := False;
reset(f);
i := 0;
j := 0;
while not eof(f) do
begin
inc(j);
readln(f, s);
i := i + length(s) + 2;
if (j mod 1000) = 0 then
begin
ProgressBar1.position := i;
Application.ProcessMessages;
if canceled then break;
end ;
<здесь мы что-то делаем с прочитанной строкой>
end ;
CloseFile(f);
end ;

Теперь комментарии к коду.

Функию GetFileSize я рсссмотрю после, она немного по другому подходит к чтению файла (кстати я знаю еще по крайней мере 3 способа ее реализации, поэтому не нужно указывать что это можно сделать легче, быстрее или просто по другому — просто давайте разберем это позже).

Переменная i — все время указывает на количество байт которое мы считали — мы определяем длину каждой строки и прибавляем 2 (символы конца строки). Зная длину файла в байтах и сколько байт прочитано можно оценить и прогресс, но eсли ставить изменение прогресса после каждой строки, то это очень сильно тормознет процесс. Поэтому вводим переменную j и обновляем прогресс например 1 раз на 1000 прочитанных строк.

Переменная Canceled — глобальная переменная. Поставьте на форму кнопку, в обработчике нажатия поставьте Canceled:=True; и нажатие кнопки прервет чтение файла.

Теперь как писать в текстовый файл:

Запись целой строки:

Запись кусочка строки(те следующая операция записи будет произведена в ту же строку):

Если переменная s содержит больше 255 символов (т.е. является длинной строкой), то таким способом ни фига не запишится, в файл вместо строки попадут 4 байта указателя на нее. Надо делать так:

Теперь разберем типизированные файлы. Типизированный файл — это файл в котором записанны идентичные структуры. Например любой файл можно считать файлом байтов — т.е. можно его читать байт за байтом, можно перейти сразу к любому байту по его номеру, можно сразу узнать сколько байт в файле, можно заменить любой байт на другой не перезаписывая файл. Теперь все это в примерах:

Объявляем файл байтов:

var
f: file of byte;
b: Byte;

Ассоциируем файловую переменную с физическим файлом:

Теперь мы можем либо перезаписать/создать файл:

Либо открыть существующий для чтения и записи:

Обратите внимание, что функция Reset хотя и имеет такой же формат как и для текстовых файлов, но открытый ей файл можно и читать и писать, в отличие от текстовых.

Теперь функции работы с файлом:

read(f,b); — прочитать 1 байт

write(f,b); — записать 1 байт

seek(f,100); — поставить текущее положение считывания/записи на сотый байт

Size(f); — прочитать количество байт в файле.

Eof(f); — узнать не являетсмя ли байт последним

Все эти функции не работают с файлами большими 2 Gb.

После работы файл надо закрыть:

Приведенные выше механизмы будут работать с любым файлом, так как любой файл можно считать файлом байтов. Теперь где это можно использовать? В принципе везде, но в подавляющем большинстве случаев это будет очень неудобно, ведь скорость считывания при чтении по байтам будет на порядки более низкой чем другими способами. Однако в некоторых случаях этот способ может быть очень полезен. Например в программе вам надо заменить 100й байт файла на другой, или прочитать 100й байт файла, например во всяких читерских программах, при взломе и т.п. Здесь такой доступ будет весьма удобен. Гораздо более интересным представляется дальнейшее развитие технологии типизированных файлов (их еще лет 15 назад называли «Файлы прямого доступа»). Представим себе, что файл состоит не из байт а из более сложных структур. Например мы имеем некоторую информацию в виде:

type
MyRec = record
Name: string [100];
Age: byte;
Membership: Boolean;
Accounts: array [1..10] of integer;
end ;

Обратите внимание, что все элементы записи точно типизированны, нет ни длинных строк, ни открытых массивов, ни объектов, другими словами, заранее точно известно сколько именно байт будет занимать переменная этого типа. Объявим переменную этого типа:

var
MyVar: MyRec;

и файл этого типа:

var
f: File of MyRec;

Теперь мы можем читать и писать сразу целую структуру, абсолютно так же как и если бы это был один байт:

AssignFile(f, ‘c:\MyFile.rec’);
Rewrite(f);
MyVar.Name := ‘Vitaly’;
MyVar.Age := 33;
MyVar.Membership := True;
MyVar.Accounts[1] := 12345;
MyVar.Accounts[2] := 34985;
Write(f, MyVar);
Closefile(f);

Все остальные функции приведенные в предыдущей статье будут работать так же, только одно отличие — Seek и Size оперируют не с количеством байт, а с количеством записей.

Идем дальше. Есть такое понятие как нетипизированный файл. Это такой файл который содержит разнородные элементы. Например файл EXE — вначале он имеет заголовок, затем двоичный код, в конце какие-то ресурсы. Все части файла имеют разную длину и разную структуру. Тут уже обратится к произвольному элементу сложно, обычно надо вначале узнать где этот элемент находится, подчас это записано в предыдущем куске информации. Работа с такими файлами достаточно сложна и требует вручную разработки алгоритмов его чтения, но в связи гибкостью структуры и компактностью такие файлы составляют большинство. Для работы с нетипизированными файлами используют процедуры BlockRead и BlockWrite, которые позволяют читать/писать произвольное количество байт. Привожу пример пользования этими функциями из справки по Дельфи:

var
FromF, ToF: file ;
NumRead, NumWritten: Integer;
Buf: array [1..2048] of Char;
begin
if OpenDialog1.Execute then <показываем диалог открытия>
begin
AssignFile(FromF, OpenDialog1.FileName);
Reset(FromF, 1); < Record size = 1 >
if SaveDialog1.Execute then <показываем диалог сохранения>
begin
AssignFile(ToF, SaveDialog1.FileName);
Rewrite(ToF, 1); <запись размером 1>
Canvas.TextOut(10,10, ‘Copying ‘ +IntToStr(FileSize(FromF))+ ‘ bytes. ‘ );
repeat
BlockRead(FromF, Buf, SizeOf(Buf), NumRead);
BlockWrite(ToF, Buf, NumRead, NumWritten);
until (NumRead = 0) or (NumWritten <> NumRead);

CloseFile(FromF);
CloseFile(ToF);
end ;
end ;
end ;

Этот код копирует из одного файла в другой. Замечания по поводу этого метода работы с файлами — плюсы — очень высокая скорость, особенно если размер буффера увеличить до 64kb-512kb, что позволит считывать файл достаточно большими кусками, чтобы обеспечить отсутствие простоев винчестера, к тому же обеспечивается очень высокая гибкость в работе. Минусы — сложность разработки, необходимость вручную писать все детали механизма чтения/записи и интерпретации данных.

Пожалуй на этом можно было бы и завершить описание работы с файлами средствами Паскаля и файловых переменных, но заглянув в Help Дельфей я обнаружил еще несколько функций достойных упоминания.

Erase(f) — удаляет файл

FilePos(f) — возвращает текущую позицию чтения/записи в файл

Flush(f) — сбрасывает кэшированные файловые операции на диск

Rename(f, ‘MyNewFileName.txt’ ) — переименование файлов

Truncate(f) — файл обрезается до текущей позиции чтения/записи

Теперь разберем возможности работы потомка TStream — TFileStream — файловый поток. Этот класс был специально введен для работы с файлами. Для работы с файловым потоком Вам надо записать в Uses модули classes, Sysutils (classes — включает в себя собственно определение класса, Sysutils — некоторые константы необходимые для работы).

Вот пример записи/перезаписи файла:


procedure WriteFileUsingStream(s, FileName: string );
begin
with TFileStream.create(FileName, fmCreate or fmOpenWrite) do
try
write(pointer(s)^, length(s));
finally
free;
end ;
end ;

Теперь небольшой разбор:

TFileStream.create — конструктор класса, его вызов требует указания имени файла и опций его открытия, следующие опции определены:

fmCreate = $FFFF;
fmOpenRead = $0000;
fmOpenWrite = $0001;
fmOpenReadWrite = $0002;
fmShareCompat = $0000;
fmShareExclusive = $0010;
fmShareDenyWrite = $0020;
fmShareDenyRead = $0030;
fmShareDenyNone = $0040;

Теперь метод Write — этим методом в файл пишется любая информация из буфера любого типа, Вам надо указать только буффер и количество записываемых байтов. В данном случае используется переменная типа String в качестве буффера, но так как для длинных строк она представляет собой лишь указатель, то конструкция «pointer(s)^» заставляет обращаться именно к ее содержимому.

А вот этот код демонстрирует чтение файла с использованием файлового потока:

var
p: PChar;
begin
GetMem(p, 255);
with TFileStream.create( ‘c:\myText.txt’ , fmOpenReadWrite) do
try
Seek(10, soFromBeginning);
read(p^, 254);
finally
free;
end ;
showmessage(p);
FreeMem(p);
end ;

И пояснения к коду:

Никаких проверок длину файла и его наличие здесь не делается — это демонстрационный код, а не готовая процедура чтения.

Файл мы считываем в буффер типа PChar (с тем же успехом можно использовать массив или любой другой контейнер). Для тех кто не помнит — процедуры GetMem(p, 255) и FreeMem(p) — распределение памяти для строки и освобождение памяти.

Метод потока Seek позволяет установить текущую позицию считывания/записи файла. Первый параметер — номер байта, второй — это от чего считать этот байт (у нас считать от начала файла), возможны варианты:

soFromBeginning — от начала файла

soFromCurrent — от текущей позиции считывания

soFromEnd — от конца файла (в этом случае номер байта должен быть отрицательным или равным нулю)

Собственно считывание из потока осуществляется методом read, в котором указывается в качестве параметров буфер в который мы читаем и желаемое количество байт для чтения. Метод read является функцией, которая возвращает количество байт реально прочитанных из потока.

Заканчивая о файловых потоках хочу упомянуть о методе CopyFrom который позволяет перекачивать информацию из одного потока в другой и о свойствах:

Size — размер файла

Position — текущая позиция чтения/записи потока

Работа с файловыми потоками весьма быстра, этот класс, являсь классом VCL, в то же время базируется на низкоуровневых функциях Windows, что обеспечивает очень высокую скорость работы и стабильность операций. К тому же многие компоненты и классы VCL поддерживаю прямое чтение и запись с файловыми потоками, что занчительно упрощает работу — например TStringList, TBlobField, TMemoField и другие.

Файловые потоки могут быть рекомендованы к использованию в большинстве случаев для чтения и записи файлов (за исключением специфических ситуаций, требующих каких-то других подходов), другими словами если вам надо просто записать или считать файл, используйте файловые потоки.

Работа через Handle

Еще один способ работы с файлами — это открытие Handle на файл и работу через него. Тут есть 2 варианта — можно использовать функции Дельфи или использовать WinAPI напрямую.

При использовании функций Дельфи можно применять следующие функции:

FileOpen(FileName, fmOpenWrite or fmShareDenyNone) — функция открывает файл и возвращает целое цисло — Handle на файл. Параметры функции — имя файла и тип доступа (все типы доступа я перечислил ранее). Если файл успешно открыт то Handle должен быть положительным цислом, отрицательное число — это код ошибки.

Во всех остальных функциях используется именно значение Handle, возвращаемое этой функцией.

FileClose(Handle: Integer) — закрывает файл

FileRead(Handle: Integer; var Buffer; Count: Integer): Integer;

FileWrite(Handle: Integer; const Buffer; Count: Integer): Integer;

Эти функции для чтения/записи файла, где Buffer любая переменная достаточного размера для чтения/записи куска информации (обычно типа PChar или массив), Count-количество байт, которое Вы желаете записать/прочитать. Функции возвращают количество байт которые реально были прочитанны или записаны.

Этот тип доступа к файлам применяется весьма редко. Дело в том что он практически дублирует соответствующие функции WinAPI и к тому же обычно работает несколько медленнее, чем например потоки. И все же использование функций FileOpen и FileClose не лишено привлекательности. Наряду с тем что эти функции намного легче в использовании соответствующих функций WinAPI (можете сравнить — FileOpen имеет 2 параметра, cooтветствующая функция WinAPI — CreateFile имеет 7 параметров, большая часть из которых реально требуется лишь в ограниченном числе случаев) этот путь доступа открывает возможность прямого использования всех функций WinAPI про работе с файлами, которые требуют Handle на открытый файл.

Дельфи предоставляет довольно широкие возможности по файловым операциям без использования механизмов открытия/закрытия файлов.

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

ChDir(NewCurrentPath: string); — изменяет текущий каталог (в среде Windows сие конечно не так актуально как в ДОС, но все же), прочитать же текущий каталог можно функцией GetCurrentDir, а текущий каталог для определенного драйва — GetDir.

CreateDir(const Dir: string): Boolean; — создает каталог. При этом предыдущий уровень должен присутствовать. Если вы хотите сразу создать всю вложенность каталогов используйте функцию ForceDirectories(Dir: string): Boolean; Обе функции возвращают True если каталог создан

DiskFree(Drive: Byte): Int64; — дает свободное место на диске. Параметер — номер диска 0 = текущий, 1 = A, 2 = B, и так далее

DiskSize(Drive: Byte): Int64; — размер винта. Обратите внимание на то что для результата этой и предыдущей функций абсолютно необходимо использовать переменную типа Int64, иначе макимум того что вы сможете прочитать правильно будет ограничен 2Gb

FileExists(const FileName: string) — применяется для проверки наличия файла

FileGetAttr(const FileName: string): Integer;

FileSetAttr(const FileName: string; Attr: Integer): Integer; — функции для работы с атрибутами файлов. Вот список возможных атрибутов:

faReadOnly $00000001 Read-only files
faHidden $00000002 Hidden files
faSysFile $00000004 System files
faVolumeID $00000008 Volume ID files
faDirectory $00000010 Directory files
faArchive $00000020 Archive files
faAnyFile $0000003F Any file

(Естественно не все атрибуты применимы во всех случаях)

RemoveDir(const Dir: string): Boolean; — удаляет папку(пустую)

DeleteFile(const FileName: string): Boolean; — удаляет файл

RenameFile(const OldName, NewName: string) — переименовывает файл

Информация о файле

Привожу пример функции которая собирает довольно большое количество информации о выбранном файле:

type
TFileInfo = record
Exists: boolean; //true если файл найден
Name: string ; //имя файла с расширением
ShortName: string ; //DOS 8.3 имя файла
NameNoExt: string ; //имя файла без расширения
Extension: string ; //расширение файла
AssociatedFile: string ; //программа с которой ассоциирован файл
Path: string ; // путь к файлу
ShortPath: string ; // DOS 8.3 путь файла
Drive: string ; // дисковод на котором находится файл
CreateDate: TDateTime; //время когда файл создан
Size: Int64; // размер файла (работает для файлов и больше 2Gb)
Attributes: record //наличие/отсутствие системных атрибутов
ReadOnly: boolean;
Hidden: boolean;
System: boolean;
Archive: boolean;
end ;
ModifyDate: TDateTime; // время последнего изменения файла
LastAccessDate: TDateTime; // дата последнего открытия файла
end ;

function ReadFileInfo(FileName: string ): TFileInfo;
var
ts: TSearchRec;

function FileTime2DateTime(FT: _FileTime): TDateTime;
var
FileTime: _SystemTime;
begin
FileTimeToLocalFileTime(FT, FT);
FileTimeToSystemTime(FT, FileTime);
Result := EncodeDate(FileTime.wYear, FileTime.wMonth, FileTime.wDay)+
EncodeTime(FileTime.wHour, FileTime.wMinute, FileTime.wSecond,
FileTime.wMilliseconds);
end ;

function AssociatedFile(FileExt: string ): string ;
var
key: string ;
begin
with TRegistry.create do
try
RootKey := HKEY_CLASSES_ROOT;
OpenKey(FileExt, false);
Key := ReadString( » );
CloseKey;
OpenKey(key + ‘\Shell\open\command’ , false);
result := ReadString( » );
Closekey;
finally
free;
end
end ;

begin
Result.Name := ExtractFileName(FileName);
Result.Extension := ExtractFileExt(FileName);
Result.NameNoExt := Copy(Result.Name, 1, length(Result.Name) —
length(Result.Extension));
Result.Path := ExtractFilePath(FileName);
Result.Drive := ExtractFileDrive(FileName);
Result.ShortPath := ExtractShortPathName(ExtractFilePath(FileName));
if lowercase(Result.Extension) <> ‘.exe’ then
Result.AssociatedFile := AssociatedFile(Result.Extension);
if FindFirst(FileName, faAnyFile, ts) = 0 then
begin
Result.Exists := true;
Result.CreateDate := FileDateToDateTime(ts.Time);
Result.Size := ts.FindData.nFileSizeHigh * 4294967296 +
ts.FindData.nFileSizeLow;
Result.Attributes.ReadOnly := (faReadOnly and ts.Attr) > 0;
Result.Attributes.H > 0;
Result.Attributes.System := (faSysFile and ts.Attr) > 0;
Result.Attributes.Archive := (faArchive and ts.Attr) > 0;
Result.ModifyDate := FileTime2DateTime(ts.FindData.ftLastWriteTime);
Result.LastAccessDate := FileTime2DateTime(ts.FindData.ftLastAccessTime);
Result.ShortName := ts.FindData.cAlternateFileName;
Findclose(ts);
end
else
Result.Exists := false;
end ;

Скорее всего эта функция как есть вряд ли понадобится, так как наверняка бОльшее количество определяемых параметров избыточно, тем ни менее может кому пригодится как пример выяснения информации о файле.

Теперь поговорим о поиске файлов. Для этой цели могут использоваться процедуры FindFirst, FindNext, FindClose, при участии переменной типа TSearchRec которая хранит информацию о текущем статусе поиска и характеристики последнего найденного файла.

Пример иллюстрирующий поиск всех файлов и каталогов в определенном каталоге:

var
SearchRec: TSearchRec;
.
if FindFirst( ‘c:\Windows\*.*’ , faAnyFile, SearchRec) = 0 then
repeat
<Вот здесь мы можем делать с найденным файлом что угодно
SearchRec.name — имя файла
ExpandFileName(SearchRec.name) — имя файла с полным путем>
until
FindNext(SearchRec) <> 0;

Примечания по приведенному коду:


Первыми в список могут попадать файлы с именами «.» и «..» — это ДОСовские имена для переходов на «родительский уровень», иногда нужна обработка для их игнорирования.

FindFirst в качестве первого параметра принимает шаблон для поиска, так как он был принят для ДОС. Если шаблон не включает путь то файлы будут искаться в текущем каталоге.

FindFirst требует задания атрибута для файла — здесь мы искали все файлы, если надо какие-то определенные (например только скрытые, или только каталоги) то надо это указать, список всех атрибутов я уже приводил выше.

SearchRec переменная связывает во едино FindFirst и FindNext, но требует ресурсов для своей работы, поэтому желательно ее освободить после поиска процедурой FindClose(SearchRec) — на самом деле утечки памяти небольшие, но если программа работает в цикле и долгое время пожирание ресурсов будет значительным.

FindFirst/FindNext — работают не открывая файлы, поэтому они корректно находят даже Swap файлы Windows.

Блог GunSmoker-а

. when altering one’s mind becomes as easy as programming a computer, what does it mean to be human.

17 октября 2011 г.

Сериализация — файлы в стиле Pascal

Оглавление

Общие сведения

В языке Pascal (и, следовательно, Delphi) есть встроенные средства по работе с файлами, не зависящие от нижележащей операционной системы.

В отличие от файловых API уровня операционной системы, файлы Pascal ориентированы на работу с типами данных языка (а не нетипизированным байтовым потоком, как это имеет место для API операционной системы, которая понятия не имеет про типы данных языка Pascal) и включают в себя высокоуровневую оболочку для работы со строками и наборами однородных данных. Поэтому работа с файлами Pascal удобнее, чем обращение к API операционной системы.

В отличие от других, более современных методов, использующих мета-информацию, файлы Pascal отличаются простотой использования. Поэтому, хотя другие методы работы с файлами могут упростить повседневную работу, но кривая их изучения существенно круче.

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

Сразу же замечу, что я не буду рассказывать для совсем уж начинающих — тут будет подробное изложение. Предполагается, что вы уже прочитали какую-то «книжку по Delphi».

Всего в языке есть три файловых типа:

  • текстовые файлы ( var F: TextFile; )
  • типизированные файлы ( var F: file of Тип-Данных; )
  • нетипизированные файлы ( var F: file; )

Нетипизированные файлы являются самым общим типом, а текстовые и типизированные файлы — обёрткой над нетипизированными файлами, частным случаем. Они предоставляют более удобные способы работы, путём наложения ограничений на содержимое файла.

Для начала стоит немного прояснить терминологию. Вообще говоря, файлы в системе однородны: любой файл — это набор байтов. Не существует какого-то жёсткого разделения файлов по типам. Тем не менее, часто используются следующие термины для какой-то классификации файлов по содержимому.

Под текстовым файлом понимают файлы, содержащие читабельный для человека текст в кодировках ANSI, UTF-8 или Unicode. Текст представлен в явном виде, при этом некоторые специальные символы (с кодами от 0 до 31) являются специальными — они всегда имеют один и тот же смысл и обычно отвечают за форматирование текста: перенос строк, табуляцию и т.п. Обычно текстовыми файлами не называют документы — файлы, содержащие текст, но оформленные в формате определённой программы (вроде Word), потому что кроме самих данных (текста) такие файлы содержат служебную мета-информацию (заголовки, данные форматирования, атрибуты и т.п.).

В Pascal и Delphi в настоящий момент поддерживаются только текстовые файлы в кодировке ANSI. Файловые типы Pascal считаются устаревшим средством — появившись в языке давно, они уже не развиваются. В те далёкие дни люди мало волновались о кодировках, а о Unicode и UTF-8 и слыхом не слыхивали. Поэтому вы не сможете работать с текстовыми файлами, отличными от канонического ANSI-формата (кодовая страница ANSI, без BOM).

Примечание: начиная с Delphi XE2 (год выхода 2011), файлы Pascal впервые за пару десятков лет получили обновление API: теперь становится возможным указывать кодировку файла, которая может быть отличной от системной кодовой страницы, но вы всё ещё не можете использовать BOM.

Как я уже сказал, «текстовый файл» — это просто удобный ярлык, навешиваемый на файл, чтобы как-то охарактеризовать его содержимое. Это не однозначная и неизменная характеристика. Вы можете открыть текстовый файл в двоичном режиме и вы можете открыть произвольный бинарный файл как текстовый (весь вопрос в осмысленности результатов от таких действий; если первое ещё имеет смысл, то открытие двоичного файла как текстового — весьма сомнительная операция).

Типизированные файлы содержат записи одного типа и фиксированной длины. Чаще всего компонентами файла выступают именно записи ( record ), но это может быть и любой другой тип фиксированного размера. К примеру, текстовый файл можно открыть как типизированный — file of AnsiChar .

Наконец, под нетипизированными файлами понимают файлы произвольной, нерегулярной структуры. Это означает открытие файла в двоичном режиме — как набор байт, без какой-либо предопределённой структуры. Именно в этом режиме работают файловые API операционной системы. В подобном режиме можно открыть любой файл и это всегда будет иметь смысл.

Следует заметить, что понятие «типизированный файл» может трактоваться в двух смыслах. Во-первых, про файл можно говорить, что он типизированный в смысле строгого определения типизированного файла Pascal. Т.е. это файл, определяемый как » file of что-то «, состоящий из набора блоков одинаковой длины. Иными словами, это файл-массив однотипных элементов. Но на практике «типизированный файл» может употребляться и в более широком смысле — как файл с однородными данными. При этом файл не обязан состоять исключительно из последовательности записей, и сами записи файла не обязаны иметь одинаковый размер. К примеру, в начале файла может быть записан заголовок (сигнатура, контрольная сумма, число записей, версия файла и т.п.), а за ним идти набор записей одинакового вида (скажем, три числа и строка), но записи будут иметь переменную длину (строка разной длины). Такой файл могут называть типизированным (в смысле однородности его данных), но надо понимать, что он не будет типизированным в смысле языка Pascal — и работать с ним нужно будет как с нетипизированным, двоичным файлом. Применение термина «типизированный файл» к файлам нерегулярной структуры не корректно. Примером такого файла является .exe файл: в нём содержится первичный заголовок, который ссылается на дополнительные заголовки, в нём есть секции кода и данных (произвольных размеров), оглавление ресурсов (и сами ресурсы) и т.п. Все части файла имеют разную длину и разную структуру.

Общие принципы работы с файлами Pascal

Работа с любыми типами файлов имеет общие элементы, которые мы сейчас и рассмотрим.

Во-первых, работа с файлами в стиле Pascal почти всегда следует одному шаблону (кратко):

  1. AssignFile
  2. Reset/Rewrite/Append
  3. работа с файлом
  4. CloseFile

Подробно:

  1. Вы начинаете с объявления файловой переменной нужного типа. В зависимости от выбранного вами вида файла, вам нужно использовать var F: TextFile; var F: file of Тип-Данных; или var F: file; (текстовый, типизированный и двоичный файл соответственно), где «Тип-Данных» является типом данных фиксированного размера.

    Вы ассоциируете переменную с именем файла. Делается это вызовом AssignFile . Которая имеет прототип: Примеры вызова: Как видите — вы можете указывать любое допустимое имя файла с путём или без, но по соображениям надёжности кода я рекомендую вам всегда указывать полностью квалифицированное имя файла.

    Параметр CodePage является необязательным и он появился только начиная с Delphi XE 2. Он применим только для текстовых файлов. Если он не указан, то используется DefaultSystemCodePage , что для «русской Windows» равно Windows-1251.

    Он указывает кодовую страницу для выполнения перекодировки текста перед записью и после чтения данных из файла. Например: Текстовый файл должен быть в указанной вами кодовой странице. Кодовая страница — любая из принимаемых функцией WideCharToMultiByte (или её аналога на не Windows платформах). Файл не может иметь BOM. Так что на практике эта возможность полезна только для открытия ANSI файлов в кодировке, отличной от системной, но не текстовых файлов в UTF-8 и UTF-16, которые почти всегда имеют BOM.

    Данная операция НЕ открывает файл. Она просто записывает имя файла в файловую переменную. Так что когда вы откроете файл в будущем (как правило — сразу же после этой операции), то будет открыт именно файл с указанным тут именем.

    Данная операция всегда выполняется успешно, но вам нужно быть внимательным и никогда не вызывать её для уже открытых файлов — это приведёт к утечке открытых описателей файла (фактически, AssignFile просто тупо затирает нулями переменную перед выполнением своей работы; так что, чтобы там у вас ни было — оно будет потеряно). Собственно, эта операция нужна не столько для ассоциации с файловым путём, сколько для инициализации переменной. Как правило, за всё время жизни файловой переменной, эта операция применяется к ней единственный раз — первым действием.

    В Pascal вместо AssignFile используется Assign . Это ровно эта же процедура, просто в Delphi Assign переименовали в AssignFile , чтобы избежать конфликтов имён с другим кодом, который тоже использует имя Assign (к примеру — форма, компоненты, да и любой TPersistent ). Assign всё ещё существует в Delphi, так что любой старый код может быть перекомпилирован в Delphi и он будет работать. Но для нового кода вам лучше использовать AssignFile .

    Далее файл открывается. Открывать файл можно в трёх режимах: чтение, запись и чтение-запись. Как несложно сообразить, при открытии файла в режиме чтения из него можно только читать, но не писать — и так далее. Таким образом, всего у файловой переменной может быть 4 режима: закрыт, открыт на чтение, открыт на запись и открыт на чтение-запись (любые другие значения указывают на отсутствие инициализации файловой переменной — т.е. закрытый файл): Открытие файла выполняется с помощью функций Reset , Rewrite и Append .

    Что касается общих моментов: Rewrite создаёт новый файл; Reset открывает существующий файл, а Append является модификацией Reset и открывает файл для дополнения: т.е. после открытия файла переходит в его конец для дозаписи данных. Append применима только к текстовым файлам.

    Используются все эти процедуры в целом одинаково:
    Хотя конкретные типы файлов имеют свои особенности использования (см. ниже), но обычно эти подпрограммы работают следующим образом:

    1. Rewrite создаёт новый файл. Если файл с таким именем уже существует, то он удаляется и вместо него создаётся новый файл. Если файловая переменная уже открыта к моменту вызова Rewrite , то файл закрывается и пересоздаётся. После открытия файл позиционируется на начало файла, а функция EOF возвращает True — указывая на конец файла.
    2. Reset открывает существующий файл в режиме, указываемом в глобальной переменной FileMode (по умолчанию — чтение-запись; возможные значения — только чтение, только запись и чтение-запись). Если файл не существует или не может быть открыть в нужном режиме (заблокирован, отказ доступа) — то возникнет ошибка. Если файловая переменная уже открыта к моменту вызова Reset , то файл закрывается и открывается заново. После открытия файл позиционируется на начало файла, а функция EOF возвращает True , если файл существует и имеет размер 0 байт или (обычно) False — если файл существует и имеет ненулевой размер.
    3. Append применимо только к текстовым файлам и будет рассмотрена ниже.

    Глобальная переменная FileMode является именно глобальной переменной, а потому установка доступа не является потоко-безопасным действием. В любом случае, переменная может содержать комбинацию (через «or») следующих флагов: Вы можете указать один флаг вида fmOpenXYZ и один флаг из второй группы. Первый флаг определяет режим открытия: только чтение ( fmOpenRead ), только запись ( fmOpenWrite ) или чтение-запись ( fmOpenReadWrite ), вторые флаги определяют режим разделения файла: без разделения ( fmShareExclusive ), запретить другим читать ( fmShareDenyRead ), запретить другим писать ( fmShareDenyWrite ) и без ограничений ( fmShareDenyNone ). И два флага являются специальными. Флаги разделения используют устаревшую deny-семантику MS-DOS, в отличие от современного API. См. также: взаимодействие флагов режима открытия и разделения.

    Здесь общие моменты заканчиваются и начинаются различия — все три процедуры имеют небольшие особенности в зависимости от используемого типа файловой переменной, так что мы подробнее рассмотрим их ниже.

    Затем идёт работа с файлом — чтение и/или запись данных. Эти операции специфичны для разных типов файловых переменных. Их мы отдельно рассмотрим ниже.

    В итоге после работы файл нужно закрыть. Делается это вызовом CloseFile : После этого файл будет полностью закрыт, все изменения полностью сброшены на диск, а файловую переменную можно использовать заново — связать её с другим файлом (через AssignFile ), открыть и т.д., но, как правило, с файловой переменной работают лишь один раз.

    Примечание: аналогично AssignFile (см. выше), CloseFile является переименованной Close , которая тоже всё ещё доступна, но чаще всего замещается другим кодом (к примеру — Close у формы). Всегда используйте CloseFile в новом коде.

Итак, на этом мы закончили разбор общих принципов работы с файлами Pascal. Но прежде чем перейти к обсуждению способов работы с каждым конкретным типом файловых переменных, нужно обсудить ещё один важный общий момент: обработку ошибок.

Обработка ошибок

Тут надо сказать, что обработка ошибок работы с файлами Pascal весьма запутывающа. Дело в том, что обработку ошибок подпрограммы ввода-вывода файлов Pascal производят в двух режимах — которые переключаются весьма нестандартно: опцией компилятора. Она называется I/O Checking и расположена на вкладке Compiling (Compiler в старых версиях Delphi) в опциях проекта в Delphi (Project/Options). Что ещё хуже — эта настройка может быть изменена прямо в коде, используя директиву компилятора. В коде можно использовать <$I+>для включения этой опции и <$I->для выключения.

Итак, если эта опция выключена (либо в коде стоит <$I->), то обработку ошибок любых функций по работе с файлами в стиле Pascal (кроме AssignFile , которая всегда успешна) нужно проводить так: вам доступна функция IOResult , которая возвращает статус последней завершившейся операции. Если она вернула 0 — то операция была успешно выполнена (файл открыт, данные записаны и т.п.). Если же она возвращает что-то иное — произошла ошибка. Какая именно ошибка — зависит от значения, которое она вернула, которое (значение) представляет собой код ошибки. Возможные коды ошибок можно посмотреть здесь (только не закладывайтесь на неизменность этих кодов и неизменность таблицы). Помимо ошибок ввода-вывода, вы можете получать и системные коды ошибок. Их список можно увидеть в модуле Windows. Откройте его и запустите поиск по «ERROR_SUCCESS» (без кавычек). А ниже вы увидите список системных кодов ошибок. Наиболее частые ошибки при работе с файлами — ERROR_FILE_NOT_FOUND, ERROR_PATH_NOT_FOUND, ERROR_ACCESS_DENIED, ERROR_INVALID_DRIVE, ERROR_SHARING_VIOLATION. Но вообще код может быть почти любым. В любом случае, вам доступен только код ошибки, но не её текстовое описание. Если произошла ошибка, то все последующие вызовы функций ввода-вывода будут игнорироваться, пока вы не вызовете IOResult .

Итак, это старый режим обработки ошибок, который пришёл в Delphi из языка Pascal. С ним код выглядит примерно так: Вызов функции очищает код ошибки, так что если вы хотите обработать ошибку, нужно поступать так: Поскольку ошибка блокирует вызовы функций ввода-вывода, то обработку ошибок удобно делать один раз — в конце работы. Например: Этот код основан на том факте, что если при работе с файлом возникнет ошибка (к примеру — при открытии файла в момент вызова Rewrite из-за того, что в Edit1 указано недопустимое имя файла), то все нижеследующие функции не будут ничего делать, а код ошибки будет сохранён до вызова IOResult .

Обратите внимание, что правило «ничего не делать при ошибке» не распространяется на CloseFile . Вот почему мы вставили обработку ошибок в самый конец, а не до CloseFile .

Окей, с этим способом всё. Теперь, если вы включаете опцию I/O Checking или используете <$I+>, то функция IOResult вам не доступна, а всю обработку ошибок берут на себя сами функции ввода-вывода: если при вызове любой из них возникнет ошибка — функция сама обработает её путём возбуждения исключения (если модуль SysUtils не подключен — то возбуждением run-time ошибки; подробнее — тут). Исключение имеет класс EInOutError : Собственно, код в этом режиме всегда будет выглядеть так: А при возникновении проблем — у вас будет исключение, которое в стандартной VCL Forms программе в конечном итоге обрабатывается показом сообщения:

Я допускаю, что вы можете не очень быть знакомы с работой с исключениями (и в таком случае вам можно начать с этой статьи), но я бы всё же рекомендовал использовать режим <$I+>по одной очень простой причине: с исключениями поведение по умолчанию — показ ошибки; в режиме <$I->— скрытие и игнорирование. Иными словами, если в <$I->по незнанию или из-за лени вы опустите обработку ошибок, а при выполнении программы ошибка всё же возникнет — вы останетесь ни с чем: код просто не работает и вы понятия не имеете почему. С исключениями же такое невозможно: вам нужно специально прикладывать усилия, чтобы скрыть ошибку, так что это не может произойти случайно, по недосмотру.

В любом случае, вот код приведения второго режима к шаблону первого:
На этом разговор об обработке ошибок при работе с файлами Pascal можно считать законченным.

Персональное примечание: напомню, что именно запутанность обработки ошибок в файлах Pascal привела к обнаружению вируса Virus.Win32.Induc.a.

В любом случае, это всё. И нам наконец-то можно приступить к обсуждению непосредственно работы с файлами.

Текстовые файлы

Итак, текстовый файл — это обычный файл, открываемый через переменную типа TextFile , для которого делается предположение о чисто текстовом содержимом. Тип TextFile в Delphi имеет псевдоним Text — это текстовый тип в Pascal. И снова: тип был переименован, старый Text всё ещё существует по соображениям совместимости, но в новых программах нужно использовать тип TextFile .

Когда вы открываете текстовый файл, его содержимое интерпретируется специальным образом: предполагается, что файл содержит последовательности читабельных символов, организованных в строки, при этом каждая строка заканчивается маркером end-of-line («конец строки»). Иными словами, текстовый файл трактуется не как просто file of Char , а с дополнительной смысловой нагрузкой. Для текстовых файлов есть специальные формы Read и Write , на которые мы посмотрим чуть позже.

В Delphi есть три стандартные глобальные переменные типа текстовых файлов: Input для чтения или Output для вывода — это специальные имена для стандартных каналов ввода-вывода; ещё есть ErrOutput — стандартный канал вывода ошибок, по умолчанию он направляется в канал вывода, но вызывающая программа может отделить его. Соответственно, Input открывается только для чтения и обычно представляет собой клавиатуру в консольных приложениях (если ввод не был перенаправлен), а остальные два — только для записи (и обычно представляют собой экран). Эти файловые переменные открываются автоматически. В Windows они открываются только для консольных программ, но на других платформах это может быть и не так.

В любом случае, для открытия текстовых файлов можно использовать Rewrite (создание), Reset (открытие) и Append (дозапись в существующий файл). Все три используются одинаково — им передаётся переменная файлового типа. Больше аргументов у них нет. Особенность текстовых файлов: Rewrite открывает файл (вернее — создаёт) в режиме только запись, Reset всегда открывает файл в режиме только чтение, а Append открывает файл в режиме только запись. Текстовые файлы нельзя открыть в режиме чтение-запись.
Append аналогична Reset , только устанавливает текущую позицию файла в конец и открывает в режиме записи, а не чтения.

После открытия вам доступны процедуры Write , WriteLn , Read и ReadLn для записи и чтения строк (варианты функций с *Ln допустимы только для текстовых файлов). Эти подпрограммы не являются настоящими функциями и процедурами, а представляют собой магию компилятора. Первым параметром у них идёт файловая переменная — она указывает файл, с которым будет производится работа (чтение строк или запись), а далее идёт произвольное число параметров — что пишем или читаем.


Если опустить файловую переменную — будет подразумеваться консоль ( Input и Output ). Это используется для ввода-вывода в консольных программах. Вам необязательно работать со стандартными каналами ввода-вывода именно через файлы Pascal, вы можете использовать и API.

Иными словами, вызовы этих функций имеют форму: Где «что-то» — это одна или более строковых или числовых переменных, указанных через запятую — как обычные аргументы. Мы рассмотрим это чуть позже.

При этом, краткая форма: эквивалентна: В Windows Input , Output и ErrOutput доступны только для консольных программ. Попытка использовать их в GUI приложении приведёт к ошибке (операция над закрытой файловой переменной). Это — частая ошибка новичков. Они забывают указать первым параметром файловую переменную. Эта ошибка не страшна в Windows, поскольку вы тут же увидите проблему при запуске программы, но достаточно коварна на других платформах, где стандартные каналы ввода-вывода могут быть открыты для всех программ. Таким образом, если вы забудете указать файловую переменную, то ваша программа будет работать, не выдавая ошибки — но будет работать неправильно.

Функции с постфиксом Ln отличаются от своих собратьев тем, что используют разделитель строк. Иными словами, Write записывает данные в файл, а WriteLn дополнительно после этого вписывает в файл перенос строки. Т.е. несколько вызовов Write подряд будут писать данные в одну строку. Аналогично, Read читает данные из файла, а ReadLn после этого ищет конец строки и делает переход к следующей строке в файле. В качестве разделителя строк используется умолчание для платформы, если вы пишете в файл, или Enter, если вы работаете с консолью. К примеру, для Windows разделителем строк является последовательности из двух символов #13#10 — известные как CR (carriage return) и LF (line feed), они имеют коды 13 и 10, соответственно. По историческим причинам выбор разделителя строк зависит от платформы. Вы можете изменить умолчание вызовом SetLineBreakStyle , но замечу, что работа с файлами другой платформы — это достаточная головная боль. Я не буду это подробно рассматривать. Часто наилучшее решение — предварительная нормализация данных и файлов. В частности, в Delphi есть функция AdjustLineBreaks.

Read читает все символы из файла вплоть до конца строки или конца файла, но она не читает сами маркеры. Чтобы перейти на следующую строчку — используйте ReadLn . Если вы не вызовите ReadLn , то все вызовы Read после встречи маркера конца строки будут возвращать пустые строки (для чисел — нули). Опознать конец строки или конец файла можно с помощью функций EoLn и EoF (или функций SeekEoLn и SeekEoF — и эти функции не следует путать с функцией Seek ). Если же читаемая строка длиннее, чем аргумент у Read / ReadLn , то результат обрезается без возбуждения ошибки. Но если вы читаете числа, то Read пропускает все пробелы, табуляторы и переносы строк, пока не встретит число. Иными словами, при чтении чисел они должны отделяться друг от друга пробелами, табуляторами или размещаться на отдельных строках. При чтении строк вы должны переходить на следующую строку сами.

Write записывает значения в файл. При этом каждый параметр у Write может быть строковым, логическим или числовым (включая числа с плавающей запятой) типом и иметь опциональный спецификатор форматирования значения. При этом все не текстовые данные конвертируются в строки перед записью в файл согласно указанным спецификаторам форматирования.

В общем случае параметр выглядит так:

Где в квадратных скобках указываются опциональные (необязательные) части. OutExpr — это само выражение для записи. Оно может быть переменной, константой или непосредственным значением. MinWidth и DecPlaces являются спецификаторами форматирования и должны быть числами. MinWidth указывает общую длину вывода и должно быть числом большим нуля. По необходимости слева добавляется нужное количество пробелов. А DecPlaces указывает число знаков после десятичной точки и применимо только при записи чисел. Если эти данные не указаны, то используется научный формат представления чисел. Форматирование значений при выводе — наследие Pascal, где не было функций форматирования строк. В современных программах предпочтительнее использовать функцию Format и её варианты. Этот современный вариант форматирования данных является стандартным решением в Delphi и предоставляет больше возможностей.

Write пишет значения друг за другом, без разделителей. Поэтому, когда вы записываете числа, вам нужно вставлять пробелы, табуляторы или использовать WriteLn .

При этом нужно быть аккуратным при чтении и записи смеси чисел и строк. Оптимальнее всего размещать каждое значение на отдельной строке в файле. Связано это с тем, что если вы записали в файл в одну строчку, скажем, число, текст и число, то при чтении из файла прочитается число, а затем текст — но в этот текст будет включено второе число — поскольку нет никакой возможности отличить текст от числа. Иными словами, при чтении строк читаются все данные до ближайшего маркера (конца строки или файла).

Далее необходимо заметить, что переменные текстового файлового типа имеют буфер. Все данные, записываемые в файл, на самом деле записываются в этот буфер. И лишь при закрытии файла или переполнения буфера данные сбрасываются на диск. Аналогичные действия производятся и при чтении файла. Это делается для оптимизации посимвольного ввода-вывода. Буфер можно сбросить и вручную в любой момент — с использованием подпрограммы Flush . По умолчанию размер буфера равен 128 байтам, но его можно изменить на произвольное значение вызовом SetTextBuf .

Текстовые файлы не поддерживают позиционирование.

Типизированные файлы

Типизированный файл — это очень простая БД в виде «array of что-то». Как уже было сказано, «что-то» должно иметь фиксированный размер в байтах, поэтому строки и динамические массивы хранить нельзя (но можно — короткие строки или статические массивы символов). Для открытия файлов доступны Rewrite и Reset . Здесь нет никаких особенностей по сравнению с вышеуказанными общими принципами. Запись и чтение из файла осуществляется с помощью Write и Read .

В отличие от текстовых файлов, типизированные и нетипизированные файлы поддерживают позиционирование. Вы можете установить текущую позицию в файле с помощью Seek . Процедура принимает два параметра — файл и номер позиции, на которую нужно переместиться. Положение отсчитывается не в байтах, а в размере записи файла. Иными словами, если вы работаете с, к примеру, file of Integer , то Seek(F, 0) переместит вас в начало файла, Seek(F, 1) — ко второму элементу (т.е. через 4 байта от начала файла), Seek(F, 2) — к третьему (через 8 байт), а Seek(F, FileSize(F)) — в конец файла. Т.е. функция FileSize тоже возвращает размер файла не в байтах, а в записях. Этот размер совпадает с размером в байтах только для file of byte и аналогичных типов — с однобайтовыми записями. Текущую файловую позицию (снова в записях) всегда можно узнать вызовом FilePos .

Ещё одной особенностью типизированных (и нетипизированных) файлов является функция Truncate . Она удаляет содержимое файла за текущей позицией. После её вызова функция EoF возвращает True .

Нетипизированные файлы

Нетипизированные файлы заполняют пробел в файлах Pascal, позволяя открывать произвольные файлы в двоичном режиме и осуществлять побайтовый доступ. Данный тип файлов не имеет никакого преимущества перед другими методами работы с файлами. В частности, почти всегда для нетипизированного доступа к файлу оказывается предпочтительнее TFileStream (мы рассмотрим его в другой раз).

Для нетипизированных файлов нам доступны Rewrite и Reset — равно как и для типизированных файлов. Но тут есть одно важное отличие: для нетипизированных файлов эти процедуры принимают два параметра. Первый параметр, как обычно, файловая переменная. А второй параметр — размер блока. Размер блока измеряется в байтах и является аналогом размера записи у типизированных файлов. Размер блока влияет на все подпрограммы работы с нетипизированными файлами, которые принимают размеры или позицию. Все они подразумевают указание размеров/позиции в блоках, а не байтах. Вы можете указать 1, чтобы производить измерения в байтах. Плохая новость — второй параметр является опциональным, его можно не указывать. Проблема тут в том, что если вы его не укажете, то размер блока по умолчанию будет 128 байт — не самое очевидное поведение.

Далее, для чтения и записи в нетипизированный файл вместо Read и Write используются функции BlockRead и BlockWrite . Обе они используются одинаково: первый параметр — файловая переменная, второй параметр — что пишем/читаем, третий параметр — сколько пишем/читаем (в блоках). Функция возвращает сколько реально было прочитано/записано. Если блок прочитан/записан целиком, то результат равен третьему параметру. У обеих функций есть перегруженные варианты, у которых результат функции возвращается четвёртым параметром.

Замечу, что второй параметр — нетипизированный. Это значит, что компилятор не выполняет проверок типа. И вам лучше бы не напутать, что туда передавать. Я в первую очередь сейчас говорю про указатели и динамические типы. К примеру: Фух, достаточно сложно. Но только если вы не понимаете как работают указатели.

Следует заметить, что достаточно часто вместо использования типизированного файла оказывается проще открыть файл в двоичном режиме (как нетипизированный) и загрузить/сохранить все данные за раз — вместо организации цикла. К примеру (обработка ошибок убрана):
Разумеется, нетипизированные файлы поддерживают позиционирование — ровно как и типизированные файлы. Только не забывайте про измерение в блоках, а не байтах.

Прочие подпрограммы и особенности

Хотя типы файловых переменных являются «чёрным ящиком», но внутренне они представлены записями TTextRec (для текстовых файлов) или TFileRec (для всех прочих файлов). Обе записи объявлены в модуле System : Вам не нужны эти записи для обычной работы, но иногда могут быть ситуации, когда вам нужно получать доступ к полям записи. Сделать это можно простым приведением типа: Примечание: структура записей зависит от версии вашей Delphi. Прежде чем использовать код, который работает с этими записями или объявляет аналогичные хак-записи — убедитесь, что код написан для вашей версии Delphi или адаптируйте объявления и код.

Практика

Все примеры ниже приводятся с полной обработкой ошибок для режима <$I+>. Примеры используют файл в папке с программой. Это удобно для тестирования и экспериментов, но, как указано ранее, этого нужно избегать в реальных программах. После тестирования и перед использованием кода в релизе вы должны заменить папку программы на подпапку в Application Data.

Практика: текстовые файлы

  1. Одно значение: Double Обратите внимание, что запись чисел всегда использует фиксированный формат числа — вне зависимости от региональных установок. Иными словами, конвертация чисел в строки и обратно выполняется процедурами Str и Val . Это имеет как плюсы, так и минусы.

С одной стороны, файл, созданный на одной машине, без проблем прочитается на другой.

С другой стороны, текстовый файл, очевидно, предназначен для редактирования человеком. Иначе зачем делать его текстовым? А человек вправе ожидать, что числа будут использовать «правильный формат». К примеру, для России это — использование запятой в качестве разделителя целой и дробной частей, а не точки. Простого решения этой проблемы для текстовых файлов в стиле Pascal нет.

Одно значение переменного размера: String Ключевой момент при записи данных с динамическим размером — размещать каждое такое значение на отдельной строке. Тогда размер данных = размеру строки.

Набор однородных значений: array of Double Обратите внимание, что записываем мы все значения в одну строчку — поэтому нам нужно вставить разделитель (пробел). Кроме того, мы могли бы также писать каждое число на отдельной строке — используя WriteLn . Тогда пробел был бы уже не нужен.

Что касается чтения, то мы могли бы читать ровно как и писать — циклом. Но это подразумевает, что у вас жёсткая структура файла. Т.е. записали все числа в одну строчку — значит, и человек после редактирования файла должен оставить все числа в одной строке. И так далее.

Поэтому вместо этого я показал, как вы можете считать файл произвольной структуры — лишь бы в нём были бы числа. Для этого мы делаем цикл чтения, пока не будет встречен конец файла ( while not SeekEof(F) do ), а в самом цикле считываем и добавляем в массив каждое число. При этом мы пользуемся тем фактом, что Read при чтении числа будет пропускать все пробельные символы, включая переносы строк. Вот почему нам не нужно явно вызывать ReadLn .

Ещё один момент — использование SeekEoF вместо просто EoF . Если бы мы использовали EoF , то тогда в массив добавлялся бы ноль в конец, если в конце файла стоит несколько пробелов или пустых строк.

Набор однородных значений переменного размера: array of String Здесь всё оказывается ещё проще — динамические данные должны размещаться на отдельной строке, так что мы просто используем WriteLn / ReadLn . Обратите внимание, что в этом случае, поскольку мы записываем строки, то нет никакой возможности отличить пустую строку в конце файла — часть ли это данных или просто человек случайно добавил её. Если в ваших данных пустые строки недопустимы, то вы можете заменить EoF на SeekEoF , как это сделано в предыдущем примере.

Запись — набор неоднородных данных: Тут всё достаточно прозрачно — каждое поле на новой строке.

Набор (массив) из записей — иерархический набор данных: Пример почти полностью эквивалентен предыдущему. Обратите внимание, что мы не используем каких либо разделителей между записями (элементами массива) — они идут сплошным потоком. Следующая запись начинается за предыдущей. Общая длина данных, как и ранее, определяется по размеру самого файла — через признак его конца.

Массив из записей внутри записи — составные данные: С вложением самих записей проблем не возникает — я даже не буду писать пример, т.к. он эквивалентен предыдущему. Просто выпишите в ряд WriteLn полей TCompose . Для полей-записи вместо одного WriteLn вам нужно будет написать несколько — по одному на каждое поле вложенной записи. Ну а ReadLn будут зеркальным отражением WriteLn .

Но вот вложение массива записей уже является непреодолимым препятствием для общего случая. Дело в том, что в текстовом файле нет возможности как-то указать размер вложенных данных, ведь обычная техника записи динамических данных в текстовый файл — использование разделителей (чаще всего — переноса строк). В частных случаях вы можете найти решение. Скажем, отделять поле Related пустой строкой от следующего элемента/поля. Но в общем случае приемлемого решения нет — вам нужно использовать нетипизированный файл. В некоторых случаях вы можете предложить введение формата в текстовый файл. Вроде INI, XML, JSON и т.п. Но на это мы посмотрим в следующий раз.

Практика: типизированные файлы

  1. Одно значение: Double В данном случае число записывается в двоичном формате, а не текстовом, так что все проблемы с региональными настройками обходят нас стороной.

Тут надо сделать примечание, почему вообще для этого примера выбран именно тип Double , а не Extended , который в Delphi является де-факто стандартом для чисел с плавающей запятой. Дело в том, что Extended зависит от платформы. Его размер может меняться. Так что если вы компилируете, скажем 32-битную и 64-битную программы — у них размер Extended будет разным. Что означает, что из одной программы вы не сможете прочитать данные, созданные в другой. Это не проблема, если вы планируете работу только в одной платформе — можете спокойно использовать Extended . В противном случае вам нужно использовать Double . Ну а если вам нужно прочитать Extended , созданный на другой платформе, то вы можете использовать тип TExtended80Rec (появился, начиная с Delphi XE2, где, собственно, и появилась поддержка нескольких платформ) вместо Extended .

Аналогично, по этой же причине вам следует избегать Integer и Cardinal при работе с типизированными файлами — потому что это generic-типы, размер которых может меняться. Используйте вместо них LongInt и LongWord соответственно.

Одно значение переменного размера: String . Сделать это для типизированных файлов невозможно. В типизированный файл (в смысле файловых типов языка Pascal) нельзя записывать данные переменного (динамического) размера. Для динамических данных нужно использовать либо текстовые, либо нетипизированные файлы. Что вы можете сделать — так это использовать какое-то ограничение.

К примеру, если брать строки, то вы можете использовать ShortString — это ограничит ваши данные ANSI и 255 символами. Ещё вариант — статический массив символов. Скажем array[0..4095] of Char ( AnsiChar / WideChar ). Обратите внимание, что запись в файл ShortString или массива из символов — это не аналог текстовых файлов, потому что кроме значимого текста в файле появляется т.н. padding — мусорные данные, не несущие смысловой нагрузки, а служащие для дополнения данных до нужного размера. Вы можете вызывать FillChar или ZeroMemory для предварительной очистки данных перед записью — чтобы визуально подчеркнуть неиспользуемость дополнения.

Надо понимать, что чем больше (по байтовому размеру) вы возьмёте тип, тем больше места у вас будет тратиться зря (на padding), если в основной массе у вас короткие строки. С другой стороны, если вы возьмёте недостаточно большой тип, то ваши данные будут обрезаться. Так что подобный вариант далеко не всегда возможен — а только, если вы можете предложить подходящее ограничения размера.

Здесь и далее я не буду приводить пример — он всегда эквивалентен предыдущему примеру с данными фиксированного размера. Только замените типы.


Набор однородных значений: array of Double Обратите внимание, что нам не нужен разделитель, поскольку элементы имеют фиксированный размер. Кроме того, нет проблемы с пробелами в конце, ранее решаемой SeekEoF . Кроме того, фиксированность элементов позволяет узнать длину массива заранее — по размеру файла, что в итоге позволяет написать более эффективный код.

Запись — набор неоднородных данных: Аналогично второму примеру, записать неоднородные данные в типизированный файл невозможно. Если вы объявите file of LongWord , то вы не сможете записать в него строку и наоборот. В общем, нужно использовать текстовые или нетипизированные файлы.

Вы можете подумать, что вы могли бы объявить file of TData — ну, с заменой динамических строк на фиксированные аналоги, конечно же. А затем использовать первый пример. Да, это будет работать для конкретного объявления TData , но только этот пример — не на запись в файл record -а, а на запись неоднородных данных.

Два остальных примера (с массивами записей и вложенными записями) также не дадут ничего нового — либо вы используете фиксированные элементы, и тогда эти примеры сводятся к предыдущим, либо — нет, и тогда использование типизированных файлов невозможно.

Практика: нетипизированные файлы

  1. Одно значение: Double
    Обратите внимание на 1 у Rewrite / Reset . Конечно, мы могли бы использовать вместо неё SizeOf(Double) . Но это фактически означало бы, что мы используем нетипизированный файл как типизированный. А в чём тогда смысл примера?

Одно значение переменного размера: String Данный случай прост — размер данных определяется по размеру файла. Для примера я выбрал AnsiString , а не String по друм причинам — во-первых, String — это псевдоним либо на AnsiString , либо на UnicodeString , в зависимости от версии Delphi. Иными словами, тут получается ситуация, аналогичная ситуации с Extended в разделе примеров для типизированных файлов. Так что вам нужно использовать явные типы — AnsiString , WideString (или UnicodeString ), а не String , иначе файл, созданный в одном варианте программы, нельзя будет прочитать в другом варианте программы.

Во-вторых, используя AnsiString , я показал, как вы можете загрузить в строку весь файл целиком, «как есть». Хотя, если подобный подход использовать в реальных программах, то уж лучше использовать array of Byte или хотя бы RawByteString — чтобы подчеркнуть двоичность данных.

Набор однородных значений: array of Double Данный пример эквивалентен примеру с типизированными файлами. Только теперь мы используем двоичный доступ, без учёта типа — так что нам приходится указывать явно все размеры. Вообще говоря, это общий принцип — работа с нетипизированными файлами и фиксированными размерами эквивалента типизированным файлам с учётом коэффициента размера.

И снова, благодаря фиксированности размеров элементов, мы можем установить размер массива ещё до чтения из файла.

Обратите внимание, что не имеет значения, какой индекс используется внутри выражения у SizeOf . Более того, не требуется даже наличие (существование) этого элемента. Это потому, что мы не обращаемся к нему — мы только просим у компилятора его размер. Это, по сути, константа. Так что всё выражение вообще не вычисляется — оно просто заменяется числом. Это удобный трюк для написания подобного кода, потому что это удобнее, чем писать тип явно: SizeOf(Double) . Почему? А что, если мы изменим объявление типа с Double на Single ? И забудем обновить SizeOf ? Тогда это приведёт к порче памяти — т.к. писаться или читаться будет больше, чем реально есть байт в элементе. Это выглядит не очень страшно для массива из Double , но рассмотрите вариант, скажем, строки — изменение размера Char гораздо более вероятно. А вот если мы используем форму SizeOf как в примере, то такой проблемы не будет — размер изменится автоматически.

Набор однородных значений переменного размера: array of String С записью набора динамических данных возникает проблема — как отличить один элемент от другого? Мы не можем более использовать переход на другую строку, как это было с текстовыми файлами. Тут есть несколько вариантов.

Самый простой и очевидный — ввести разделитель данных. Т.е. элементы отделяются друг от друга специальным символом. В качестве такого чаще всего выступает нулевой символ (#0) — это аналог разделителя строк в текстовых файлах. Тогда чтение-запись сведётся к примеру два. Но я не стал показывать этот путь, т.к. он очевидно вводит ограничение на возможные данные: теперь данные не могут содержать в себе разделитель (каким бы вы его ни выбрали). Конечно, вы можете его экранировать, но гораздо проще будет выбор другого подхода.

И я его показал — это явная запись размера данных до записи самих данных. Т.е. мы пишем два значения для каждого элемента: длину и сами данные.

Кроме того, в этом же примере показано, как можно сделать так, чтобы внутри программы работать с хорошо знакомым String , а в файле хранить фиксированный тип ( AnsiString / RawByteString или WideString / UnicodeString ). Вообще говоря, даже если вы работаете на Delphi 7 или любой другой версии Delphi до 2007 включительно — я бы рекомендовал всегда писать Unicode-данные в формате WideString во внешние хранилища.

Обратите внимание, что в качестве счётчика длины используется LongInt , а не Integer — по причинам, указанным выше для типизированных файлов: String , Extended , Integer и Cardinal могут менять свои размеры в зависимости от окружения — поэтому мы используем другие типы, которые гарантировано всегда имеют один и тот же размер.

Запись — набор неоднородных данных: В отличие от типизированных файлов, для нетипизированных файлов нет никаких проблем с записью неоднородных данных — вы просто пишете одно за другим. Данные фиксированного размера самодокументируются, а для динамических данных вы пишете сначала их размер, а затем сами данные. При чтении повторяете всё это в обратном порядке.

Кстати, я бы вынес запись динамических данных в отдельные служебные подпрограммы: Тогда чтение-запись свелись бы к: Выглядит существенно проще и красивее, не так ли? Иллюстрация силы выделения кода в подпрограммы.

Набор (массив) из записей — иерархический набор данных: Для начала хочу сразу же заметить, что странное выражение для поля Salary сделано для обхода бага Delphi. Вообще, там должно стоять просто BlockWrite(F, Values[Index].Salary, SizeOf(Values[Index].Salary)) , но в настоящий момент это выражение даёт ошибку «Variable required», поэтому используется обходной путь: мы берём указатель и разыменовываем его. Вообще говоря, это NOP-операция. А смысл её заключается в потере информации о типе. Это достаточно частый трюк, когда мы хотим запустить свои шаловливые руки под капот языка, минуя информацию типа, но в данном случае он используется для более благих целей: обхода бага компилятора. Вы можете использовать BlockWrite(F, Values[Index].Salary, SizeOf(Values[Index].Salary)) , если ваша версия компилятора это позволяет, или просто выбрать другой тип данных (не Currency ).

В любом случае, надо заметить, что достаточно часто при записи/чтении массива записей новички пытаются сделать такую вещь, как запись элемента целиком ( BlockWrite(F, Values[Index], SizeOf(Values[Index])) ). Это будет работать для записей фиксированного размера, не содержащих динамические данные (указатели). Ровно как это работает для типизированных файлов. Но если в записях у вас встречаются строки, динамические массивы и другие данные-указатели, то этот подход не будет работать. Собственно, если вы используете типизированные файлы, то компилятор даже не даст вам объявить такой тип данных ( file of String , например, или file of Запись , где Запись содержит String ). Но суть нетипизированных файлов — в прямом доступе, минуя информацию типа. Так что по рукам за это вам никто не даст. Вместо этого код будет просто вылетать или давать неверные результаты. А проблема тут в том, что для динамических данных, поле — это просто указатель. Записывая элемент «как есть» вы запишете в файл значение указателя, но не данные, на которые он указывает. Запись в файл произойдёт нормально, но в файле вы не найдёте своих строк. Чтение из файла тоже пройдёт отлично. Но как только вы попробуете обратиться к прочитанной строке — код вылетит с access violation, потому что указатель строки указывает в космос, на мусор.

Аналогично обсуждению типизированных файлов, самый простой способ решения проблемы (но не всегда достаточный) — замена String в записях на ShortString или статический массив символов. Я не буду рассматривать этот вариант, т.к. он сводится к предыдущим примерам с записью данных фиксированного размера.

Вместо этого в примере я показал уже известную технику: запись длины строки вместе с её данными. Это избавляет вас от всех недостатков ShortString /массива символов, но даёт новый недостаток: теперь вы не можете сохранить данные одной строчкой, вам нужно писать их поле-за-полем.

Массив из записей внутри записи — составные данные: Как видите — здесь нет никаких проблем, вы просто соединяете воедино техники из предыдущих примеров. Мы используем технику с записью счётчика длины для динамических данных в двух местах: при записи строк и при записи массивов (поле Related ).

Кроме того, хотя я мог бы написать весь код в цикле, друг за другом, я всё же выделил новые подпрограммы — исключительно ради удобства. Код теперь выглядит компактно и аккуратно. Он прозрачен и его легко проверить. А если бы я внёс под из подпрограмм в главные циклы, то получилась бы слабочитаемая мешанина кода.

Заметьте, что вы всё ещё должны писать записи по отдельным полям. И если вы меняете объявление записи — вам лучше бы не забыть поменять код, сериализующий и десериализующий запись.

Из последнего утверждения следует, что обеспечение обратной совместимости указанными методами (чтение файла одной версии программы в более новой версии программы и наоборот) — большая головная боль. Так просто она не обеспечивается, вам специально нужно реализовывать поддержку обратной совместимости. Проще всего это делать на нетипизированных файлах. Вы пишете в начало файла заголовок, в котором указываете версию содержимого файла. Тогда любая программа сможет прочитать заголовок и определить версию данных. А дальше — либо читать их, либо откинуть как не поддерживаемые. Впрочем, это разговор для другого раза.

Преимущества и недостатки файлов в стиле Pascal

Плюсы:

  • Отлично подходят для начального изучения языка
  • Вы наверняка знаете, как с ними работать, ибо это первый способ работы с файлами, который все изучают
  • Удобство работы с (текстовыми) данными: форматирование и переменное число аргументов
  • Гибкий подход, позволяющий работать с текстовыми, типизированными и произвольными данными
  • Встроенная буферизация даёт прирост производительности при записи небольших кусочков из-за экономии на вызовах в режим ядра
  • Могут быть расширены на поддержку любых файловых устройств, а не только дисковых файлов (т.е. IPC, pipes, сетевых каналов и т.п.) — путём написания своих адаптеров ввода-вывода, называемых «Text File Device Drivers». Подробнее см. Text File Device Drivers в справке Delphi

Минусы:

  • Необходимость ручной сериализации данных
  • Неудобная (и неоднозначная) обработка ошибок
  • Поведение кода зависит от директив компилятора
  • Нет поддержки Unicode и кодировок (улучшено начиная с Delphi XE2)
  • Проблемы с обработкой больших файлов (более 2 Гб)
  • Проблемы с глобальными переменными
  • Проблемы с многопоточностью
  • В некоторых случаях требуется ручной сброс буфера
  • Недостаточная гибкость для некоторых задач
  • Нестандартный код

Пояснение по последнему пункту: файлы Pascal пришли в Delphi из её предшественника. В них есть странная обработка ошибок, волшебные функции с переменным числом параметров, нестандартное для языка форматирование строк — всё это выбивается из обычного кода Delphi, оно выглядит иначе. Разнородный код — обычно это не хорошо.

Вывод: это отличный выбор для изучения языка и работы с файлами, но лично я для практических задач предпочитаю держаться подальше от файлов в стиле Pascal, но кто-то может считать это удобным способом. Если текстовые файлы ещё как-то оправдывают своё существование (если вы закроете глаза на их минусы), то типизированные и нетипизированные файлы практически зеркалируют потоки данных, но не имеют над ними никаких преимуществ.

TextFile — Тип Delphi

Delphi 4 для начинающих. Часть 3
Разгребаясь с письмами читателей, я решил начать следующую статью с работы с файлами и строковыми переменными. На такой ход меня натолкнуло следующее письмо: «Я только начал программировать на Delphi. У меня к вам большая просьба: при создании различных программ часто приходится работать с файлами (создавать, удалять, копировать, переписывать файлы), не могли бы вы в ближайшем номере «Компьютерной газеты» мне рассказать о методах работы с файлами». Что ж, желание просящих выполняю.

Подавляющее большинство приложений, с которыми работает пользователь, должно как-то сохранять результаты своей работы, сохранять изменения в настройках. Для сохранения настроек приложения зачастую используют системный реестр, а вот для сохранения конечных данных приходится использовать файлы или базы данных. О базах данных мы поговорим в будущих статьях, а о работе с файлами я поведаю Вам уже сейчас.

Для использования файлов в приложении разработчику приходится решать множество задач. Основные из них – это поиск необходимого файла и выполнение с ним операций ввода/вывода.

Основные принципы и структура файловой системы мало изменились еще со времен MS-DOS. Если не принимать во внимание способы защиты файлов и организацию их хранения на уровне кластеров, то все остается без изменений вот уже скоро двадцать лет. Новые варианты файловых систем (FAT32, NTFS) не изменяют главного – понятия файла и способов обращения к нему. Поэтому современный программный код Delphi, например, для чтения данных из файла, удивительно похож на аналогичный, написанный, к примеру, на Turbo Pascal 4.0.

При организации операций файлового ввода/вывода в приложении большое значение имеет тип используемых данных. В основном это строки, но встречаются числовые данные или структурированная информация, например, записи или массивы данных. О типе хранимых в файле данных необходимо знать заранее. Для работы с файлами необходимо для начала описать тип файла. Для этого используются специальные файловые переменные. Они делятся на нетипизированные и типизированные. В Delphi имеется одна нетипизированная переменная. Для ее обозначения используется ключевое слово file :

Такие файловые переменные используются для организации быстрого и эффективного ввода/вывода безотносительно к типу данных. При этом подразумевается, что данные читаются или записываются в виде двоичного массива. Для этого используются специальные процедуры блочного чтения и записи.

Типизированные файловые переменные обеспечивают ввод/вывод с учетом конкретного типа данных. Для их объявления используется ключевое слово file of, к которому добавляется конкретный тип данных. Например, для работы с двоичным файлом файловая переменная будет иметь вид:

v ar ByteFile: file of byte;

При этом можно использовать любые типы фиксированного размера, за исключением указателей. Разрешается использовать структурные типы, если их составные части удовлетворяют названному выше ограничению. Например, можно создать файловую переменную для записи:


type Country = record

var CountryFile: file of Country;

Для работы с текстовыми файлами используется специальная файловая переменная TextFile или Text :

Теперь, после рассмотрения типов файлов, рассмотрим две наиболее распространенные операции, выполняемые с файлами: чтение и запись. Для их выполнения применяются специальные функции файлового ввода/вывода.

Итак, для выполнения операции чтения и/или записи необходимо выполнить следующие действия:

  1. Объявить файловую переменную;
  2. При помощи функции AssignFile связать эту переменную с требуемым файлом;
  3. Открыть файл при помощи функций Append, Reset или Rewrite;
  4. Выполнить операции чтения и/или записи. При этом, в зависимости от сложности задачи и структуры данных, может использоваться целый ряд вспомогательных функций.
  5. Закрыть файл при помощи функции CloseFile .



В качестве примера рассмотрим небольшой фрагмент исходного кода программы:

If OpenDlg1.Execute then AssignFile(F, OpenDlg1.fileName) else exit;

While Not EOF(F) do

Если в диалоге OpenDlg1 был выбран файл, то его имя связывается с файловой переменной F при помощи процедуры AssignFile . В качестве имени файла всегда рекомендуется передавать полное имя файла (включая путь к нему). Как раз в таком виде возвращают результат выбора файла диалоги работы с файлами, описанные в материале прошлого номера. Затем при помощи процедуры Reset этот файл открывается для чтения и записи. В цикле осуществляется чтение из файла текстовых строк и запись их в компонент TMemo . Процедура Readln осуществляет чтение текущей строки файла и переходит на следующую строку. Цикл выполняется до тех пор, пока функция EOF не сообщит о достижении конца. По завершении цикла файл закрывается.

Теперь остановимся поподробнее на назначении функций, используемых для файлового ввода/вывода. Открытие файла может осуществляться тремя процедурами:


procedure Reset(var F [: File; RecSize: Word ] );
— открывает существующий файл для чтений и записи, текущая позиция устанавливается на первой строке файла;


procedure Append(var F: Text);
— открывает файл для записи информации после его последней строки, текущая позиция устанавливается на конец файла;

procedure Rewrite(var F: File [; Recsize: Word ] ); — создает новый файл и открывает его, текущая позиция устанавливается в начало файла. Если файл с таким именем уже существует, то он перезаписывается.

Чтение данных из файла выполняют процедуры Read и Readln.


procedure Read( [ var F: Text; ] V1 [, V2. Vn ] ); —
для текстовых файлов;


Procedure Read(F, V1 [, V2,…,Vn ] );
— для типизированных файлов.

При одном вызове процедуры можно читать данные в произвольное число переменных. Естественно, тип переменных должен совпадать с типом файла. При чтении в очередную переменную читается ровно столько байт из файла, сколько занимает тип данных. В следующую переменную читается столько же байт, расположенных следом. После выполнения процедуры текущая позиция устанавливается на первом непрочитанном байте. Аналогично работают несколько процедур Read для одной переменной, выполненных подряд.

procedure Readln([ var F: Text; ] V1 [, V2, . Vn ]); — считывает одну строку текстового файла и устанавливает текущую позицию на следующую строку. Если использовать процедуру без переменных, то она просто передвигает текущую позицию на следующую строку.

Процедуры для записи данных в файл Write и Writeln работают аналогично.

Для контроля за текущей позицией в файле применяются две основные функции. Функция EOF возвращает значение True, если достигнут конец файла. Функция EOLN сигнализирует о достижении конца строки. В качестве параметра в функции необходимо передавать файловую переменную.


procedure Seek(var F; N: Longint); —
обеспечивает смещение текущей позиции на N элементов. Размер одного элемента в байтах зависит от типа данных файла (от типизированной переменной).

Рассмотрим теперь режим блочного чтения и записи в файл. Он обеспечивает ввод/вывод данных между файлом и областью адресного пространства (буфером). Этот режим отличается значительной скоростью, причем скорость пропорциональна размеру одного передаваемого блока – чем больше блок, тем больше скорость.

Для реализации этого режима необходимо использовать только нетипизированные файловые переменные. Размер блока определяется в процедуре открытия файла ( Reset, Rewrite, Append ). Непосредственно для выполнения операций используются процедуры BlockRead и BlockWrite . Процедура


procedure BlockRead(var F: File; var Buf; Count: Integer [; var AmtTransferred: Integer]);

выполняет запись блока из памяти в файл. При этом параметр F содержит нетипизированную файловую переменную, связанную с нужным файлом, параметр Buf определяет переменную (число, строку, массив, структуру), в которую читаются байты из файла, параметр Count определяет число считываемых блоков, а параметр AmtTransferred возвращает число реально считанных блоков.

В блочном режиме чтения/записи размер блока необходимо выбирать таким образом, чтобы он был кратен размеру одному значению типа, который хранится в файле. Например, если в файле хранятся значения типа Double (8 байт), то размер блока может быть равен 8, 16, 24, 32 и т.д. Фрагмент исходного кода блочного чтения из такого файла выглядит так:

DoubleArray: array [0..255] of Double;

If OpenDlg.Execute then AssignFile(F, OpenDlg.FileName) else Exit;

BlockRead(F, DoubleArray, 32, Transfered);

ShowMessage(‘Считано ‘+IntToStr(Transfered)+’ блок(-а,-ов)’);

Как видно из примера, размер блока установлен в процедуре Reset и кратен размеру элемента массива DoubleArray, в который считываются данные. В переменную Transfered возвращается число считанных блоков. Если размер файла меньше заданного в процедуре BlockRead числа блоков, ошибка не возникает, а в переменную Transfered передается число реально считанных блоков.

Обратите внимание, что для процедуры BlockRead организовывать цикл для чтения нескольких блоков нет необходимости.


procedure BlockWrite(var f: File; var Buf; Count: Integer [; var AmtTransferred: Integer]);

Еще одна часто выполняемая с файлом операция – поиск файлов в заданном каталоге. Для организации поиска и отбора файлов используются специальные процедуры, а также структура, в которой сохраняются результаты поиска.

Эта запись обеспечивает хранение характеристик файла при удачном поиске. Дата и время создания файла хранятся в формате MS-DOS, поэтому для получения этих параметров в формате TDateTime необходимо использовать функцию


function FileDateToDateTime(FileDate: Integer): TDateTime;

Обратное преобразование выполняет функция


function DateTimeToFileDate(DateTime: TDateTime): Integer;

Свойство Attr может содержать комбинацию следующих значений:

faReadOnly – только для чтения;

Для определения параметров файла используется оператор AND :


If SearchRec.Attr AND faReadOnly) > 0 then ShowMessage(‘
файл только для чтения’);

Непосредственно для поиска файлов используются функции FindFirst и FindNext .

function FindFirst(const Path: string; Attr: Integer; var F: TSearchRec): Integer;

находит первый файл, заданный полным маршрутом Path и параметрами Attr . Если заданный файл найден, функция возвращает 0, иначе – код ошибки. Параметры найденного файла возвращаются в записи F типа TSearchRec .

function FindNext(var F: TSearchRec): Integer;

используется для поиска файла. При этом используются параметры поиска, заданные последним вызовом функции FindFirst . В случае удачного поиска возвращается 0.

Для освобождения ресурсов, выделенных для выполнения поиска, применяется функция

procedure FindClose(var F: TSearchRec);

В качестве примера организации поиска файлов рассмотрим фрагмент исходного кода, в котором маршрут поиска файлов задается в однострочном текстовом редакторе DirEdit, а список найденных файлов передается в компонент TListBox.

FindFirst(DirEdit.Text, faArchive + faHidden, SearchRec);

While FindNext(SearchRec) = 0 do

Напоследок, как и обещал в прошлом номере, немного о создании дополнительных форм. Итак, для создания дополнительной формы необходимо воспользоваться пунктом меню New Form ( File/New Form ) или кнопкой New Form на панели инструментов. Перед Вами появится новая форма проекта с названием Form2 . При запуске программы эта форма не будет активизирована. Для ее запуска необходимо воспользоваться процедурой Show из кода исходной программы. Список существующих форм и возможность активизация форм проекта доступны при нажатии на кнопку ViewForm на панели инструментов или при нажатии на пункт Forms… ( View/Forms… ). Таким образом, необязательно загромождать экран бесконечными формами проекта, когда на данный момент из них необходима только одна. Достаточно закрыть ненужные формы, а при необходимости активизировать их при помощи диалога ViewForm .

TextFile — Тип Delphi

For saving one or more strings to a text file, you code the following steps:

  1. Declare a variable of the type TextFile.
  2. Use the command AssignFile to connect the TextFile variable to a physical file on disk.
  3. «Open» the text file for writing with the command Rewrite. This means, that a new text file will be created on the disk. If there is already a file with the same name, it will be overwritten.
  4. Write a string to the file with WriteLn. You can repeat this step as many times as you want.
  5. Finally, «close» the text file with CloseFile.

The example below shows how to save the contents of two Edit-components to a text file C:\Test\Data.txt

Reading strings from a text file is very similar, but in order to be on the safe side, you need an extra step. Before trying to read, you have to check if the file exists. You also need an extra variable to receive the strings that you read from the file. This are the steps:

  1. Declare two variables, one of the type TextFile and one of the type String.
  2. If the file exists, continue with step 3. If not, it ends here. Optionally, you can show an error message to the user.
  3. Use AssignFile to connect the TextFile variable to a physical file.
  4. «Open» the text file for reading with the command Reset.
  5. Read a string from the file into the string variable, with the command ReadLn. Repeat this step to read the next line(s) of the file.
  6. Finally, «close» the text file with CloseFile.
Илон Маск рекомендует:  Свойство padding
Понравилась статья? Поделиться с друзьями:
Кодинг, CSS и SQL