Занятие второе
Файловые операции
Любая операционная система, а Windows не является исключением, должна включать в себя сервис, ответственный за файловый ввод и вывод данных. У Windows достаточно большой набор вызовов API, управляющих файлами и данными в них. Ранее, в 16-разрядных версиях, Windows использовал функции API, начинающиеся с символа "_": _hread, _hwrite, _lclose, _lcreat, _llseek, _lopen, _lread, _lwrite. Такой набор обеспечивал все, что требовалось нормальному программисту. С приходом Win32 API ситуация несколько переменилась. Новые 32-разрядные функции заменили 16-разрядные. Разумеется, можно использовать в работе и старые, проверенные, но тогда потеряются два огромных плюса от использования Win32 API - скорость дискового обмена данными и переносимость программ на другие платформы, использующие интерфейс программирования Win32, среди которых PowerPC, Alpha и MIPS. Тем более что уже ходят слухи о переносе Win32 в среду Unix. Исходя из этих соображений, давайте изучим именно 32-разрядные функции работы с файлами.
Начнем с того, что создадим каркас тестовой программы, используя пакет Borland C++ Builder. Программа должна содержать две формы: Form1 и Form2. Form1 - главная форма программы, на которой необходимо расположить мемо-поле, используемое в дальнейшем в качестве интерфейса между вами и файловыми функциями Windows.
Form2 будет служить диалоговой панелью, в которой мы будем вводить имена файлов для экспериментов. Ее содержимое состоит из строки ввода и кнопки Ok.
В исходный текст для формы Form2 требуется добавить функцию AskFileName(), которая запускает форму модально, ждет, пока пользователь введет имя файла и по закрытию возвращает введенную строку с именем файла:
System::AnsiString __fastcall TForm2::AskFileName(void) { ShowModal(); return Edit1->Text; }
Здесь Edit1 - ссылка на строку ввода, а Text - свойство, через которое можно считать и записать в эту строку ввода какой-либо текст. Обратите внимание на то, что свойство Text возвращает строку как класс типа System::AnsiString. Этот класс инкапсулирует текстовую строку и все операции для работы с ней, что в общем-то очень удобно.
Для кнопки Ok создадим следующий обработчик:
void __fastcall TForm2::Button1Click(TObject *Sender) { Close(); }
Вся его работа - закрыть форму Form2 по нажатию кнопки.
Перейдем к главной форме программы. К настоящему времени на ней должны быть мемо-поле и кнопка создания нового файла. К ней мы привяжем следующий обработчик нажатия:
void __fastcall TForm1::Button1Click(TObject *Sender) { System::AnsiString FileName; FileName = Form2->AskFileName(); if( NewFile(FileName) ) Memo1->Text = "Файл успешно создан!"; else Memo1->Text = "Произошла ошибка в момент создания файла Возможно, файл с таким именем уже существует"; }
Одна из вызываемых функций, AskFileName(), вам уже знакома, а вторую функцию, NewFile(), еще предстоит написать:
bool __fastcall TForm1::NewFile(System::AnsiString Name) { HANDLE hFile; hFile = CreateFile( Name.c_str(), // имя файла, преобразуемое к типу char* GENERIC_READ|GENERIC_WRITE, // доступ для чтения и записи 0, // файл не может быть разделяемым NULL, // дескриптор файла не наследуется CREATE_NEW, // создать новый файл, если его не существует FILE_ATTRIBUTE_READONLY, // файл имеет атрибут //"только для чтения" NULL); // всегда NULL для Windows 95 if( hFile != INVALID_HANDLE_VALUE ) { CloseHandle( hFile ); return true; } else { CloseHandle( hFile ); return false; } }
Эта функция принимает в качестве аргумента класс строки System::AnsiString и, передавая его вызову Win32 API под названием CreateFile(), преобразует его в простую строку символов путем вызова метода c_str() класса System::AnsiString. Мы будем пользоваться методом c_str() постоянно. Новый файл создается вызовом CreateFile(), который, принимая массу параметров, создает файл, открывает его и возвращает его дескриптор. Создание нового файла - это одно из применений функции CreateFile(). С ее помощью можно открывать уже существующие файлы и консоли для консольных приложений, усекать их, открывать каталоги. Это все задается пятым параметром. Поскольку нам нужно всего лишь создать новый файл, этот параметр задается как CREATE_NEW. Вновь создаваемый файл открывается как для чтения, так и для записи, о чем свидетельствует второй параметр функции. Третий и четвертый параметры редко имеют какое-либо практическое применение в Windows 95 и требуются в основном при создании систем с разделением доступа на базе Windows NT, поэтому мы не будем на них останавливаться. Шестой параметр весьма интересен. Он определяет, какие атрибуты будут установлены для создаваемого файла. В нашем случае будет присвоен атрибут Read-Only, позволяющий только чтение файла без записи в него. Далее вы увидите, почему это нам необходимо. Один важный момент, о котором хотелось бы сказать. Если у вас есть дескриптор файла, не забудьте закрыть его по окончании работы. Это делается вызовом CloseHandle() Win32 API. По его команде файл закрывается и дескриптор файла освобождается. Вообще-то Windows самостоятельно закрывает все файлы и освобождает дескрипторы, как только вы завершаете выполнение программы, но правило чистоты программирования гласит: сделай все сам и не оставляй ничего системе. Если произойдет сбой при автоматическом закрытии дескрипторов (а он когда-нибудь произойдет), ваши данные могут исчезнуть вместе с зарядом микросхем оперативной памяти, так что уж лучше перестраховаться.
Скомпилируйте полученную программу и попытайте ее на предмет создания файла. Все идет гладко,
но стоит повторно создать файл, не удалив с диска его предыдущую копию, как вы получите сообщение об ошибке.
Надо сказать, файловые функции Win32 довольно "капризны" и чутко реагируют на неправильные действия программиста, что страхует его от возможных неприятностей.
Следующая операция, о которой мы поговорим, это OpenFile(). Как явствует из названия, она открывает файл. Помимо этого, OpenFile() умеет создавать и удалять файлы. Во многом назначения этой функции пересекаются с CreateFile(), и оставлена она для совместимости с ранними версиями Windows. Поэтому везде, где это возможно, лучше пользоваться CreateFile(). В нашей программе мы рассмотрим только, как открывать файлы с помощью OpenFile().
Добавьте к главной форме кнопку Open File и привяжите к ней следующий обработчик нажатия:
void __fastcall TForm1::Button2Click(TObject *Sender) { System::AnsiString FileName; FileName = Form2->AskFileName(); if( FileOpen(FileName) ) Memo1->Text = "Файл успешно открыт!"; else Memo1->Text = "Произошла ошибка в момент открытия файла Возможно, такого файла не существует"; }
Как видите, он немногим отличается от обработчика TForm1::Button2Click, точнее одной строкой, где происходит вызов метода FileOpen(). Сам же этот метод выглядит следующим образом:
bool __fastcall TForm1::FileOpen(System::AnsiString Name) { HFILE hFile; OFSTRUCT os; os.cBytes = sizeof os; hFile = OpenFile( Name.c_str(), // имя файла, преобразуемое к типу char* &os, // указатель на буфер с информацией о файле OF_READ); // файл открыт для чтения if( hFile != HFILE_ERROR ) { CloseHandle( HANDLE(hFile) ); return true; } else { CloseHandle( HANDLE(hFile) ); return false; } }
Функция OpenFile() принимает всего три аргумента: имя открываемого файла, указатель на структуру OFSTRUCT и флаг режима открытия файла. Структура OFSTRUCT заполняется данными об открытом файле. Она предоставляет такую информацию, как свойства файла, его размер и т. д. Третий параметр, флаг открытия файла, устанавливается в режим чтения, дабы избежать случайного повреждения данных. Разумеется, вы можете установить и другие режимы.
Далее производится проверка полученного от функции OpenFile() дескриптора. При правильной работе программы в мемо-поле будет выведена строчка, говорящая об успехе операции.
Если произошла ошибка, то с высокой долей вероятности можно считать, что запрашиваемого на открытие файла просто не существует.
Результат наших исследований возвращается как булевская константа.
Создание каталога весьма походит на создание файла, но несколько проще. Чтобы наша программа "научилась" делать эту операцию, разместим третью кнопку Create Dir на главной форме и добавим обработчик нажатия:
void __fastcall TForm1::Button3Click(TObject *Sender) { System::AnsiString DirName; DirName = Form2->AskFileName(); if( NewDir(DirName) ) Memo1->Text = "Каталог был создан!"; else Memo1->Text = "Произошла ошибка при создании каталога Возможно, каталог с таким именем уже существует"; }
Собственно создание каталога происходит внутри метода NewDir, который вызывает функцию API CreateDirectory():
bool __fastcall TForm1::NewDir(System::AnsiString Name) { return CreateDirectory( Name.c_str(), // имя создаваемого каталога NULL); // атрибуты защиты; в Windows 95 // игнорируются }
CreateDirectory() - очень простая функция. У нее всего два параметра. Первый задает имя создаваемого каталога, второй - атрибуты защиты, которые присваиваются всем объектам ядра Windows. Обычно этот параметр равен NULL. Как и при создании файлов, если вы получаете сообщение об ошибке, это может означать, что данный каталог уже имеется в файловой системе.
Удаление каталога производится функцией API RemoveDirectory(), для которой передается единственный параметр с именем удаляемого каталога. Для экспериментирования добавьте к главной форме четвертую кнопку Delete Dir, обработчик ее нажатия и вспомогательный метод TForm1::DelDir():
void __fastcall TForm1::Button4Click(TObject *Sender) { System::AnsiString DirName; DirName = Form2->AskFileName(); if( DelDir(DirName) ) Memo1->Text = "Каталог успешно удален!"; else Memo1->Text = "Произошла ошибка удаления каталога Возможно, он не пуст"; } bool __fastcall TForm1::DelDir(System::AnsiString Name) { return RemoveDirectory( Name.c_str()); // имя удаляемого каталога }
Если вдруг произошла ошибка, это может означать одно из двух: либо заданного каталога не существует, либо внутри этого каталога имеются неудаленные файлы.
Что касается удаления файлов, то этот процесс от удаления каталогов не отличается ничем:
void __fastcall TForm1::Button5Click(TObject *Sender) { System::AnsiString FileName; FileName = Form2->AskFileName(); if( DelFile(FileName) ) Memo1->Text = "Файл успешно удален!"; else Memo1->Text = "Произошла ошибка удаления файла Возможно, файл защищен"; } bool __fastcall TForm1::DelFile(System::AnsiString Name) { return DeleteFile( Name.c_str()); // имя удаляемого файла }
Если вы нажмете пятую кнопку Delete File, то наверняка получите сообщение об ошибке. Это не загадка, просто вы, наверно, пытаетесь удалить файл, который создала наша программа. Помните, что мы создавали файл с атрибутом Read-Only? Так вот, это было сделано для того, чтобы продемонстрировать реакцию функции DeleteFile() на защищенные от записи файлы. Снимите атрибут Read-Only, и файл будет удален без всяких проблем.
Теперь в нашей работе наступает следующий этап - копирование и перемещение файлов. Здесь наша программа должна быть немного изменена. Во-первых, удалите все накопившиеся кнопки из главной формы. Во-вторых, во второй форме нужно добавить еще одну строку редактирования. Это необходимо в связи с тем, что для операций копирования и перемещения нужно два имени - исходное и целевое.
Для того чтобы возвращать из функции сразу два параметра, нам потребуется изменить методику работы второй формы. Будем использовать структуру для передачи параметров. В заголовочном файле второй формы опишем такую структуру:
struct FileNames { System::AnsiString from; System::AnsiString to; };
Она состоит из двух полей. Первое хранит ссылку на строку с параметром исходного файла. Второе поле содержит ссылку на получаемый файл. Внутри кода формы создадим глобальную переменную типа этой структуры:
FileNames fn;
Разумеется, придется переписать и метод TForm::AskFileName(), запрашивающий у пользователя имена файлов с помощью второй формы. В новом варианте полученные данные пакуются в поля структуры, а адрес структуры возвращается методом:
FileNames* TForm2::AskFileName(void) { ShowModal(); fn.from = Edit1->Text; fn.to = Edit2->Text; return &fn; }
Теперь разместите на главной форме кнопку Copy File и привяжите к ней обработчик нажатия:
void __fastcall TForm1::Button1Click(TObject *Sender) { FileNames* fn; fn = Form2->AskFileName(); if(CopyFile( (fn->from).c_str(), // имя копируемого файла (fn->to).c_str(), // имя нового файла TRUE)) // если файл уже существует, // не копировать заново Memo1->Text = "Файл скопирован!"; else Memo1->Text = "Произошла ошибка в процессе копирования файла Возможно, файл с таким именем уже существует"; }
Работа обработчика достаточно прозрачна. Запросив у пользователя с помощью второй формы имена файлов, обработчик получает указатель на структуру с введенными параметрами. Поля структуры разыменовываются и обрабатываются методом c_str(), на выходе которого получается обычная строка. Эти две строки передаются в функцию API CopyFile(), которая и производит копирование. Третий параметр этой функции определяет поведение функции на тот случай, если файл с запрошенным именем уже существует. Мы устанавливаем параметр так, чтобы при возникновении ситуации с дублированием имени файла последний сохранялся и процесс копирования прерывался с выдачей сообщения об ошибке.
Аналогично CopyFile() действует функция API для переноса файла MoveFile(). Сделайте еще одну кнопку на главной форме и напишите следующий исходный текст обработки события нажатия:
void __fastcall TForm1::Button2Click(TObject *Sender) { FileNames* fn; fn = Form2->AskFileName(); if(MoveFile( (fn->from).c_str(), // имя переносимого файла (fn->to).c_str())) // имя нового файла Memo1->Text = "Файл перемещен успешно!"; else Memo1->Text = "Произошла ошибка при переносе файла Возможно, файл с таким именем уже существует"; }
Единственное отличие MoveFile() от CopyFile() - отсутствие третьего параметра, отвечающего за блокировку процесса переноса в случае, если файл уже существует.
Третий этап наших исследований посвящается чтению информации из файла и записи ее туда. Так что не поленитесь снова удалить все старые кнопки на главной форме и восстановить исходный текст второй формы, который был у нас изначально.
Сначала прочтем что-нибудь из файла, для чего воспользуемся функцией API ReadFile(). Снова создайте на главной форме кнопку и обработчик ее нажатия:
void __fastcall TForm1::Button1Click(TObject *Sender) { HFILE hFile; DWORD counter; char* read_buff; BY_HANDLE_FILE_INFORMATION fi; OFSTRUCT os; os.cBytes = sizeof os; read_buff = (char*) new char[0xfffe]; // Открыть файл hFile = OpenFile( (Form2->AskFileName()).c_str(), // имя файла, преобразуемое к типу char* &os, // указатель на буфер // с информацией о файле OF_READWRITE); // файл открыт для чтения и записи if( hFile == HFILE_ERROR ) { Memo1->Text = "Произошла ошибка открытия файла!"; CloseHandle( HANDLE(hFile) ); return; } // получить информацию о файле GetFileInformationByHandle( HANDLE(hFile), // дескриптор файла &fi); // адрес структуры, в кото- // рой сохраняется информация // считать данные из файла if(!ReadFile( HANDLE(hFile), // дескриптор читаемого файла read_buff, // адрес буфера для чтения fi.nFileSizeLow, // сколько считать байтов &counter, // адрес переменной счетчика // считанных байтов NULL)) // практически всегда NULL Memo1->Text = "Произошла ошибка чтения"; else Memo1->Text = read_buff; CloseHandle( HANDLE(hFile) ); delete[] read_buff; }
Рассмотрим работу этого фрагмента. Для начала в исходном тексте объявляются две структуры. Первая, BY_HANDLE_FILE_INFORMATION, нужна для хранения полезной информации о файле. Вторая, OFSTRUCT, строго говоря, нам и не нужна, но требуется для работы функции API OpenFile(). Далее идет инициализация поля размера этой структуры.
Теперь встает вопрос о создании буфера, в который будут скопированы данные, считанные из файла. Можно проделать расчет, основанный на размере файла, но мы с вами поступим проще: зададим его размер принудительно - 64 Кбайт. Для создания такого буфера воспользуемся операцией new char[0xfffe], выделяющей блок памяти подходящего размера. В конце обработчика нажатия кнопки освободим блок памяти операцией delete[].
Само чтение происходит в три этапа: открытие файла, получение его размера и собственно чтение. Открываем файл уже знакомой нам функцией OpenFile(). Полученный дескриптор открытого файла необходимо передать в качестве параметра для другой функции API - GetFileInformationByHandle(), которая заполняет структуру fi типа BY_HANDLE_FILE_INFORMATION, переданную в качестве второго параметра, данными об открытом файле. В этой структуре имеются два поля, хранящие старшие и младшие четыре байта размера файла. Это как раз то, что нам нужно. Раз мы решили попробовать прочесть небольшой файл, то младших четырех байтов нам вполне хватит. Их мы передадим третьим параметром в функцию ReadFile(). Первые два параметра - это дескриптор читаемого файла и адрес буфера, в который будут считаны данные. Четвертый параметр функции - счетчик байтов, в который ReadFile() записывает количество байтов, считанных из файла. Пятый параметр игнорируется. Обратите внимание на то, что дескриптор файла надо преобразовывать к типу HANDLE для правильной работы. Непонятно, почему разработчики Windows провели различие между такими родственными типами.
Попробуйте откомпилировать и запустить полученный пример. Если все в порядке, то в мемо-поле появятся данные из файла. В случае ошибки мемо-поле отобразит сообщение.
Проверим теперь работу функции WriteFile(), записывающей данные в файл. В этом случае исходный текст будет проще, чем при чтении. Это в первую очередь связано с тем, что не требуется выделять буфер под данные - компонент Memo уже владеет таким буфером. Кроме того, вовсе не требуется вычислять количество записываемых байтов - их любезно предоставит вызванный метод Length() класса строки System::AnsiString, которым владеет компонент Memo (см. свойство Text).
Создайте на главной форме новую кнопку Write и привяжите к ней обработчик нажатия со следующим содержимым:
void __fastcall TForm1::Button1Click(TObject *Sender) { HFILE hFile; DWORD counter; char* read_buff; BY_HANDLE_FILE_INFORMATION fi; OFSTRUCT os; os.cBytes = sizeof os; read_buff = (char*) new char[0xfffe]; // Открыть файл hFile = OpenFile( (Form2->AskFileName()).c_str(), // имя файла, преобразуемое к типу char* &os, // указатель на буфер // с информацией о файле OF_READWRITE); // файл открыт для чтения и записи if( hFile == HFILE_ERROR ) { Memo1->Text = "Произошла ошибка открытия файла!"; CloseHandle( HANDLE(hFile) ); return; } // получить информацию о файле GetFileInformationByHandle( HANDLE(hFile), // дескриптор файла &fi); // адрес структуры, в кото- // рой сохраняется информация // считать данные из файла if(!ReadFile( HANDLE(hFile), // дескриптор читаемого файла read_buff, // адрес буфера для чтения fi.nFileSizeLow, // сколько считать байтов &counter, // адрес переменной счетчика // считанных байтов NULL)) // практически всегда NULL Memo1->Text = "Произошла ошибка чтения"; else Memo1->Text = read_buff; CloseHandle( HANDLE(hFile) ); delete[] read_buff; }
Попробуйте программу в действии. Если все пройдет удачно, то введенный в мемо-поле текст исчезнет, а вместо него возникнет сообщение о корректной записи данных в файл, в противном случае будет выведено сообщение об ошибке.
На этом наше занятие заканчивается. Полистайте файлы подсказки от Microsoft SDK и ознакомьтесь с различными параметрами и флажками, имеющимися у рассмотренных нами функций. Напоследок хочу дать один совет: обдуманно пользуйтесь различными флагами. Если вдруг вы неправильно установили их, то функция вернет вам код ошибки безо всяких комментариев. Будьте предельно аккуратны!