Глава 6
Файлы и устройства ввода/вывода
Большинство приложений создаются для того, чтобы обрабатывать данные — это прописная истина. С развитием программных технологий мы получаем возможность получать и посылать все более крупные и сложные массивы данных; однако до сих пор 90% из них хранятся в файлах.
Для использования файлов в приложении разработчику приходится решать множество задач. Основные из них — это поиск необходимого файла и выполнение с ним операций ввода/вывода.
Основные принципы и структура файловой системы мало изменились со времен MS DOS. Новые файловые системы (FAT32, NTFS) и появившаяся в Windows 2000 служба Active Directory не изменяют главного — понятия файла и способов обращения к нему.
Среда Delphi дает вам возможность выбрать один из четырех вариантов работы:
В этой главе мы изучим все основные способы работы с файлами в приложениях Delphi на конкретных примерах создания программного кода.
Использование файловых переменных. Типы файлов
Зачастую современный программный код Delphi для чтения данных из файла удивительно похож на аналогичный, написанный, к примеру, в Turbo Pascal 4.0. Это возможно потому, что программисты Borland сохранили неизменным "старый добрый" набор файловых функций, работающих через файловые переменные.
При организации операций файлового ввода/вывода в приложении большое значение имеет, какого рода информация содержится в файле. Чаще всего это строки, но встречаются и двоичные данные или структурированная информация, например, массивы или записи.
Естественно, что сведения о типе хранящихся в файле данных важно изначально задать. Для этого используются специальные файловые переменные, определяющие тип файла. Они делятся на нетипизированные и типизированные.
Перед началом работы с любым файлом необходимо описать файловую переменную, соответствующую типу данных этого файла. В дальнейшем эта переменная используется при обращении к файлу.
В Delphi имеется возможность создавать не типизированные файлы. Для их обозначения используется ключевое слово file:
var UntypedFile: file;
Такие файловые переменные используются для организации быстрого и эффективного ввода/вывода безотносительно к типу данных. При этом подразумевается, что данные читаются или записываются в виде двоичного массива. Для этого используются специальные процедуры блочного чтения и записи (см. ниже).
Типизированные файлы обеспечивают ввод/вывод с учетом конкретного типа данных. Для их объявления используется ключевое слово file of, к которому добавляется конкретный тип данных. Например, для работы с файлом, содержащим набор байтов, файловая переменная объявляется так:
var ByteFile: file of byte;
При этом можно использовать любые типы фиксированного размера, за исключением указателей. Разрешается использовать структурные типы, если их составные части удовлетворяют названному выше ограничению. Например, можно создать файловую переменную для записи:
type Country = record
Name: String;
Capital: String;
Population: Longint;
Square: Longint;
end;
var CountryFile: file of Country;
Для работы с текстовыми файлами используется специальная файловая переменная TextFile или Text:
var F: TextFile;
Операции ввода/вывода
Теперь рассмотрим две самые распространенные операции, выполняемые при работе с файлами. Это чтение и запись. Для их выполнения применяются специальные функции файлового ввода/вывода.
Итак, для выполнения операции чтения или записи необходимо выполнить следующие действия.
Обратите внимание: по сравнению с Turbo Pascal изменились названия только двух функций: Assign стала AssignFile, a close превратилась в closeFilc.
В качестве примера рассмотрим небольшой фрагмент кода.
var F: TextFile;
S: strings;
begin
if OpenDIg.Execute then AssignFile(F, OpenDlg.FileName) else Exits; Reset(F);
while Not EOF(F) do
begin
ReadlnfF, S);
Memo.Line s.Add(S);
end;
CloseFile(F);
end;
Если в диалоге открытия файла OpenDlg был выбран файл, то его имя связывается с файловой переменной f при помощи процедуры AssignFile. В качестве имени файла рекомендуется всегда передавать полное имя файла (включая путь). Как раз/в таком виде возвращают результат выбора файла диалоги работы с файлами TOpenDialog, TOpenpictureDialog. Затем при помощи процедуры Reset этот файл открывается для чтения и записи.
В цикле осуществляется чтение из файла текстовых строк, и запись их в компонент тмето. Процедура Readin осуществляет чтение текущей строки файла и переходит на следующую строку. Цикл выполняется, пока функция eof не сообщит о достижении конца файла.
После завершения чтения файл закрывается.
Такой же код можно использовать и для записи данных в файл. Необходимо только заменить процедуру чтения процедурой записи.
Теперь остановимся подробнее на назначении функций, используемых для файлового ввода/вывода.
Открытие файла может осуществляться тремя процедурами — в зависимости от его дальнейшего использования.
Процедура
procedure Reset(var F [: File; RecSize: Word ] );
открывает существующий файл для чтения и записи, текущая позиция устанавливается на первой строке файла.
Процедура
procedure Append(var F: Text);
открывает файл для записи информации после его последней строки, текущая позиция устанавливается на конец файла.
Процедура
procedure Rewrite(var F: File [;Recsize: Word ]);
создает новый файл и открывает его, текущая позиция устанавливается на начало файла. Если файл с таким именем уже существует, то он перезаписывается.
Переменная Recsize используется только при работе с нетипизированными файлами и определяет размер одной записи для операции передачи данных. Если данный параметр опущен, то по умолчанию Recsize равно 128 байт.
Чтение данных из типизированных и текстовых файлов выполняют процедуры Read И Readin.
Процедура Read имеет различное объявление для текстовых и других типизированных файлов:
procedure Read([var F: Text;] VI [, V2,...,Vn]); —для текстовых
файлов;
procedure Read(F, VI [, -V2,...,Vn]); — для других типизированных
файлов.
При одном вызове процедуры можно читать данные в произвольное число переменных. Естественно, что тип переменных должен совпадать с типом
файла. При чтении в очередную переменную читается ровно столько байт из файла, сколько занимает тип данных. В следующую переменную читается столько же байтов, расположенных следом. После выполнения процедуры текущая позиция устанавливается на первом непрочитанном байте. Аналогично работают несколько процедур Read для одной переменной, выполненных подряд.
Процедура
procedure Readln([var F: Text;] VI [,V2, ...,Vn ]);
считывает одну строку текстового файла и устанавливает текущую позицию на следующей строке. Если использовать процедуру без переменных v1. .vn, то она просто передвигает текущую позицию на следующую строку файла.
Процедуры для записи в файл write и writein описываются аналогично:
procedure Write([var F: Text;] P1 [,P2,...,Pn] );
procedure Writeln([var F: Text;] P1 [,P2,...,Pn ] );
Параметры p1, p2, ... , pn могут быть одним из целых, вещественных, строковых типов или логическим типом. Но у них есть возможность дополнительного форматирования при выводе. Каждый параметр записи может иметь форму:
Pn [: MinWidth [: DecPlaces ] ] где pn — выводимая переменная или выражение;
MinWidth — минимальная ширина поля в символах, которая должна быть больше 0;
DecPlaces — содержит количество десятичных символов после запятой при отображении вещественных чисел с фиксированной точкой.
Обратите внимание, что для текстовых файлов в функциях Read и write файловая переменная F может быть опущена. В этом случае чтение и запись осуществляются в стандартные файлы ввода/вывода. Когда программа компилируется как консольное приложение (флаг ($apptype console)), Delphi автоматически связывает входной и выходной файлы с окном консоли.
Для контроля текущей позицией в файле применяются две основные функции. Функция eof(f) возвращает значение True, если достигнут конец файла. Функция eoln(f) аналогично сигнализирует о достижении конца строки. Естественно, в качестве параметра f в функции необходимо передавать файловую переменную.
Процедура
procedure Seek(var.F;N:Longint);
обеспечивает смещение текущей позиции на N элементов. Размер одного элемента в байтах зависит от типа данных файла (типизированной переменной).
Рассмотрим теперь режим блочного ввода/вывода данных между файлом и областью адресного пространства (буфером). Этот режим отличается значительной скоростью передачи данных, причем скорость пропорциональна размеру одного передаваемого блока — чем больше блок, тем больше скорость.
Для реализации этого режима необходимо использовать только нетипизированные файловые переменные. Размер блока определяется в процедуре открытия файла (Reset, Rewrite). Непосредственно для выполнения операций используются процедуры BlockRead И BlockWrite.
Процедура
procedure BlockRead(var F: File; var Buf; Count: Integer [; var AmtTransferred: Integer]);
выполняет запись блока из файла в буфер. Параметр f ссылается на не типизированную файловую переменную, связанную с нужным файлом.
Параметр Buf определяет любую переменную (число, строка, массив, структура), в которую читаются байты из файла. Параметр Count определяет число считываемых блоков. Наконец, необязательный параметр AmtTransf erred возвращает число реально считанных блоков.
При использовании блочного чтения или записи размер блока необходимо выбирать таким образом, чтобы он бьы кратен размеру одного значения того типа, который хранится в файле. Например, если в файле хранятся значения типа Double (8 байт), то размер блока может быть равен 8, 16, 24, 32 и т. д. Блочное чтение из такого файла выглядит следующим образом:
var F: File;
DoubleArray: array [0..255] of Double;
Transfered: Integer;
begin
if OpenDIg.Execute then AssignFile(F, OpenDlg.FileName) else Exit;
Reset(F, 64);
BlockRead(F, DoubleArray, 32, Transfered);
CloseFile(F);
ShowMessage('Считано' + IntToStr(Transfered) + 'блоков');
end;
Как видно из примера, размер блока устанавливается в процедуре Reset и кратен размеру элемента массива DoubleArray, в который считываются данные. В переменной Transfered возвращается число считанных блоков. Если размер файла меньше заданного: в процедуре BlockRead числа блоков, то ошибка не возникает, а в переменной Transferee! возвращается число реально считанных блоков.
Процедура
procedure BlockWrite(var f: File; var Buf; Count: Integer [; var AmtTransferred: Integer]);
используется аналогично.
Оставшиеся функции ввода/вывода, работающие с файловыми переменными, подробно описаны в табл. 6.1.
Таблица 6.1. Процедуры и функции для работы с файлами
Объявление |
Описание |
function ChangeFileExt(const FileName, Extension: string): string; |
Функция позволяет изменить расширение файла, при этом сам файл не переименовывается |
procedure ChDir(S:string) ; |
Процедура изменяет текущий каталог на другой, путь к которому описан в строке s |
procedure CloseFile(var F) ; |
Вызов процедуры разрывает связь между файловой переменной и файлом на диске Имя этой процедуры изменено из-за конфликта имен в Delphi (в Borland Pascal используется процедура Close) |
function DeleteFile(const FileName: string): Boolean; |
Функция производит удаление файла FileName с диска и возвращает значение False, если файл удалить не удалось или файл не существует |
function ExtractFile-Ext(const FileName: string): string; |
Функция возвращает расширение файла |
function ExtractFileName(const FileName: string): string; |
Извлекает имя и расширение файла, переданного в параметре FileName |
function ExtractFilePathfconst FileName: string): string; |
Функция возвращает полный путь к файлу
|
procedure Erase(var F) ; |
Удаляет файл, связанный с файловой переменной F |
function FileSearch(const Name, DirL ist: string): string; |
Данная процедура производит поиск в каталогах DirList файла Name. Если в процессе выполнения FileSearch обнаруживается искомое имя файла, то функция возвращает в строке типа String пол ный путь к найденному файлу. Если файл не найден, то возвращается пустая строка |
Function FileSetAttr(const FileName: string; Attr: Integer): Integer; |
Присваивает файлу с именем FileName атрибуты Attr. Функция возвращает 0, если присвоение атрибутов прошло успешно. В противном случае возвращается код ошибки |
function FilePos(var F): Longint; |
Возвращает текущую позицию в файле. Функция используется для нетекстовых файлов. Перед вызовом FilePos файл должен быть открыт |
function FileSize(var F): Integer; |
FileSize возвращает размер файла в байтах или количество записей в файле, содержащем записи. Перед вызовом данной функции файл должен быть открыт. Для текстовых файлов функция FileSize не используется. |
procedure Flush(var F: Text) ;
|
Процедура очищает буфер текстового файла открытого для записи. F — файловая переменная. Когда текстовый файл открыт для записи с использованием функций Rewrite или Append, Flush очищает выходной буфер, связанный с файлом. После выполнения данной пооцедуры все символы, которые направлены для записи в файл, будут гарантированно записаны в нем. |
procedure GetDir(D: Byte; var S: strings-
|
Возвращает число, соответствующее диску, на ко тором содержится текущий каталог s D может принимать одно из следующих значений: О—по умолчанию (текущий), 1 — А, 2— В, 3—Си так далее. Процедура не генерирует код ошибки. Если имя диска Х в D оказывается ошибочным, то в строке S возвращается значение Х:\ как если бы текущая директория была на этом ошибочно указанном диске |
function IOResult: Integer; procedure MkDir(S: string); |
Функция возвращает статус последней произведенной операции ввода/вывода, если контроль ошибок выключен {$I-} Процедура создает новый каталог, который описывается в строке S |
procedure Rename(var F; NewName: string); |
Процедура изменяет имя файла, связанного с файловой переменной F. Переменная NewName является строкой типа string или типа pchar (если включена поддержка расширенного синтаксиса) |
procedurе RmDir (S: string) ;
|
Процедура удаляет пустой каталог, путь к которому задается в строке S. Если указанный каталог не существует или он не пустой, то возникает сообщение об ошибке ввода/вывода |
procedure Seek (var F; N: Longint) ;
|
Перемещает текущую позицию курсора на N позиций. Данная процедура используется только для открытых типизированных или не типизированных файлов для проведения чтения-записи с нужной позиции файла. Началу файла соответствует нулевой номер позиции. Для добавления новой информации в конец существующего файла, необходимо установить указатель на символ, следующий за последним. Для этого можно использовать выражение Seek (F, FileSize(F)) |
function SeekEof[(var F: Text)]: Boolean; |
Возвращает значение True, если указатель текущей позиции находится на символе конца файла. SeekEof может быть использована только с открытым текстовым файлом |
function SeekEoln[(var F: Text)]: Boolean;
|
Возвращает значение True, если указатель текущей позиции находится на символе конца строки. SeekEoln может быть использована только с открытым текстовым файлом |
procedure SetTextBuf(var F: Text; var Buf [ ; Size: Integer]);
|
накапливаются данные при чтении и записи. Такой буфер пригоден для большинства операций. Однако при выполнении программ с интенсивным вводом/выводом, буфер может переполниться, что при ведет к записи операций ввода-вывода на диск и, как следствие, к существенному замедлению работы приложения. SetTextBuf позволяет помещать в текстовый файл F информацию об операциях ввода-вывода вместо ее размещения в буфере, size указывает размер буфера в байтах. Если этот параметр опускается, то полагается размер, равный SizeOf (Buf). Новый буфер действует до тех пор, пока F не будет связана с новым файлом процедурой AssignFile. |
procedure Truncate (var F); |
Удаляет все позиции, следующие после текущей позиции в файле. А текущая позиция становится концом файла. С переменной F может быть связан файл любого типа, за исключением текстового |
Ввод/вывод с использованием Windows API
Для тех, кто переходит на Delphi не с прежних версий Turbo Pascal, а с других языков программирования или начинает освоение с нуля, более привычными будут стандартные функции работы с файлами Windows. Тем более что возможности ввода/вывода в них расширены. Каждый файл в этом наборе функций .описывается не переменной, а дескриптором (Handle) — 32-разрядной величиной, которая идентифицирует файл в операционной системе.
В Win32 файл открывается при помощи функции, имеющей обманчивое название
function CreateFileflpFileName: PChar;
dwDesiredAccess, dwShareMode:DWORD;
IpSecurityAttributes: PSecurityAttributes;
dwCreationDistribution, dwFlagsAndAttributes: DWORD;
hTemplateFile: THandle): THandle;
Хоть ее название и начинается с create, но она позволяет не только создавать, но и открывать уже существующие файлы.
Такое огромное количество параметров оправдано, так как createFile используется для открытия файлов на диске, устройств, каналов, портов и вообще любых источников ввода/вывода. Назначение параметров описано в табл. 6.2.
Таблица 6.2. Параметры функции createFile
Параметр |
Смысл |
LpFileName: pChar |
Имя открываемого объекта. Может представлять собой традиционную строку с путем и именем файла, UNC (для открытия объектов в сети, имя порта, драйвера или устройства) |
DwDesiredAccess,: DWORD |
Способ доступа к объекту. Может быть равен: GENERIC READ — для чтения; GENERIC WRITE — для записи. Их комбинация позволяет открыть файл для чтения и записи. Параметр 0 применяется, если нужно получить атрибуты файла без его фактического открытия |
dwShareMode: DWORD
|
Режим совместного использования файла: 0 — совместный доступ запрещен; FILE_ SHARE READ —для чтения; FILE_ SHARE_ WRITE — для записи. Комбинация FILE_ SHARE READ и File_ share_ write —для полного совместного доступа |
IpSecurityAttributes: PSecurityAttributes |
Атрибуты защиты файла. В Windows 95/98 не используются (должны быть равны nil). В Windows NT/2000 этот параметр, равный nil, дает объекту атрибуты по умолчанию |
DwCreationDistributio n: DWORD ;
|
Способ открытия файла: Create_ new — создается новый файл; если таковой уже существует, функция возвращает ошибку ERROR_ALREADY_EXISTS; Create_ always — создается новый файл; если таковой уже существует, он перезаписывается; Open_ existing — открывает существующий файл, если таковой не найден, функция возвращает ошибку; Open_ always — открывает существующий файл, если таковой не найден, он создается |
DwFlagsAndAttributes: DWORD; |
Набор атрибутов (скрытый, системный, сжатый) и флагов для открытия объекта. Подробное описание смотри в документации по Win32 |
HTemplateFile: THandle |
Файл-шаблон, атрибуты которого используются для открытия. В Windows 95/98 не используется и должен быть равен 0 |
Функция creafeFile возвращает дескриптор открытого объекта ввода/вывода. Если открытие невозможно из-за ошибок, возвращается код invalid_handle_value, а расширенный код ошибки можно узнать, вызвав функцию GetLastError.
Закрывается файл в Win32 функцией closeHandle (не closeFile, a closeHandie!) Правда, "легко" запомнить? Что поделать, так их назвали разработчики Win32).
Приведем несколько приемов из большого разнообразия использования createFile. Часто программисты хотят иметь возможность организовать посекторный доступ к физическим устройствам хранения — например, к дискете. Сделать это не так уж сложно, но при этом методы для Windows 95 и NT различаются. В NT придется открывать устройство ('\\.\A:'), а в Windows 95 — специальный драйвер доступа (обозначается '\\. \vwin32'). И то и другое делается функцией CreateFile.
Листинг 6.1 Чтение сектора с дискеты при помощи функции CreateFile
type
pDIOCRegs = ^TDIOCRegs;
TDIOCRegs = packed record rEBX,rEDX,rECX,rEAX,rEDI,rESI,rFlags : DWORD;
end;
const VWIN32_DIOC_DOS_IOCTL = 1;
VWIN32_DIOC_DOS_INT13 =4; // Performs Interrupt 13h commands.
SectorSize = 512;
function ReadSector(Head, Track, Sector: Integer; buffer: pointed; Floppy: char):Boolean;
var hDevice: THandle;
Regs: TDIOCRegs;
DevName: string;
nb: Integer;
begin
if WIN32PLATFORM 0 VER_PLATFORM_WIN32_NT then begin (win95/98}
hDevice := CreateFile('\\.\vwin32', GEMERIC_READ, 0, nil, 0, FILE_FLAG_DELETE_ON_CLOSE, 0);
if (hDevice = INVALID_HANDLE_VALUE) then begin
Result := FALSE;
Exit;
end;
regs.rEDX := Head * $100 + Ord(Floppy in ['b', 'B']);
regs.rEAX := $201; // код операции read sector
regs.rEBX := DWORD(buffer); // buffer
regs.rECX := Track * $100 + Sector;
regs.rFlags := $0;
Result := DeviceIoControl(hDevice,VWIN32_DIOC_DOS_INT13, @regs, sizeof(regs), @regs, sizeof(regs), nb, nil)
and ( (regs. rFlags and $1)=0); CloseHandle(hDevice);
end (win95} else
begin // Windows NT
DevName :='\\.\A:';
if Floppy in I'b', 'B'] then DevName[5] := Floppy;
hDevice:= CreateFile(pChar(Devname), GENERIC_READ,
FILE_SHARE_READ or FILE_SHARE_WRITE, nil, OPEN_EXISTING, FILE_ATTRIBUTE_NORMAL, 0) ;
if (hDevice = INVALID_HANDLE_VALUE) then begin
Result := FALSE;
Exit;
end;
SetFilePointer(hDevice, (Sector-1)*SectorSize, nil, FILE_BEGIN);(нумерация с 1 }
Result := ReadFile(hDevice, buffer", SectorSize, nb, nil) and (nb=SectorSize);
CloseHandle(hDevice) ;
end;
end;
Функция CreateFile используется и для доступа к портам ввода/вывода. Часто программисты сталкиваются с задачей: как организовать обмен данными с различными нестандартными устройствами, подключенными к параллельному или последовательному порту? В Turbo Pascal для DOS был очень хороший псевдомассив ports: пишешь portix] :- у; и не знаешь проблем. В Win32 прямой доступ к портам запрещен, и приходится открывать их как файлы:
hCom := CreateFile('COM2', GENERIC_READ or GENERIC_WRITE,0, NIL, OPEN_EXISTING, FILE_FLAG_OVERLAPPED, 0);
if hCom = INVALID_HANDLE_VALUE then
begin
raise EAbort.CreateFInt('Ошибка открытия порта: %d',[GetLastError]);
end;
Самое большое отличие от предыдущего примера — в скромном флаге file_flag_overlapped. О роли этих изменений — в следующем разделе.
Для чтения и записи данных в Win32 используются функции
function ReadFile(hFile: THandle; var Buffer; nNumberOfBytesToRead:
DWORD; var IpNumberOfBytesRead: DWORD; IpOverlapped: POverlapped): BOOL;
function WriteFile(hFile: THandle; const Buffer; nNumberOfBytesToWrite:
DWORD; var IpNumberOfBytesWritten: DWORD; IpOverlapped: POverlapped):
BOOL;
Здесь все сходно с BlockRead и BlockWrite: hFile — это дескриптор файла, Buffer — адрес, по которому будут читаться (писаться) данные; третий параметр означает требуемое число читаемых (записываемых) байт, а четвертый — фактически прочитанное (записанное). Последний параметр —
IpOverlapped — обсудим чуть ниже.
Отложенный (асинхронный) ввод/вывод
Эта принципиально новая возможность введена впервые в Win32, с появлением реальной многозадачности. Вызывая функции чтения и записи данных, вы на самом деле передаете исходные данные одному из потоков (threads) операционной системы, который и исполняет фактические обязанности по работе с устройством. Время доступа всех периферийных устройств гораздо больше времени доступа к ОЗУ, и ваша программа, вызвавшая Read или Write, будет дожидаться окончания операции ввода/вывода. Замедление работы программы налицо.
Выход был найден в использовании отложенного (overlapped) ввода/вывода. До начала отложенного ввода/вывода инициализируется дескриптор объекта типа события (функция createEvent) и инициализируется структура типа TOverlapped. вы вызываете функцию ReadFile или WriteFile, в которой последним параметром указываете на TOverlapped. Эта структура содержит дескриптор события Windows (event).
ОС начинает операцию (ее выполняет отдельный программный поток, скрытый от программиста) и немедленна возвращает управление приложению: вы можете не тратить время на ожидание. Признак того, что операция началась и продолжается — возврат кода error_io_pending. Пусть вас не пугает слово "error" в названии — это совершенно нормально. Если операция продолжается долго (а чтение и запись файлов на дискете, да и на диске, использование именованных каналов можно отнести к "длинным" операциям), то программа может спокойно выполнять последующие операторы. Событие будет "взведено" ОС тогда, когда ввод/вывод закончится.
Когда, по мнению программиста, ввод/вывод должен быть завершен, можно проверить это, используя функцию WaitForSingleObject.
function WaitForSingleObject(hHandle: THandle; dwMilliseconds: DWORD):
DWORD;
Объект ожидания (параметр hHandle) в этом случае — тот самый, который создан нами, указан в структуре TOverlapped и передан в качестве параметра в функцию ReadFile или writeFiie. Можно указать любое время ожидания, в том числе бесконечное (параметр Timeout при этом равен константе infinite). Признаком нормального завершения служит получение кода возврата WAIT_OBJECT_0.
Листинг 6.2 Пример отложенной операции чтения
function TMyClass.Read(var Buffer; Count: Longint): Longint;
var succ : boolean;nb : Cardinal;LastError : Longint;
Overlap: TOverlapped;
begin FillChar(Overlap,SizeOf(Overlap),0);
Overlap.hEvent := CreateEvent(nil, True, False, nil);
Result := Maxint;
succ := ReadFile(FHandle, Buffer, Count, nb, @OverlapRd);
//
// здесь можно вставить любые операторы, которые
// могут быть выполнены до окончания ввода/вывода
//
if not succ then begin LastError := GetLastError;
if LastError = ERROR_IO_PENDING then
begin
if WaitForSingleObject(OverlapRd.hEvent, INFINITE)=WAIT_OBJECT_0 then
GetOverlappedResult(FHandle, OverlapRd, nb, TRUE);
end else raise EAbort.Create(Format('Read failed, error %d',[LastError]));
end;
Result :- nb;
CloseHandle( hEvent );
end;
Если вы задали конечный интервал в миллисекундах, а операция еще не закончена, waitForsingleobject вернет код завершения wait timeout. Функция GetOveriappedResult возвращает в параметре nb число байт, действительно прочитанных или записанных во время отложенной операции.
Контроль ошибок ввода/вывода
При работе с файлами разработчик обязательно должен предусмотреть обработку возможных ошибок. Практика показывает, что именно операции ввода/вывода вызывают большую часть ошибок, возникающих в приложении из-за воздействия окружающей программной среды.
Контроль за ошибками ввода/вывода зависит от применяемых функций. При использовании доступа через Win32 API все функции возвращают код ошибки Windows, который и нужно проанализировать.
При возникновении ошибок ввода/вывода в функциях, использующих файловые переменные, генерируется исключительная ситуация класса EinOutError. Но так происходит только в том случае, если включен контроль ошибок ввода/вывода. Для этого используются соответствующие директивы компилятора:
Класс EinOutError отличается тем, что у него есть поле ErrorCode. При возникновении этой исключительной ситуации вы можете получить его значение и принять решение. Основные коды имеют такие значения:
При отключенном контроле в случае возникновения ошибки выполнение программы продолжается без остановки. Однако в этом случае устранение возможных последствий ошибки возлагается на разработчика. Для этого применяется функция
function IQResult: integer;
которая возвращает значение 0 при отсутствии ошибок.
Атрибуты файла. Поиск файла
Еще одна часто выполняемая с файлом операция — поиск файлов в заданном каталоге. Для организации поиска и отбора файлов используются специальные процедуры, а также структура, в которой сохраняются результаты поиска.
Запись
type
TFileName = string;
TSearchRec = record
Time: Integer; {Время и дата создания}
Size: Integer; (Размер файла}
Attr: Integer; {Параметры файла}
Name: TFiieName; {Полное имя файла}
ExcludeAttr: Integer; {He используется}
FindHandle: THandle; {Дескриптор файла}
FindData: TWin32FindData; (He используется}
end;
обеспечивает хранение характеристик файла после удачного поиска. Дата и время создания файла хранятся в формате MS DOS, поэтому для получения этих параметров в принятом в Delphi формате TDateTime необходимо использовать следующую функцию:
function FileDateToDateTime(FileDate: Integer): TDateTime;
Обратное преобразование выполняет функция
function DateTimeToFileDate(DateTime: TDateTime): Integer;
Свойство Attr может содержать комбинацию следующих флагов-значений:
Для определения параметров файла используется оператор 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, иначе — код ошибки Windows. Параметры найденного файла возвращаются в записи F типа TSearchRec. Функция
function FindNext(var F: TSearchRec): Integer;
используется для повторного поиска следующего файла, удовлетворяющего критерию поиска. При этом используются те параметры поиска, которые заданы последним вызовом функции FindFirst. В случае удачного поиска возвращается 0.
Для освобождения ресурсов, выделенных для выполнения поиска, применяется функция:
procedure FindClose(var F: TSearchRec);
В качестве примера организации поиска файлов рассмотрим фрагмент исходного кода, в котором маршрут поиска файлов задается в однострочном текстовом редакторе DirEdit, а список найденных файлов передается в компонент TListBox.
procedure TFormI.FindBtnClick(Sender: TObject);
begin
ListBox.Items.Clear;
FindFirst(DirEdit.Text, faArchive + faHidden, SearchRec);
while FindNext(SearchRec) = 0 do
ListBox.Items.Add(SearchRec.Name) ;
FindClose(SearchRec);
end;
Потоки
Потоки — очень удачное средство для унификации ввода/вывода на различные носители. Потоки представляют собой специальные объекты-потомки абстрактного класса TStream. Сам TStream "умеет" открываться, читать, писать, изменять текущее положение и закрываться. Поскольку для разных носителей эти вещи происходят по-разному, конкретные аспекты реализованы в его потомках. Наиболее часто используются потоки для работы с памятью и файлами на диске.
Многие классы VCL имеют унифицированные методы LoadFromStream и SaveToStream, которые обеспечивают обмен данными с потоками. От того, с каким физическим носителем работает поток, зависит место хранения данных.
Базовые классы TStream и THandleStream
В основе иерархии классов потоков лежит класс TStream. Он обеспечивает выполнение основных операций потока безотносительно к реальному носителю информации. Основными из них являются чтение и запись данных.
Класс TStream порожден непосредственно от класса TObject.
Потоки также играют важную роль в чтении/записи компонентов из файлов ресурсов (.dfm). Большая группа методов обеспечивает взаимодействие компонента и потока, чтение свойств компонента из ресурса и запись значений свойств в ресурс (табл. 6.3).
Таблица 6.3, Свойства и методы класса TStream
Объявление |
Описание |
property Position: Longint ; |
Определяет текущую позицию в потоке |
property Size: Longint; |
Определяет размер потока в байтах |
function CopyFrom(Source: TStream; Count: Longint): Longint; |
Копирует из потока Source Count байтов, начиная с текущей позиции. Возвращает число скопированных байтов |
function Readfvar Buffer; Count: Longint): Longing-virtual; abstract; |
Абстрактный класс, перекрываемый в потомках. Считывает из потока Count байтов в буфер Buffer. Возвращает число скопированных байтов |
procedure ReadBuffer(var Buffer; Count: Longint); |
Считывает из потока Count байтов в буфер Buffer. Возвращает число скопированных байтов |
function Seek(Off set: Longint; Origin: Word): Longint; virtual; abstract/ |
Абстрактный класс, перекрываемый в потомках. Смещает текущую позицию в реальном носителе данных на offset байтов в зависимости от условия Origin (см. ниже) |
function Write(const Buffer; Count: Longint): Longint; virtual/abstract; |
Абстрактный класс, перекрываемый в потомках. Записывает в поток Count байтов из буфера Buffer. Возвращает число скопированных байтов |
procedure WriteBuffer(const Buffer; Count: Longint) ; |
Записывает в поток Count байтов из буфера Buffer. Возвращает число скопированных байтов |
function ReadComponent(Instance: TComponent): TComponent; |
Передает данные из потока в компонент Instance, заполняя его свойства значениями |
function ReadComponentRes(Instance: TComponent): TComponent; |
Считывает заголовок ресурса компонента instance и значения его свойств из потока |
procedure ReadResHeader; |
Считывает заголовок ресурса компонента из потока |
procedure WriteComponent(Instance: TComponent) ; |
Передает в поток значения свойств компонента Instance |
procedure WriteComponentRes(const ResName: string; Instance: TComponent); |
Записывает в поток заголовок ресурса компонента instance и значения его свойств |
Итак, в основе операций считывания и записи данных в потоке лежат методы Read и Write. Именно они вызываются для реального выполнения операции внутри методов ReadBuffer И WriteBuffer, ReadComponent И WriteComponent.
Так как класс TStream является абстрактным, то методы Read и write также являются абстрактными. В классах-потомках они перекрываются, обеспечивая работу с конкретным физическим носителем данных.
Метод seek используется для изменения текущей позиции в потоке. "Точка отсчета" позиции зависит от значения параметра origin:
Группа методов обеспечивает чтение и запись из потока ресурса компонента. Они используются при создании компонента на основе данных о нем, сохраненных в формате файлов ресурсов. Для чтения ресурса используется метод ReadComponentRes, в котором последовательно вызываются:
Для записи ресурса в поток применяется метод writeComponentRes.
Класс THandieStream инкапсулирует поток, связанный с физическим носителем данных через дескриптор.
Для создания потока используется конструктор
constructor Create(AHandle: Integer);
в параметре которого передается дескриптор. Впоследствии доступ к дескриптору осуществляется через свойство:
property Handle: Integer;
Класс TFileStream
Класс TFileStream позволяет создать поток для работы с файлами. При этом поток работает с файлом без учета типа хранящихся в нем данных (см. выше).
Полное имя файла задается в параметре Filename при создании потока:
constructor Create(const FileName: string; Mode: Word);
Параметр Mode определяет режим работы с файлом. Он составляется из флагов режима открытия:
и флагов режима совместного использования:
Для чтения и записи из потока используются методы Read и Write, унаследованные от класса HandieStream:
procedure TFormI.CopyBtnClick(Sender: TObject);
var Streami, Stream2: TFileStream;
IntBuf: array[0..9] of Integer;
begin if Not OpenDIg.Execute then Exit;
try Streami := TFileStream.Create(OpenDlg.FileName, fmOpenRead);
Streami.ReadBufferfIntBuf, SizeOf(IntBuf));
try Stream2 := TFileStream.Create('TextFile.tmp', fmOpenWrite);
Stream2.Seek(0, soFromEnd);
Stream2.WriteBufferfIntBuf, SizeOf(IntBuf));
finally Stream2.Free;
end;
finally Streami.Free;
end;
end;
Обратите внимание, что в данном фрагменте кода функция seek используется для записи данных в конец файлового потока.
При необходимости копирования одного файла в другой целиком используется метод СopyFrom, унаследованный от TStream:
procedure TFormI.CopyBtnClick(Sender: TObject);
var Streami, Stream2: TFileStream;
begin
if Not OpenDlg.Execute then Exit;
try
Stream1 := TFileStream.Create(OpenDlg.FileName, fmOpenRead);
Stream2 := TFileStream.Create('Sample.tmp', fmOpenWrite);
Stream2.Seek(0, soFromEnd);
Stream2.CopyFrom(Streami, Streami.Size);
finally
Stream2.Free; Streami.Free;
end;
end;
Обратите внимание, что в данном случае для определения размера передаваемого потока необходимо использовать свойство Stream.size, которое дает реальный объем данных, содержащихся в потоке.Функция SizeOf (Stream) в этом случае даст размер объекта потока, и не более того.
Класс TMemoryStream
Класс TMemoryStream обеспечивает сохранение данных в адресном пространстве. При этом методы доступа к этим данным остаются теми же, что и при работе с файловыми потоками. Это позволяет использовать адресное пространство для хранения промежуточных результатов работы приложения, а также при помощи стандартных методов осуществлять обмен данными между памятью и другими физическими носителями.
Свойство
property Memory: Pointer;
определяет область памяти, отведенную для хранения данных потока. Изменение размера отведенной памяти осуществляется методом
procedure SetSize(NewSize: Longint); override;
Для очистки памяти потока используется метод
procedure Clear;
Чтение/запись данных в память выполняется привычными методами Read и
Write. Также запись данных в память может осуществляться методами:
Дополнительно можно использовать методы записи данных в файл или поток:
Класс TStringStream
Так как строковые константы и переменные широко применяются при разработке приложений, то для удобства работы с ними создан специальный класс TStringStream. Он обеспечивает хранение строки и доступ к ней во время выполнения приложения.
Он обладает стандартным для потоков набором свойств и методов, добавляя к ним несколько, упрощающих использование строк.
Свойство только для чтения
property DataString: string;
обеспечивает доступ к хранимой строке. Методы
function Readfvar Buffer; Count: Longint): Longint; override;
и
function Write(const Buffer; Count: Longint): Longint; override;
реализуют обычный для потоков способ чтения и записи строки для произвольной переменной Buffer.
Метод
function ReadString(Count: Longint): string;
обеспечивает чтение Count байтов строки потока, начиная с текущей позиции. Метод
procedure WriteString(const AString: string);
дописывает к строке строку AString, начиная с текущей позиции.
При работе с файлами и потоками используются дополнительные классы исключительных ситуаций.
Класс EFCreateError возникает при ошибке создания файла, a EFOpenError — при открытии файла.
При чтении/записи данных в поток могут возникнуть ИС EReadError и EWriteError.
Оповещение об изменениях в файловой системе
Многие программисты задавались вопросом: как получить сигнал от операционной системы о том, что в файловой системе произошли какие-то изменения? Такой вид оповещения позаимствован из ОС Unix и теперь доступен программистам, работающим с Win32.
Для организации мониторинга файловой системы нужно использовать три
функции — FindFirstChangeNotification, FindNextChangeNotification и FindcioseChangeNotification. Первая из них возвращает дескриптор объекта файлового оповещения, который можно передать в функцию ожидания. Объект активизируется тогда, когда в заданной папке произошли те или иные изменения (создание или уничтожение файла или папки, изменение прав доступа и т. д.). Вторая — готовит объект к реакции на следующее изменение. Наконец, с помощью третьей функции следует закрыть ставший ненужным объект.
Так может выглядеть код метода Execute потока, созданного для мониторинга:
var DirName : string;
procedure TSimpleThread.Execute;
var r: Cardinal;
fn : THandle;
begin
fn := FindFirstChangeNotification(pChar(DirName),True,
FILE_NOTIFY_CHANGE_FILE_NAME);
repeat r := WaitForSingleObject(fn,2000);
if r = WAIT_OBJECT_0 then Formi.UpdateList;
if not FindNextChangeNotification(fn) then break;
until Terminated;
FindCloseChangeNotification(fn);
end;
В главной форме должны находиться компоненты, нужные для выбора обследуемой папки, а также компонент TListBox, в который будут записываться имена файлов:
procedure TFormI.ButtonlClick(Sender: TObject);
var dir : string;
begin
if SelectDirectory(dir,[ ],0) then begin Editl.Text := dir;
DirName := dir;
end;
end;
procedure TFormI.UpdateList;
var SearchRec: TSearchRec;
begin
ListBoxl.Clear;
FindFirst(Editl.Text+'\*.*', faAnyFile, SearchRec);
repeat ListBoxl.Items.Add(SearchRec.Name);
until FindNext(SearchRec) <> 0;
FindClose(SearchRec);
end;
Приложение готово. Чтобы оно стало полнофункциональным, предусмотрите в нем механизм перезапуска потока при изменении обследуемой папки.
Использование отображаемых файлов
Последний, самый нетрадиционный, вид работы с файлами — это так называемые отображаемые файлы.
Вообще говоря, в 32-разрядной Windows под "памятью" подразумевается не только оперативная память (ОЗУ), но также и память, резервируемая операционной системой на жестком диске. Этот вид памяти называется виртуальной памятью. Код и данные отображаются на жесткий диск посредством страничной системы (paging system) подкачки. Страничная система использует для отображения страничный файл (win386.swp в Windows 95/98 и pagefiie.sys в Windows NT). Необходимый фрагмент виртуальной памяти переносится из страничного файла в ОЗУ и, таким образом, становится доступным.
А что, если так же поступить и с любым другим файлом и сделать его частью адресного пространства? В Win32 это возможно. Для выделения фрагмента памяти должен быть создан специальный системный объект Win32, называемый отображаемым файлом. Этот объект "знает", как соотнести файл, находящийся на жестком диске, с памятью, адресуемой процессами.
Одно или более приложений могут открыть отображаемый файл и получить тем самым доступ к данным этого объекта. Таким образом, данные, помещенные в страничный файл приложением, использующим отображаемый файл, могут быть доступны другим приложениям, если они открыли и используют тот же самый отображаемый файл.
Создание и использование объектов файлового отображения осуществляется посредством использования функций Windows API. Этих функций три:
Отображаемый файл создается операционной системой при вызове функции CreateFileMapping. Этот объект поддерживает соответствие между содержимым файла и адресным пространством процесса, использующего этот файл. Функция CreateFileMapping имеет шесть параметров:
function CreateFileMapping(hFile: THandle;
IpFileMappingAttributes: PSecurityAttributes;
flProtect, dwMaximumSizeHigh, dwMaximumSizeLow: DWORD;
IpName: PChar): THandle;
Первый параметр имеет тип THandle. Он должен соответствовать дескриптору уже открытого при помощи функции CreateFile файла. Если значение параметра hFile равно $ffffffff, то это приводит к связыванию объекта файлового отображения со страничным файлом операционной системы.
Второй параметр — указатель на запись типа TSecurityAttributes. При отсутствии требований к защите данных в Windows NT значение этого параметра всегда равно nil. Третий параметр имеет тип dword. Он определяет атрибут защиты. Если при помощи отображаемого файла вы планируете совместное использование данных, третьему параметру следует присвоить значение PAGE_READWRITE.
Четвертый и пятый параметры также имеют тип dword. Когда выполняется функция CreateFileMapping, значение типа dword четвертого параметра сдвигается влево на четыре байта и затем объединяется со значением пятого параметра посредством операции and. Проще говоря, значения объединяются в одно 64-разрядное число, равное объему памяти, выделяемой объекту файлового отображения из страничного файла операционной системы. Поскольку вы вряд ли попытаетесь осуществить выделение более чем четырех гигабайт данных, то значение четвертого параметра всегда должно быть равно нулю. Используемый затем пятый параметр должен показывать, сколько памяти в байтах необходимо зарезервировать в качестве совместной. Если вы хотите отобразить весь файл, четвертый и пятый параметры оба должны быть равны нулю.
Шестой параметр имеет тип pchar и представляет собой имя объекта файлового отображения.
Функция CreateFileMapping возвращает значение типа THandle. В случае успешного завершения возвращаемое функцией значение представляет собой дескриптор созданного объекта файлового отображения. В случае возникновения какой-либо ошибки возвращаемое значение будет равно 0.
Следующая задача — спроецировать данные файла в адресное пространство нашего процесса. Этой цели служит функция MapviewOfFiie. Функция MapviewOfFile имеет пять параметров:
function MapViewOfFile(hFileMappingObject: THandle;
dwDesiredAccess: DWORD;
dwFileOffsetHigh, dwFileOffsetLow, dwNumberOfBytesToMap: DWORD):
Pointer;
Первый параметр имеет тип THandle. Его значением должен быть дескриптор созданного объекта файлового отображения — тот, который возвращает функция CreateFileMapping. Второй параметр определяет режим доступа к файлу: FILE_MAP_WRITE, FILE_MAP_READ или FILE_MAP_ALL_ACCESS.
Третий и четвертый параметры также имеют тип dword. Это — смещение отображаемого участка относительно начала файла в байтах. В нашем случае эти параметры должны быть установлены нулем, поскольку значение, которое мы даем пятому (последнему) параметру функции MapviewOfFile, также равно нулю.
Пятый и последний параметр функции MapviewOfFile, как и предыдущие параметры, имеет тип dword. Он используется для определения (в байтах) количества данных объекта файлового отображения, которые надо отобразить в процесс (сделать доступными для вас). Для достижения наших целей это значение должно быть установлено нулем, что означает автоматическое отображение в процесс всех данных, выделенных перед этим функцией
CreateFileMapping.
Значение, возвращаемое функцией MapviewOfFile, имеет тип "указатель". Если функция отработала успешно, то она вернет начальный адрес данных объекта файлового отображения.
Следующий фрагмент кода демонстрирует вызов функции MapviewOfFile:
var
hMappedFile: THandle;
pSharedBuf: PChar;
begin
hMappedFile := CreateFileMapping(FHandle, nil, PAGE_READWRITE, 0, 0, 'SharedBlock');
if (hMappedFile = 0) then
ShowMessage('Mapping error!')
Else
Begin
pSharedBuf := MapviewOfFile(hMappedFile, FILE_MAP_ALL_ACCESS, 0, 0, 0);
if (pSharedBuf = nil) then
ShowMessage ('MapView error');
end;
end;
После того как получен указатель pSharedBuf, вы можете работать со своим файлом как с обычной областью памяти, не заботясь о вводе, выводе, позиционировании и т. п. Все эти проблемы берет на себя файловая система.
Последние две функции, имеющие отношение к объекту файлового отображения, называются UnMapViewOfFile И CloseHandle. функция
UnMapviewOfFile делает то, что подразумевает ее название. Она прекращает отображение в адресное пространство процесса того файла, который перед этим был отображен при помощи функции MapviewOfFile. Функция CloseHandle закрывает дескриптор объекта файлового отображения, возвращаемый функцией CreateFileMapping.
Функция UnMapViewOfFile должна вызываться перед функцией CloseHandle.
Функции UnMapViewOfFile передается единственный параметр типа указатель:
procedure TClientForm.FormDestroy(Sender: TObject);
begin
UnMapViewOfFile(pSharedBuf) ;
CloseHandle(hFileMapObj);
end;
Отображаемые файлы уже будут использоваться и в других главах этой книги. Неудивительно, ведь это очень мощный инструмент: помимо возможности совместного доступа, он позволяет заметно ускорить доступ к файлам, особенно большого размера.
Резюме
При разработке приложений очень часто приходится решать задачи обмена данными между приложениями или приложением и устройством ввода/вывода. При этом большая часть программы занимается обеспечением работы приложения с файлами.
В этой главе рассмотрены методы, обеспечивающие взаимодействие программы с файловой системой и примеры их использования.
Для организации обмена данными в приложениях используются специальные объекты — потоки, которые не только хранят информацию во время выполнения приложения, но и предоставляют разработчику набор стандартных свойств и методов для управления данными.
Затруднительно было бы сказать, при изучении каких глав будет полезен материал этой главы. Скорее всего, он понадобится везде.