Файлы и устройства ввода/вывода


Большинство приложений создаются для того, чтобы обрабатывать данные — это прописная истина. С развитием программных технологий мы имеем возможность получать и посылать все более крупные и сложные массивы данных; однако до сих пор 90% из них хранятся в файлах.
Для использования файлов в приложении разработчику приходится решать множество задач. Главные из них — поиск необходимого файла и выполнение с ним операций ввода/вывода.
Основные принципы и структура файловой системы мало изменились со времен MS-DOS. Файловые системы (FAT32, NTFS), появившаяся в Windows 2000 служба Active Directory не изменяют главного — понятия файла и способов обращения к нему.
Среда Delphi дает вам возможность выбрать один из четырех вариантов работы:

  •  использование традиционного набора функций работы с файлами, унаследованного от Turbo Pascal;
  •  использование функций ввода/вывода из Windows API; 
  •  использование потоков (rstream и его потомки); 
  •  использование отображаемых файлов.

В этой главе мы изучим все основные способы работы с файлами в приложениях 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: Longlnt;
Square:     Longlnt;
 end;
var CountryFile: file of Country;
Для работы с текстовыми файлами используется специальная файловая переменная TextFile или Text:
var F: TextFile;
 
Операции ввода/вывода
Теперь рассмотрим две самые распространенные операции, выполняемые при работе с файлами. Это чтение и запись. Для их осуществления применяются специальные функции файлового ввода/вывода.
Итак, для выполнения операции чтения или записи необходимо произвести следующие действия:
1. Объявить файловую переменную необходимого типа.
2. При помощи функции AssignFile связать эту переменную с требуемым файлом.
3. Открыть файл при помощи функций Append, Reset, Rewrite.
4. Выполнить операции чтения или записи. При этом, в зависимости от сложности задачи и структуры данных, может использоваться целый ряд вспомогательных функций.
5. Закрыть файл при помощи функции CloseFile.
Внимание
По сравнению с Turbo Pascal изменились названия только двух функций: Assign стала AssignFile, a Close превратилась в CloseFile.
В качестве примера рассмотрим небольшой фрагмент исходного кода.
...
var  F: TextFile;
S: string;
 begin
if OpenDlg.Execute
then AssignFiie(F, OpenDlg.FileName) 
else Exit; Reset(F);
while Not EOF(F) do 
begin
Readln(F, S) ; 
Memo.Lines.Add(S); 
end;
CloseFile(F); 
end;
...
Если в диалоге открытия файла OpenDlg был выбран файл, то его имя связывается с файловой переменной F при помощи процедуры AssignFiie. В качестве имени файла рекомендуется всегда передавать полное имя файла (включая его маршрут). Как раз в таком виде возвращают результат выбора файла диалоги работы с файлами TOpenDialog, TOpenPictureDiaiog. Затем при помощи процедуры Reset этот файл открывается для чтения и записи.
В цикле выполняется чтение из файла текстовых строк и запись их в компонент TMemo. Процедура 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 ]);
считывает одну строку текстового файла и устанавливает текущую позицию на следующей строке. Если использовать процедуру без переменных vi. .vn, то она просто передвигает текущую позицию на очередную строку файла.
Процедуры для записи в файл write и writein описаны аналогично:
procedure Write([var F: Text; ] PI [, P2,..., Pn]) ; procedure Writein([ var F: Text; ] PI [, P2,...,Pn ]);
Параметры P1, P2, ..., Pn могут быть одним из целых или вещественных типов, одним из строковых типов или логическим типом. Но у них есть возможность дополнительного форматирования при выводе. Каждый параметр записи может иметь форму:
Рn [: MinWidth [: DecPlaces ] ]
Рn — выводимая переменная или выражение;
MinWidth — минимальная ширина поля в символах, которая должна быть больше 0;
DecPlaces — содержит количество десятичных символов после запятой при отображении вещественных чисел с фиксированной точкой.
Обратите внимание, что для текстовых файлов в функциях Read и write файловая переменная F может быть опущена. В этом случае чтение и запись осуществляются в стандартные файлы ввода/вывода. Когда программа компилируется как консольное приложение (флаг {$APPTYPE CONSOLE}), Delphi автоматически связывает входной и выходной файлы с окном консоли.
Для контроля за текущей позицией в файле применяются две основные функции. Функция EOF(F) возвращает значение True, если достигнут конец файла. Функция EOLN(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 содержит число считываемых блоков. Наконец, необязательный параметр AmtTransferred возвращает число реально считанных блоков.
При использовании блочного чтения или записи размер блока необходимо выбирать таким образом, чтобы он был кратен размеру одного значения того типа, который хранится в файле. Например, если в файле хранятся значения типа Double (8 байт), то размер блока может быть равен 8, 16, 24, 32 и т. д. Фрагмент исходного кода блочного чтения при этом выглядит следующим образом:
...
var F: File;
DoubleArray: array [0..255] of Double;
Transfered: Integer;
begin
if OpenDlg.Execute then AssignFile(F, OpenDlg.FileName) else Exit; 
Reset(F, 64);
BlockRead(F, DoubleArray, 32, Transferee!); 
CloseFile(F);
ShowMessage('Считано '+IntToStr(Transfered)+' блоков');
 end;
...
Как видно из примера, размер блока установлен в процедуре Reset и кратен размеру элемента массива DoubleArray, в который считываются данные. В переменной Transfered возвращается число считанных блоков. Если размер файла меньше заданного в процедуре BlockRead числа блоков, ошибка не возникает, а в переменной Transfered передается число реально считанных блоков.
Процедура
procedure BlockWrite(var f: File; var Buf; Count: Integer 
[; var AmtTransferred: Integer]);
используется аналогично.
Оставшиеся функции ввода/вывода, работающие с файловыми переменными, подробно описаны в табл. 9.1.
Таблица 9.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 ExtractFileExt (const FileName: string): string;

Функция возвращает расширение файла

function ExtractFileName (const FileName: string) : string;

Извлекает имя и расширение файла, содержащегося в параметре FileName

function ExtractFilePath( const FileName: string): string;

Функция возвращает полный путь к файлу

procedure Erase (var F);

Удаляет файл, связанный с файловой переменной F

function FileSearch (const Name, DirList: 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: string);

Возвращает число, соответствующее диску, на котором содержится текущий каталог s. о может принимать одно из следующих значений:

  •   0 — по умолчанию (текущий);
  •   1 -А; 
  •  2 -В; 
  •  3-С 

и т. д.
Процедура не генерирует код ошибки. Если имя диска в D оказывается ошибочным, то в строке S возвращается значение Х:\, как если бы текущая папка была на этом ошибочно указанном диске

function lOResult: Integer;

Функция возвращает статус последней произведенной операции ввода/вывода, если контроль ошибок выключен { $1- }

procedure MkDir(S: string);

Процедура создает новый каталог, который описывается в строке S

procedure Rename (var F; NewName: string) ;

Процедура изменяет имя файла, связанного с файловой переменной F. Переменная NewName является строкой типа string или PChar (если включена поддержка расширенного синтаксиса)

procedure 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] );

Связывает с текстовым файлом буфер ввода/вывода. F — файловая переменная текстового типа. Каждая файловая переменная текстового типа имеет внутренний буфер емкостью 128 байт, в котором накапливаются данные при чтении и записи. Такой буфер пригоден для большинства операций. Однако при выполнении программ с интенсивным вводом/выводом буфер может переполниться, что приведет к записи операций ввода/вывода на диск и, как следствие, к существенному замедлению работы приложения. SetTextBuf позволяет помещать в текстовый файл F информацию об операциях ввода/вывода вместо ее размещения в буфере. Size указывает размер буфера в байтах. Если этот параметр опускается, то полагается размер, равный SizeOf(Buf). Новый буфер действует до тех пор, пока F не будет связана с новым файлом процедурой AssignFile

procedure Truncate {var F) ;

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

Ввод/вывод с использованием функций Windows API


Для тех, кто переходит на Delphi не с прежних версий Turbo Pascal, а с С, других языков или начинает освоение с "нуля", более привычными будут стандартные функции работы с файлами Windows. Тем более, что возможности ввода/вывода в них расширены. Каждый файл в этом наборе функций описывается не переменной, а дескриптором (Handle) — 32-разрядной величиной, которая идентифицирует файл в операционной системе.
В Win32 файл открывается при помощи функции, имеющей обманчивое название:
function CreateFile(IpFileName: PChar; dwDesiredAccess, 
dwShareMode: DWORD; IpSecurityAttributes: PSecurityAttributes;
 dwCreationDistribution, dwFlagsAndAttributes: DWORD; 
hTemplateFile: THandle): THandle;
Хоть ее название и начинается с create, но она позволяет не только создавать, но и открывать уже существующие файлы.
Такое огромное количество параметров оправдано, т. к. createFile используется для открытия файлов на диске, устройств, каналов, портов и вообще любых источников ввода/вывода. Назначение параметров описано в табл. 9.2.
Таблица 9.2. Параметры функции CreateFile


Параметр

Описание

IpFileName:pChar

Имя открываемого объекта. Может представлять собой традиционную строку с путем и именем файла, UNC (для открытия объектов в сети, имя порта, драйвера или устройства)

dwDesiredAccess,:DWORD

Способ доступа к объекту. Может быть равен:

  •  GENERIC READ — для чтения; 
  •  GENERIC WRITE — для записи.

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

dwShareMode:DWORD

Режим совместного использования файла: 

  •  0 — совместный доступ запрещен;
  • FILE SHARE READ — для чтения;
  • FILE_SHARE_WRITE -  для записи.

Их комбинация — для полного совместного доступа

IpSecurityAttributes :PSecurityAttributes

Атрибуты защиты файла. В Windows 95/98 не используются (должны быть равны nil). В Windows NT/2000 этот параметр, равный nil, дает объекту атрибуты по умолчанию

dwCreationDistribution: DWORD;

Способ открытия файла:

  •  CREATE NEW — создается новый файл, если таковой уже существует, функция возвращает ошибку ERROR_ALREADY_EXISTS;
  •  CREATE ALWAYS — создается новый файл, если таковой уже существует, он перезаписывается;
  •  OPEN EXISTING— открывает существующий файл, если таковой не найден, функция возвращает ошибку;
  •  OPEN ALWAYS — открывает существующий файл, если таковой не найден, он создается

dwFlagsAndAttributes: DWORD;

Набор атрибутов (скрытый, системный, сжатый) и флагов для открытия объекта. Подробное описание см. в документации по Win32

hTemplateFile: THandle

Файл-шаблон, атрибуты которого используются для открытия. В Windows 95/98 не используется и должен быть равен 0

Функция createFile возвращает дескриптор открытого объекта ввода/вывода. Если открытие невозможно из-за ошибок, возвращается код INVALID_HANDLE_VALUE, а расширенный код ошибки можно узнать, вызвав функцию GetLastError.
Закрывается файл в Win32 функцией closeHandie (не closeFile, a closeHandle! Правда, "легко" запомнить? Что поделать, так их назвали разработчики Win32).
Приведем из большого разнообразия несколько приемов использования функции CreateFile. Часто программисты хотят иметь возможность организовать посекторный доступ к физическим устройствам хранения — например к дискете. Сделать это не так уж сложно, но при этом методы для Windows 98 и Windows 2000 различаются. В Windows 2000 придется открывать устройство ('\\.\A:'), а в Windows 98 — специальный драйвер доступа (обозначается '\\.\vwin32'). И то и другое делается функцией createFile.
 Листинг 9.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; //Прерывание 13
SectorSize = 512;
function ReadSector(Head, Track, Sector: Integer; buffer : pointer; 
Floppy: char):Boolean; 
var hDevice : THandle; 
Regs : TDIOCRegs;
 DevName : string; nb : Integer; 
begin
if WIN32PLATFORM <> VER_PLATFORM_WIN32_NT then
 begin {win95/98} hDevice := CreateFile('\\.\vwin32', GENERIC_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; // KOH onepam-iM read sector
regs.rEBX := DWORD(buffer); // buffer
regs.rECX := Track * $100 + Sector;
regs.rFlags := $0;
Result := DeviceloControl(hDevice,VWIN32_DIOC_DOS_INT13,
@regs, sizeof(regs), @regs, sizeof(regs), nb, nil) 
and ((regs.rFlags and $1)=0); CloseHandle(hDevice); 
end {win95/98} 
else
begin // Windows NT/2000 
DevName :='\\.\A:';
if Floppy in ['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; // Windows NT/2000 
end;
Для чтения и записи данных в 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 — обсудим чуть позже.
Функция createFile используется и для доступа к портам ввода/вывода. Часто программисты сталкиваются с задачей: как организовать обмен данными с различными нестандартными устройствами, подключенными к параллельному или последовательному порту? В Turbo Pascal для DOS был очень хороший псевдомассив Ports: пишешь Port[x] := у; и не знаешь проблем. В 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.CreateFmt('Ошибка открытия порта: %d*,[GetLastError]);
end;
Самое большое отличие от предыдущего примера — в скромном флаге FILE_FLAG_OVERLAPPED. О роли этих изменений- в следующем разделе
 
Отложенный (асинхронный) ввод/вывод
Эта принципиально новая возможность введена впервые в Win32 с появлением реальной многозадачности. Вызывая функции чтения и записи данных, вы на самом деле передаете исходные данные одному из потоков (threads) операционной системы, который и осуществляет фактические обязанности по работе с устройством. Время доступа всех периферийных устройств гораздо больше доступа к ОЗУ, и ваша программа, вызвавшая Read или write, будет дожидаться окончания операции ввода/вывода. Замедление работы программы налицо.
Выход был найден в использовании отложенного (overlapped) ввода/вывода. До начала отложенного ввода/вывода инициализируется дескриптор объекта типа события (функция createEvent) и структура типа TOveriapped. Вы вызываете функцию ReadFile или writeFile, в которой последним параметром указываете на TOveriapped. Эта структура содержит дескриптор события Windows (event).
ОС начинает операцию (ее выполняет отдельный программный поток, скрытый от программиста) и немедленно возвращает управление; вы можете не тратить время на ожидание. Признак того, что операция началась и продолжается — получение кода возврата ERROR_IO_PENDING. Пусть вас не пугает слово "error" в названии — это совершенно нормально. Если операция продолжается долго (а чтение и запись файлов на дискете, да и на диске, именованных каналов можно отнести к "длинным" операциям), то программа может спокойно выполнять последующие операторы. Событие будет "взведено" ОС тогда, когда ввод/вывод закончится.
Когда, по мнению программиста, ввод/вывод должен быть завершен, можно проверить это, использовав функцию WaitForSingleObject.
function WaitForSingleObject(hHandle: THandle; dwMilliseconds: DWORD): DWORD;
Объект ожидания (параметр hHandle) в этом случае — тот самый, который создан нами, указан в структуре TOveriapped и передан в качестве параметра в функцию ReadFile или WriteFile. Можно указать любое время ожидания, в том числе бесконечное (параметр Timeout при этом равен константе INFINITE). Признаком нормального завершения служит получение кода возврата WAIT_OBJECT_0.
Листинг 9.2. Пример отложенной операции чтения 
function TMyClass.Read(var Buffer; Count: Longint): Longint;
var succ : boolean;nb : Cardinal;LastError : Longint;
Overlap: TOveriapped;
begin
FillChar(Overlap,SizeOf(Overlap),0);
Overlap.hEvent := CreateEvent(nil, True, False, nil);
Result := Maxlnt;
succ := ReadFiie(FHandle, Buffer, Count, nb, SOverlapRd);
//
// Здесь можно вставить любые операторы, которые // могут быть выполнены до окончания ввода/вывода 
//
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;
Если вы задали конечный интервал в миллисекундах, а операция еще не закончена, waitForSingieObject вернет код завершения WAIT_TIMEOUT. Функция GetOverlappedResult возвращает в параметре nb число байтов, действительно прочитанных или записанных во время отложенной операции.
 
Контроль ошибок ввода/вывода
При работе с файлами разработчик обязательно должен предусмотреть обработку возможных ошибок. Практика показывает, что именно операции ввода/вывода вызывают большую часть ошибок, возникающих в приложении из-за воздействия окружающей программной среды.
Контроль за ошибками ввода/вывода зависит от применяемых функций. При использовании доступа через Win32 API все функции возвращают код ошибки Windows, который и нужно проанализировать.
При возникновении ошибок ввода/вывода в функциях, использующих файловые переменные, генерируется исключительная ситуация класса EinOutError. Но так происходит только в том случае, если включен контроль ошибок ввода/вывода. Для этого используются соответствующие директивы компилятора:

  • {$I+}— контроль включен (установлен по умолчанию); 
  • {$I-} — контроль отключен.

Класс EinOutError отличается тем, что у него есть поле ErrorCode. При возникновении этой исключительной ситуации вы можете получить его значение и принять решение. Основные коды имеют такие значения:

  •  2 — файл не найден;
  •  3 — неверное имя файла;
  •  4 — слишком много открытых файлов;
  •  5 — доступ запрещен;
  •  100 — достигнут конец файла;
  •  101 — диск переполнен;
  •  106 — ошибка ввода.

При отключенном контроле в случае возникновения ошибки выполнение программы продолжается без остановки. Однако в этом случае устранение возможных последствий ошибки возлагается на разработчика. Для этого применяется функция
function lOResult: Integer;
которая возвращает значение 0 при отсутствии ошибок.
 
Атрибуты файла. Поиск файла
Еще одна часто выполняемая с файлом операция — поиск файлов в заданном каталоге. Для организации поиска и отбора файлов используются специальные процедуры, а также структура, в которой сохраняются результаты поиска.
Запись
type
TFileName = string;
TSearchRec = record
Time: Integer; {Время и дата создания}
Size: Integer; {Размер файла}
Attr: Integer; {Параметры файла}
Name: TFileName; {Полное имя файла}
ExcludeAttr: Integer; (He используется}
FindHandle: THandle; {Дескриптор файла}
FindData: TWin32FindData; {He используется}
 end;
обеспечивает хранение характеристик файла после удачного поиска. Дата и время создания файла хранятся в формате MS-DOS, поэтому для получения этих параметров в принятом в Delphi формате TDateTime необходимо использовать следующую функцию:
function FileDateToDateTime(FileDate: Integer): TDateTime;
Обратное преобразование выполняет функция
function DateTimeToFileDate(DateTime: TDateTime): Integer;
Свойство Attr может содержать комбинацию следующих флагов-значений:

  •  faReadOnly — только для чтения;
  •  faDirectory — каталог;
  •  faHidden — скрытый; 
  •  faArchive — архивный;
  •  faSysFile — системный; 
  •  faAnyFile — любой.
  •  favoiumeio — метка тома;

Для определения параметров файла используется оператор 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 TForml.FindBtnClick(Sender: TObject); 
begin
ListBox.Items.Clear;
FindFirst(DirEdit.Text, faArchive + faHidden, SearchRec);
while FindNext(SearchRec) = 0 do
ListBox.Iterns.Add(SearchRec.Name);
FindClose(SearchRec);
 end;
 
Потоки
Потоки — очень удачное средство для унификации ввода/вывода для различных носителей. Потоки представляют собой специальные объекты-наследники абстрактного класса Tstream. Сам Tstream "умеет" открываться, читать, писать, изменять текущее положение и закрываться. Поскольку для разных носителей эти вещи происходят по-разному, конкретные аспекты реализованы в его потомках. Наиболее часто используются потоки для работы с файлами на диске и памятью.

Многие классы VCL имеют унифицированные методы LoadFromstream и saveTostream, которые обеспечивают обмен данными с потоками. От того, с каким физическим носителем работает поток, зависит место хранения данных.

Базовые классы TStream и THandleStream


В основе иерархии классов потоков лежит класс Tstream. Он обеспечивает выполнение основных операций потока безотносительно к реальному носителю информации. Основными из них являются чтение и запись данных.
Класс Tstream порожден непосредственно от класса TObject.
Потоки также играют важную роль в чтении/записи компонентов из файлов ресурсов (DFM). Большая группа методов обеспечивает взаимодействие компонента и потока, чтение свойств компонента из ресурса и запись значений свойств в ресурс.
Таблица 9.3. Свойства и методы класса Tstream


Объявление

Описание

property Position: Longint;

Определяет текущую позицию в потоке

property Size: Longint;

Определяет размер потока в байтах

function CopyFrom( Source: TStream; Count: Longint) : Longint;

Копирует из потока Source Count байты, начиная с текущей позиции. Возвращает число скопированных байтов

function Read(var Buffer; Count: Longint) : Longint; virtual; abstract;

Абстрактный класс, перекрываемый в наследниках. Считывает из потока Count байты в буфер Buffer. Возвращает число скопированных байтов

procedure Read3uffer (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:

  •  soFromBeginning — смещение должно быть положительным и отсчитывается от начата потока;
  •  soFromCurrent — смещение относительно текущей позиции в потоке;
  •  soFromEnd — смещение должно быть отрицательным и отсчитывается от конца потока.

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

  •  метод ReadResHeader — для считывания заголовка ресурса компонента из потока;
  •  метод ReadComponent — для считывания значений свойств компонента. Для записи ресурса в поток применяется метод writeComponentRes.

Класс THandleStream инкапсулирует поток, связанный с физическим носителем данных через дескриптор.
Для создания потока используется конструктор
constructor Create(AHandle: Integer);
в параметре которого передается дескриптор. Впоследствии доступ к дескриптору осуществляется через свойство:
property Handle: Integer;
 
Класс TFileStream
Класс TFileStream позволяет создать поток для работы с файлами. При этом поток работает с файлом без учета типа хранящихся в нем данных (см. выше).
Полное имя файла задается в параметре FileName при создании потока:
constructor Createfconst FileName: string; Mode: Word);
Параметр Mode определяет режим работы с файлом. Он составляется из флагов режима открытия:

  •  fmCreate — файл создается;
  •  fmOpenRead — файл открывается для чтения;
  •  fmopenwrite — файл открывается для записи;
  •  fmOpenReadWrite — файл открывается для чтения и записи.

И флагов режима совместного использования:

  •  fmShareExciusive — файл недоступен для открытия другими приложениями;
  •  fmShareDenyWrite — другие приложения могут читать данные из файла; 
  •  fmShareDenyRead — другие приложения могут писать данные в файл;
  •  fmShareDenyNone — другие приложения могут производить с файлом любые операции.

Для чтения и записи из потока используются методы Read и write, унаследованные от класса THandleStream:
procedure TForml.CopyBtnClick(Sender: TObject);
 var Streaml, Stream2: TFileStream;
IntBuf: array[0..9] of Integer/begin
if Not OpenDlg.Execute then Exit; 
try
Streaml := TFileStream.Create(OpenDlg.FileName, fmOpenRead);
 Streaml.ReadBuffer(IntBuf, SizeOf(IntBuf));
 try
Stream2 := TFileStream.Create('TextFile.tmp', fmOpenWrite); 
Stream2.Seek(0, soFromEnd);
Stream2.WriteBuffer(IntBuf, SizeOf(IntBuf));
 finally
Stream2.Free;
 end; 
finally
Streaml.Free;
 end;
 end;
Обратите внимание, что в данном фрагменте кода функция seek используется для записи данных в конец файлового потока.
При необходимости копирования одного файла в другой целиком используется метод CopyFrom, унаследованный от класса Tstream:
procedure TForml.CopyBtnClick(Sender: TObject);
 var Streaml, Stream2: TFileStream; 
begin if Not OpenDlg.Execute then 
Exit;
try
Streaml := TFileStream.Create(OpenDlg.FileName, fmOpenRead);
Stream2 := TFileStream.Create('Sample.tmp1, fmOpenWrite);
Stream2.Seek{0, soFromEnd);
Stream2.CopyFrom(Streaml, Streaml.Size);
 finally
Streaml.Free;
Stream2.Free; 
end;
 end;
Обратите внимание, что в данном случае идя определения размера передаваемого потока необходимо использовать свойство stream, size, которое дает реальный объем данных, содержащихся в потоке. Функция sizeof (stream) в этом случае даст размер объекта потока, и не более того.
 
Класс TMemoryStream
Класс TMemoryStream обеспечивает сохранение данных в адресном пространстве. При этом методы доступа к этим данным остаются теми же, что и при работе с файловыми потоками. Это позволяет использовать адресное пространство для хранения промежуточных результатов работы приложения, а также при помощи стандартных методов осуществлять обмен данными между памятью и другими физическими носителями.
Свойство
property Memory: Pointer;
определяет область памяти, отведенную для хранения данных потока. Изменение размера отведенной памяти осуществляется методом
procedure SetSize(NewSize: Longint); override;
Для очистки памяти потока используется метод
procedure Clear;
Чтение/запись данных в память выполняется привычными методами Read и Write.
Также запись данных в память может осуществляться методами:

  •  procedure LoadFromFile(const FileName: string); — из файла;
  •  procedure LoadFromStream(Stream: TStream) ; — из другого потока.

Дополнительно можно использовать методы записи данных в файл или поток:
procedure SaveToFile(const FileName: string);
 procedure SaveToStream(Stream: TStream);
 
Класс TStringStream
Так как строковые константы и переменные широко применяются при разработке приложений, то для удобства работы с ними создан специальный класс TStringStream. Он обеспечивает хранение строки и доступ к ней во время выполнения приложения.
Он обладает стандартным для потоков набором свойств и методов, добавляя к ним еще несколько, упрощающих использование строк.
Свойство только для чтения
property DataString: string;
обеспечивает доступ к хранимой строке. Методы
function Read(var 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 И FinddoseChangeNotification. Первая из них возвращает дескриптор объекта файлового оповещения, который можно передать в функцию ожидания. Объект активизируется тогда, когда в заданной папке произошли те или иные изменения (создание или уничтожение файла или папки, изменение прав доступа и т. д.). Вторая функция — готовит объект к реакции на следующее изменение. Наконец, с помощью третьей функции следует закрыть, ставший ненужным, объект.
Так может выглядеть код метода 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 Forml.UpdateList;
if not FindNextChangeNotification(fn) then break;
until Terminated;
FinddoseChangeNotification (fn) ;
 end;
На главной форме должны находиться компоненты, нужные для выбора обследуемой папки, а также компонент TListBox, в который будут записываться имена файлов:
procedure TForml.ButtonlClick(Sender: TObject); 
var dir : string; 
begin
if SelectDirectory(dir, [],0) then 
begin
Editl.Text := dir; DirName := dir; 
end; 
end;
procedure TForml.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 и pagefile.sys в Windows NT). Необходимый фрагмент виртуальной памяти переносится из страничного файла в ОЗУ и, таким образом, становится доступным.
А что, если так же поступить и с любым другим файлом и сделать его частью адресного пространства? В Win32 это возможно. Для выделения фрагмента памяти должен быть создан специальный системный объект Win32, называемый отображаемым файлом. Этот объект "знает", как соотнести файл, находящийся на жестком диске, с памятью, адресуемой процессами.
Одно или более приложений могут открыть отображаемый файл и получить тем самым доступ к данным этого объекта. Таким образом, данные, помещенные в страничный файл приложением, использующим отображаемый файл, могут быть доступны другим приложениям, если они открыли и используют тот же самый отображаемый файл.
Создание и использование объектов файлового отображения осуществляется посредством функций Windows API. Этих функций три:
CreateFileMapping
MapViewOfFile
UnMapViewOfFile
Отображаемый файл создается операционной системой при вызове функции CreateFileMapping. Этот объект поддерживает соответствие между содержимым файла и адресным пространством процесса, использующего этот файл. Функция CreateFiieMapping имеет шесть параметров:
function CreateFiieMapping(hFile: THandle; IpFileMappingAttributes: PSecurityAttributes; flProtect, dwMaximumSizeHigh, dwMaximumSizeLow: 
DWORD; IpName: PChar): THandle;
Первый параметр имеет тип THandle. Он должен соответствовать дескриптору уже открытого при помощи функции createFile файла. Если значение параметра hFile равно SFFFFFFFF, то это приводит к связыванию объекта файлового отображения со страничным файлом операционной системы.
Второй параметр — указатель на запись типа TSecurityAttributes. При отсутствии требований к защите данных в Windows NT значение этого параметра всегда равно nil. Третий параметр имеет тип DWORD. Он определяет атрибут защиты. Если при помощи отображаемого файла вы планируете совместное использование данных, третьему параметру следует присвоить значение PAGE_READWRITE.
Четвертый и пятый параметры также имеют тип DWORD. Когда выполняется функция CreateFiieMapping, значение типа DWORD четвертого параметра сдвигается влево на четыре байта и затем объединяется со значением пятого параметра посредством операции and. Проще говоря, значения объединяются в одно 64-разрядное число, равное объему памяти, выделяемой объекту файлового отображения из страничного файла операционной системы. Поскольку вы вряд ли попытаетесь осуществить выделение более чем 4 Гбайт данных, то значение четвертого параметра всегда должно быть равно нулю. Используемый затем пятый параметр должен показывать, сколько памяти в байтах необходимо зарезервировать в качестве совместной. Если вы хотите отобразить весь файл, четвертый и пятый параметры должны быть равны нулю.
Шестой параметр имеет тип PChar и представляет собой имя объекта файлового отображения.
Функция CreateFileMapping возвращает значение типа THandle. В случае успешного завершения возвращаемое функцией значение представляет собой дескриптор созданного объекта файлового отображения. В случае возникновения какой-либо ошибки возвращаемое значение будет равно 0.
Следующая задача — спроецировать данные файла в адресное пространство нашего процесса. Этой цели служит функция MapviewOfFile. Функция 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 :=
CreateFiieMapping(FHandle, nil, PAGE_READWRITE, 0, 0, 'SharedBlock');
 if {hMappedFile = 0) then
ShowMessage('Mapping error!')
else
begin
pSharedBuf :=
MapViewOfFiie(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;
Отображаемые файлы уже будут использоваться и в других главах этой книги. Не стоит удивляться, ведь это очень мощный инструмент: помимо возможности совместного доступа он позволяет заметно ускорить доступ к файлам, особенно большого размера.
 
Резюме
При разработке приложений очень часто приходится решать задачи обмена данными между приложениями или приложением и устройством ввода/вывода. При этом большая часть созданного кода обеспечивает работу приложения с файлами.
В этой главе рассмотрены методы, обеспечивающие взаимодействие программы с файловой системой и примеры их использования.
Для организации обмена данными в приложениях используются специальные объекты — потоки, которые не только хранят информацию во время выполнения приложения, но и предоставляют разработчику набор стандартных свойств и методов для управления данными.

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

 

 
На главную | Содержание | < Назад....Вперёд >
С вопросами и предложениями можно обращаться по nicivas@bk.ru. 2013 г. Яндекс.Метрика