Основы офисного программирования и язык VBA

Файловый ввод-вывод.

Разбить на страницы
Показывать лекцию целиком

Программный код большинства примеров данной лекции можно найти в проектах, доступных для просмотра: DocOne14.

Основные виды файлов в Office 2000

Как правило, приложения Office имеют дело с файлами, хранящими документы этих приложений. Это файлы с документами Word (с расширениями.doc), рабочими книгами Excel (с расширениями.xls), базами данных Access (с расширениями.mdb). Операции по созданию, открытию и записи этих файлов выполняются с помощью соответствующих методов приложений, которые запускаются командами меню File. Но офисные системы должны уметь работать и с файлами других форматов. Например, часто требуется считывать данные из обычных текстовых файлов и заносить информацию, в них хранящуюся, в таблицы Excel или базы данных Access. Возможна и обратная ситуация, когда, используя данные из приложений Office, требуется сохранять полученную информацию в файлах. В общем, есть некоторые ситуации, когда предпочтительнее работать с собственными файлами, а не со стандартными базами данных. По этой причине в VBA включены средства для работы с внешними файлами, позволяющие их создавать, открывать для работы и осуществлять ввод-вывод данных. Файлы VBA делятся на три группы.

  • Файлы последовательного доступа могут открываться в режиме чтения ( Input ), записи ( Output ), или присоединения ( Append ). Специальных средств поиска для них нет, и чтобы получить данные из такого файла, его нужно открыть и прочесть последовательно все данные до нужного места. Изменения (запись данных) в таком файле всегда происходят в его конце. Существенной особенностью этого файла является редактирование данных в процессе записи и чтения. В файл можно записывать данные разных типов, При вводе они будут отредактированы и преобразованы в строку. Таким образом, последовательный файл можно рассматривать, как поток данных, представляющий последовательность строк переменой длины. Признаки конца строк и являются разделителями элементов файла. Доступ к элементам файла только последовательный и этот поток можно только читать или только писать, но нельзя делать то и другое одновременно.
  • Файлы произвольного доступа состоят из записей постоянной длины, что позволяет организовать быстрый поиск данных и их локальное изменение внутри файла без его переписывания. В таких файлах чаще всего хранят записи пользовательского типа, все поля которых имеют фиксированную длину, удобно также хранить числовые данные в двоичном формате. В этой модели файла поток данных представляет последовательность пронумерованных элементов одинаковой длины. Возможен прямой доступ к элементу по его номеру. Разрешается одновременно выполнять операции чтения и записи в файл, можно изменять содержимое отдельных элементов. Эта простая и эффективная в реализации модель файла очень удобна в тех случаях, когда она применима, например, как мы уже говорили для хранения числовых данных. Серьезным ограничением на ее применение является требование постоянства длины элементов файла.
  • Бинарные (или бинарного доступа) файлы позволяют считывать и записывать информацию побайтно; обычно используются для хранения графических данных или любой другой информации в неструктурированном виде. Здесь также как и в предыдущей модели, элементы файла имеют постоянную длину в 1 байт. Существенным отличием является то, что никакого редактирования в момент чтения или записи не производится. Одной операцией может быть записано или прочитано произвольное число байтов.
  • Можно заметить, что среди допустимых моделей файлов VBA нет наиболее общей, когда разрешен прямой доступ к элементам файла, когда элементы могут иметь произвольную длину, когда ключ может быть произвольным и не обязательно должен задавать номер записи. Отсутствие такого типа файлов действительно представляет серьезное ограничение, но этому есть и не менее серьезное оправдание. В таких сложных ситуациях рекомендуется переходить на работу с базой данных. Это тем более оправдано тем, что база данных Access доступна из любого приложения Office 2000.

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

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

    Тип доступа Запись данных Чтение данных
    Последовательный Print#, Write# Input#, Line Input#
    Произвольный Put Get
    Бинарный Put Get

    Открытие и создание файлов

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

    Open имя-файла For режим [Access доступ] [блокировка] As [#]номерФайла [Len=длина-записи]
  • Параметр имя-файла - строковое выражение, задающее имя открываемого (создаваемого) файла, в него может входить также путь ѕ имя диска и имена каталогов (папок) на пути к файлу.
  • Параметр режим - ключевое слово, которое для файлов последовательного доступа может принимать одно из значений: Input (ввод), Output (вывод), Append (присоединение). Для файлов с произвольным доступом в качестве режима нужно указать Random, а для бинарных файлов - Binary.
  • Параметр доступ необязателен, он ограничивает набор операций, которые разрешено выполнять над открываемым файлом; возможные значения: Read (только чтение), Write (только запись), Read Write (и то и другое).
  • Параметр блокировка позволяет ограничить набор операций над открываемым файлом, выполняемых другими процессами; возможные значения: Shared (разделяемый, другим процессам разрешается работать с файлом без всяких ограничений), Lock Read (запретить чтение), Lock Write (запретить запись), Lock Read Write (запретить чтение и запись). Здесь требуется некоторые пояснения. С одним и тем же физическим файлом одновременно могут работать разные приложения. Возможна и ситуация, когда одно приложение одновременно работает с несколькими экземплярами файла, присваивая каждому экземпляру свой уникальный номер. Параметр блокировка позволяет регулировать отношения между клиентами, получающими доступ к файлу.
  • Параметр номерФайла - целое число в интервале от 1 до 511, идентифицирующее файл для других операций. Для каждой операции открытия файла номер-файла должен отличаться от номеров всех открытых в текущий момент файлов. Узнать очередной свободный номер позволяет функция FreeFile.
  • Параметр длина-записи - число не более 32767 - обязателен для файлов произвольного доступа и задает для них длину одной записи файла в байтах. Для последовательных файлов он необязателен (для них он задает размер буфера, создаваемого при открытии), для бинарных файлов он игнорируется.
  • Если последовательный файл, открываемый в режиме вывода ( Output ), существует, его содержимое будет утеряно, и данные будут записываться в его начало. Если его нет, создается новый файл. Точно так же будет создан новый файл, если файл, определяемый параметром имя-файла, не существует, а режим открытия задан как Append, Random или Binary. При попытке открыть несуществующий файл в режиме Input будет получено сообщение об ошибке.

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

    Рассмотрим детали открытия и создания файлов. В первом примере открывается файл последовательного доступа "read.me" для чтения:

    Open "read.me" For Input As #1

    Имя можно передавать и через переменную или более сложное строковое выражение:

    Const Path = "e:\O2000\CD2000\Ch14\"
    
    Public Sub Openfile()
    	Dim NewFile As String, NewNum As Integer
    	NewFile = "read.me"
    	NewNum = FreeFile	' очередной незанятый номер файла
    	Open Path  NewFile For Output As NewNum
    
    End Sub

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

    Файл с последовательным доступом особенно удобен, когда при работе с ним требуется последовательно прочесть и обработать все записи файла. Если же данные из файла обрабатываются в произвольном порядке, или файл открывается для того, чтобы получить доступ к небольшому числу его записей, крайне желательно иметь возможность произвольного (прямого) доступа к нужному элементу файла. Такую возможность и обеспечивают файлы VBA с произвольным доступом, хотя у них есть серьезное ограничение, все записи должны иметь одинаковый размер. Это крайне неудобно в наиболее типичном случае, когда элементами файла с произвольным доступом являются записи, длины которых, естественно, могут быть не постоянны из-за, например, наличия в них строк переменной длины. В таких ситуациях одним из выходов является переход к строкам постоянной длины. Радикальное решение, как мы уже говорили, состоит в том, чтобы в подобных ситуациях переходить на работу с базами данных. Рассмотрим пример открытия двух файлов - файла с произвольным доступом для хранения записей пользовательского типа и бинарного файла для хранения картинки. Вначале дадим объявление пользовательского типа:

    Type Person ' пользовательский тип записи.
    	Возраст As Integer
    	Фамилия As String * 20
    	Имя As String * 20
    End Type

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

    Public Sub OpenTwoFiles()
    	Dim attr As Integer, Num As Integer
    	Dim MyFriend As Person		' объявление переменной
    	' Открываем файл с произвольным доступом для записи:
    	Num = FreeFile
    	Open "Friends" For Random Access Read Write As Num Len = Len(MyFriend)
    	PrintAttr (Num)
    	Debug.Print "Длина Записи:", Len(MyFriend)
    	' Открываем двоичный файл
    	Num = FreeFile
    	Open "pict.bmp" For Binary Access Read Lock Read As Num
    	PrintAttr (Num)
    
    End Sub
    
    Public Sub PrintAttr(ByVal Num As Integer)
    	'Эта процедура по номеру файла определяет его атрибуты
    	'и выводит соответствующее сообщение в окно отладки
    	Dim Msg As String, attr As Integer
    	
    	attr = FileAttr(Num, 1)
    	Debug.Print "файл # ", Num
    	Select Case attr
    		Case 1: Msg = "Input"
    		Case 2: Msg = "Output"
    		Case 4: Msg = "Random"
    		Case 8: Msg = "Append"
    		Case 32: Msg = "Binary"
    		Case Else: Msg = "Таких файлов не бывает"
    	End Select
    	Debug.Print "Открыт для ", Msg
    	
    End Sub

    В этом примере для определения свободного номера файла использовалась функция FreeFile, а для определения длины записи ѕ функция Len. Файл произвольного доступа открыт как для чтения, так и для записи, а бинарный файл только для чтения. Заметьте, что бинарный файл закрыт для чтения другими процессами. Поэтому если повторно запустить процедуру открытия, то файл произвольного доступа будет повторно открыт с другим номером, но возникнет ошибка при повторном открытии бинарного файла. Возможность одновременного открытия под разными номерами нескольких логических экземпляров одного и того же физического файла бывает крайне полезной в ряде ситуаций.

    Функция

    FileAttr(номер-файла, 1)

    по номеру открытого файла возвращает число, указывающее режим открытия: 1 - Input, 2 - Output, 4 - Random, 8 - Append, 32 - Binary. Второй аргумент достался функции FileAttr в наследство от 16-разрядного режима, где при его значении 2, она возвращала системный указатель на файл.

    Мы написали простую процедуру PrintAttr, которая по номеру файла определяет его атрибуты и выводит сообщение в окно отладки. Приведем результаты работы процедуры OpenTwoFiles:

    файл #		 1 
    Открыт для	Random
    Длина Записи:    42 
    файл #		 2 
    Открыт для	Binary

    Закрытие файлов

    Для закрытия файла служит оператор Close, вызываемый:

    Close [список-номеров-файлов]

    Параметр список-номеров-файлов может содержать один или несколько номеров ранее открытых файлов, которые должны быть закрыты. Как обычно, номера в списке разделяются запятыми. Если параметр опущен, то закрываются все открытые в данный момент файлы. После выполнения оператора Close номера закрытых файлов снова можно использовать при открытии в операторе Open.

    Закрыть все открытые оператором Open файлы может также оператор Reset. Как и вызов Close без параметров, он освободит все номера открытых файлов и перенесет содержимое их буферов на диск.

    Запись в файлы последовательного доступа

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

  • Для записи "человеко-ориентированных" данных применяется оператор Print#, представляющий данные в формате, подготовленном для отображения на экране дисплея. По существу, мы уже хорошо знакомы с этим оператором, поскольку ни одна наша программа не обходилась без вывода данных в окно отладки, а применяемый для этих целей метод Print объекта Debug по синтаксису и по действию подобен оператору Print#.
  • Для записи данных, обрабатываемых после их чтения программой, используется оператор Write#. Заметим, в каком - то смысле это более универсальный оператор, чем Print, он, например, не зависит от локализации.
  • Оператор Print# записывает в файл форматированные данные. Его вызов имеет вид:

    Print #номер-файла, [список-вывода]

    Здесь номер-файла - номер файла, открытого в режиме последовательного доступа для записи ( Append или Output ), список-вывода - одно или несколько выражений, разделенных пробелами или точками с запятой, определяющих выводимые данные. Каждое выражение в этом списке может быть снабжено дополнительной информацией о расположении данных, делающее их восприятие более удобным для пользователя. Общий вид такого выражения:

    [{Spc(n) | Tab[(n)]}] [выражение] [симв-поз]
  • Spc(n) задает количество вставляемых пробелов.
  • Альтернативный параметр Tab(n) указывает абсолютный номер n столбца, в котором будет начинаться текст, определяемый параметром выражение. Если не указывать аргумент у Tab, то позицией вставки является следующая зона печати. (Можно полагать, что при выводе экран разделен на зоны, и табуляция позволяет переходить от одной зоны к другой).
  • Параметр выражение задает выражение, значение которого после редактирования и преобразования его в текстовую строку будет записано в файл. На выражения не накладывается особых ограничений, Это могут быть как строковые, так и числовые выражения, они могут содержать данные разных типов. Даты записываются в файл в кратком формате, значения булевых переменных - как ключевые слова Ложь ( False ) или Истина ( True ), пустое значение Empty - как пустое слово, нулевое значение Null выводится как "Null". Данные об ошибке выводятся в виде кода ошибки. Числовые данные выводятся в формате, зависящем от локализации. При выводе можно вызвать для форматирования данных функцию Format.
  • Параметр симв-поз задает положение следующего выражения. Если он равен ";" или пробелу, вставка следующего выражения будет происходить сразу за последним выведенным символом, Tab(n) указывает номер n столбца, начиная с которого будет выводиться выражение. Tab без аргумента или "," - переход в следующую позицию табуляции (зону печати).
  • Создадим теперь открытый ранее файл read.me, записывая в него выражения, позволяющие продемонстрировать все возможности оператора Print:

    Public Sub WritingWithPrint()
    		Dim MyBool As Boolean, MyDate As Date
    		Dim MyNull As Variant, MyFloat As Double, MyErr As Variant
     
    	Print #1, "Первая строка файла"			' запись текстовой строки.
    	'Первая строка файла
    	Print #1, "Зона 1"; Tab; "Зона 2"	' запись в двух позициях табуляции.
    	'Зона 1 Зона 2
    	Print #1, "Зона 1", "Зона 2"		 ' запятая разделяет как табуляции.
    	'Зона 1 Зона 2
    	Print #1, "Привет,"; "старик!"	'; склеивает выражения.
    	'Привет,старик!
    	Print #1, "Привет,"; "старик!"	' пробел действует аналогично.
    	'Привет,старик!
    	Print #1, Spc(5); "5 пробелов "		' вставка 5 пробелов слева.
    	'	 5 пробелов
    	Print #1, Tab(20); "Старик,"; Tab(10); "привет!"	' печать в соответствующих столбцах.
    	'					Старик
    	'		 привет!
    	Print #1, "one"; " "; "two"		'разделение пробелом
    	'one two
    	MyBool = False					 ' булева переменная
    	Print #1, MyBool
    	'False
    	MyDate = #6/14/1999#				' значение даты
    	Print #1, MyDate				 ' печать в кратком формате
    	'14.06.99
    	MyNull = Null						' нулевое значение
    	Print #1, MyNull
    	'Null
    	MyFloat = 3.1416					'вещественное значение
    	' использование функции Format:
    	Print #1, MyFloat, Format(MyFloat, "#.00"), Format(MyFloat, "00.000")
    	'3,1416 3,14 03,142
    	On Error Resume Next
    	Err.Raise 6
    	MyErr = Err
    	Print #1, MyErr
    	MyErr = CVErr(2000)
    	Print #1, MyErr
    End Sub

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

    Public Sub ReadingWithLine()
    	Dim MyStr As String
    	
    	Close #1
    	Open Path  "read.me" For Input As #1
    	Do While Not EOF(1)
    		Line Input #1, MyStr
    		Debug.Print MyStr
    	Loop
    End Sub

    Текст в окне отладки выглядит в полном соответствии с нашими ожиданиями. Вот он:

    Первая строка файла
    Зона 1		Зона 2
    Зона 1		Зона 2
    Привет,старик!
    Привет,старик!
    	 5 пробелов 
    					Старик,
    		 привет!
    one two
    False
    14.06.99 
    Null
     3,1416		3,14			03,142
     6 
    Error 2000

    Обратите внимание, ошибки, возбуждаемые методом Raise и функцией CVErr, редактируются по-разному, в первом случае записывается только код ошибки, во втором ѕ код сопровождается ключевым словом Error, не переводимым в локализованных версиях.

    Заметьте, точно такой же текст можно получить, если в процедуре WritingWithPrint заменить оператор Print на Debug.Print, поскольку этот метод и этот оператор порождают одинаковый результат, разница лишь в том, что один из них помещает результаты в файл, а другой ѕ в окно отладки.

    При выводе данных можно управлять шириной выводимых строк. Ширину выводимых строк для открытого файла устанавливает оператор:

    Width #номер-файла, ширина

    Параметр ширина принимает значения от 0 до 255. Если при печати в файл оператором Print добавление значения очередного выражения в строку приведет к превышению установленной границы, это значение будет печататься с новой строки. При этом значение одного выражения не переносится, даже если его длина превышает установленную границу. Например, если для файла с номером 1 ограничить ширину строки 5 символами:

    Width #1,5

    а затем напечатать строку:

    Str = "раму мылом"
    Print #1, "мама", "мыла", Str

    то в файле окажутся строки:

    мама
    мыла
    раму мылом

    Обращаем внимание и на то, что переход к новой строке может происходить и при встрече с разделителем Tab(n), если n задает позицию меньшую, чем текущая позиция. Пример такой ситуации встречался в нашей процедуре.

    Как было показано, оператор Print вводит в файл данные разных типов, редактируя их в момент ввода и преобразуя в текстовый формат. Попутно он занимается и форматированием текста, приводя его к форме, удобной для отображения на дисплее. Во многих ситуациях форматирование, ориентированное на выдачу на экран дисплея, не является необходимым. В этих случаях для создания последовательного файла применяется Write#. Его синтаксис:

    Write #номер-файла, [список-вывода ]

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

    Также как и оператор Print, оператор Write производит редактирование данных в момент записи. При этом приняты следующие соглашения:

  • В числах в качестве разделителя целой и дробной частей всегда используется точка.
  • В качестве булевых значений используются слова #TRUE# и #FALSE#.
  • При записи дат применяется универсальный формат.
  • Ошибки выводятся в формате #ERROR код-ошибки#.
  • Для пустого значения выражения ( Empty ) ничего не записывается
  • Значение Null записывается как #Null#.
  • В отличие от оператора Print, оператор Write# вставляет при записи запятые между соседними выражениями и заключает строковые значения в кавычки. После записи последнего выражения вставляется перевод на новую строку (Chr(13) + Chr(10)).

    Следующая процедура создает файл с последовательным доступом "readme.txt" и записывает в него данные с помощью оператора Write#. При записи мы старались повторить содержимое предыдущего файла, чтобы можно было сравнить работу, выполняемую операторами Print и Write. Вот текст этой процедуры:

    Public Sub WritingWithWrite()
    	Dim MyStr As String, MyBool As Boolean, MyDate As Date
    	Dim MyNull As Variant, MyFloat As Double, MyErr As Variant
    	
    	'Открытие файла readme.txt
    	Open Path  "readme.txt" For Output As #7
    	
    	'Создание файла
    	Write #7, "Первая строка файла"			' запись текстовой строки.
    	Write #7, "Зона 1", "Зона 2"	' запись двух строк.
    	Write #7, "Привет,", "старик!"	'еще две строки
    	MyStr = "раму мылом. "
    	Write #7, "Мама ", "мыла ", MyStr
    	MyBool = False					 ' булева переменная
    	Write #7, MyBool
    	MyDate = #6/14/1999#				' значение даты
    	Write #7, MyDate					' запись даты
    	MyNull = Null						' нулевое значение
    	Write #7, MyNull
    	MyFloat = 3.1416					'вещественное значение
    	' использование функции Format:
    	Write #7, MyFloat, Format(MyFloat, "0.00"), Format(MyFloat, "00.000")
    	On Error Resume Next
    	Err.Raise 6
    	MyErr = Err
    	Write #7, MyErr
    	MyErr = CVErr(2000)
    	Write #7, MyErr
    
    End Sub

    Результат выполнения этой процедуры - файл "readme.txt" - в текстовом редакторе выглядит так:

    "Первая строка файла"
    "Зона 1","Зона 2"
    "Привет,","старик!"
    "Мама ","мыла ","раму мылом. "
    #FALSE#
    #1999-06-14#
    #NULL#
    3.1416,"3,14","03,142"
    6
    #ERROR 2000#

    Сравните эти строки с теми, что выдаются оператором Print.

    Чтение файлов последовательного доступа

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

  • Input# ѕ данные в файле записаны оператором Write#
  • LineInput# ѕ данные в файле записаны оператором Print#.
  • Оператор Input# вызывается так:

    Input #номер-файла, список-переменных
  • Параметр номер-файла - номер открытого файла,
  • список-переменных - одно или несколько разделенных запятыми имен переменных, в которые будут считываться данные из файла. Переменные могут быть разных типов, естественное требование их тип должен быть согласован с типом записи в момент ее создания. Переменные не должны быть массивами или объектами, но могут быть элементами массивов или полями пользовательских записей.
  • При чтении автоматически выполняются преобразования данных, обратные тем, что использовались при записи оператором Write#. В частности, кавычки вокруг строк, разделяющие запятые и пустые строки, игнорируются. Слово #NULL# дает значение Null, #TRUE# и #FALSE# переводятся в булевы значения True и False, универсальные даты вида #yyyy-mm-dd hh:mm:ss# переводятся в формат соответствующих переменных типа дата/время, #ERROR номер-ошибки# при чтении передает в переменную типа Variant номер ошибки. Если для числовой переменной соответствующие по порядку в файле данные будут не числовыми, ей будет присвоено значение 0.

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

    EOF(номер-файла)

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

    Приведем процедуру чтения записей ранее созданного файла "readme.txt". Заметьте, мы поступаем корректно, читаемые переменные имеют тип, согласованный с типом читаемой записи.

    Public Sub ReadingWithInput()
    	Dim MyStr As String, MyBool As Boolean, MyDate As Date
    	Dim MyNull As Variant, MyFloat As Double, MyErr As Variant
    	Dim i As Integer
    	'Открытие файла readme.txt
    	Open Path  "readme.txt" For Input As #7
    	
    	'Чтение файла
    	'Первые 8 строк
    	For i = 1 To 8
    		Input #7, MyStr
    		Debug.Print MyStr
    	Next i
    	
    	'Данные разных типов
    	Input #7, MyBool, MyDate, MyNull, MyFloat
    	Debug.Print MyBool, MyDate, MyNull, MyFloat
    			
    	'читаем отформатированные числа
    	Input #7, MyStr
    	Debug.Print MyStr
    	Input #7, MyStr
    	Debug.Print MyStr
    		
    	'дважды читаем данные типа Error
    	Input #7, MyErr
    	Debug.Print MyErr
    	Input #7, MyErr
    	Debug.Print MyErr
    	
    	'чтение до конца файла
    	Do While Not EOF(7)
    		Input #7, MyStr
    		Debug.Print MyStr
    	Loop
    End Sub

    В результате будет напечатано:

    Первая строка файла
    Зона 1
    Зона 2
    Привет,
    старик!
    Мама 
    мыла 
    раму мылом. 
    False	 14.06.99   Null   3,1416 
    3,14
    03,142
     6 
    Error 2000

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

    Для построчного ввода данных из файлов с последовательным доступом, созданных оператором Print, вызывается оператор Line Input#:

    Line Input #номер-файла, переменная

    Здесь номер-файла имеет тот же смысл, что и для оператора Input, а переменная - имя строковой переменной или переменной типа Variant, в которую будет прочитана очередная строка файла. Оператор Line Input# считывает данные посимвольно, пока не обнаружит признак конца строки - символ возврата каретки ( Chr(13) ) или пару "возврат каретки - перевод строки" ( Chr(13) + Chr(10) ). Эти признаки в переменную не записываются. Никаких преобразований данных при построчном вводе не производится. Мы уже приводили пример чтения файла "read.me", созданного оператором Print#.

    Следует заметить, что любой файл может быть открыт для чтения и прочитан оператором Line Input#. Другое дело, что, чаще всего, это не даст желаемого результата. Если, например, прочитать файл "readme.txt" с использованием оператора Line Input#, то будут получены следующие результаты:

    "Первая строка файла"
    "Зона 1","Зона 2"
    "Привет,","старик!"
    "Мама ","мыла ","раму мылом. "
    #FALSE#
    #1999-06-14#
    #NULL#
    3.1416,"3,14","03,142"
    6
    #ERROR 2000#

    Несмотря на успех чтения, результаты не приемлемы, поскольку не выполняется нужного редактирования. Сохраняются кавычки, запятые, ограничители, в одной строке содержатся данные разных типов. Поэтому оператор Line Input # следует применять, как правило, для чтения строк из файлов, записанных с помощью оператора Print#.

    Оператор Line Input # позволяет читать файл строка за строкой. Наряду с этим в языке VBA есть возможность чтения из файла произвольного числа символов или произвольного числа байтов. Для чтения символов Input или Binary файлов используется функция Input, имеющая следующий синтаксис:

    Input(размер, [#]номер-файла)

    где размер - количество считываемых символов, номер-файла - номер открытого файла (последовательного или бинарного). Данные, считываемые этой функцией, обычно должны быть записаны в файл операторами Print# или Put.

    Еще один вариант этой функции:

    InputB(размер, [#]номер-файла)

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

    Ввод-вывод для файлов произвольного доступа и бинарных файлов

    Для последовательных файлов запись данных всегда происходит в конец файла, а чтение после открытия производится с самого начала. Для бинарных файлов и файлов произвольного доступа операция чтения выполняется оператором Get, операция записи ѕ оператором Put. Эти операции позволяют читать и записывать записи в произвольном порядке. Установить нужную позицию записи или чтения можно, вызвав оператор Seek:

    Seek [#]номер-файла, позиция

    Параметр номер-файла - номер открытого в режиме Random или Binary файла; позиция - число от 1 до 2 147 483 647, определяющее место в файле, куда будут помещены данные следующим оператором Put или откуда они будут считаны следующим оператором Get. Для файлов произвольного доступа позиция задает номер записи в файле, для бинарных и последовательных файлов - номер байта (символа). Иногда, в процессе поиска позиции необходимо передвинуться вперед или назад на некоторое число записей относительно текущей позиции. Функция Seek может быть использована для того, чтобы определить текущую позицию:

    Seek(номер-файла)

    Она возвращает число в диапазоне от 1 до 2 147 483 647. Для файлов произвольного доступа - это номер следующей считываемой или записываемой записи, для файлов, открытых в остальных режимах, - позиция байта, в которой будет выполняться следующая операция

    Операторы Put и Get позволяют сами установить позицию, переопределив, тем самым, позицию, установленную оператором Seek. Эти операторы имеют одинаковый, простой и ясный синтаксис:

    Put [#]номер-файла, [номер-записи], переменная
    Get [#]номер-файла, [номер-записи], переменная

    Параметры этих операторов имеют следующий смысл:

  • Параметр номер-файла - номер открытого в режиме Random или Binary файла;
  • номер-записи - числовое выражение, определяющее позицию в файле, куда будут помещены вводимые данные или откуда данные будут прочитаны. Для файлов произвольного доступа - это номер записи в файле, для бинарных файлов - порядковый номер байта в файле. Нумерация записей (байтов) в файле начинается с 1. Если параметр номер-записи опустить, данные будут записаны (прочитаны) в текущую позицию. Уже говорилось, что установить текущую позицию можно оператором Seek. Отметим также, что после выполнения операторов Get и Put текущей становится позиция очередной записи (байта), следующей за только что прочитанной записью (байтом). Заметьте, даже если этот параметр опущен, разделяющая запятая все же остается в операторе вызова.
  • Параметр переменная - имя переменной, значение которой должно быть записано в файл (в которую будет прочитана запись).
  • Вот простой пример записи и чтения в файл произвольного доступа:

    Public Sub Test1()
    	Dim N As Integer
    	Open Path  "test.1" For Random Access Read Write As #1 Len = 2
    	N = 1
    	Put 1, 1, N
    	Get 1, 1, N
    	Debug.Print N, LOF(1)
    	N = 2
    	Put 1, 2, N
    	Get 1, 2, N
    	Debug.Print N, LOF(1)
    	N = 3
    	Put 1, 1, N
    	Get 1, 1, N
    	Debug.Print N, LOF(1)
    	Close #1
    		
    End Sub

    Используемая здесь функция LOF(номер-файла) возвращает длину файла в байтах. Заметьте, будет создан файл из двух записей, поскольку третий по счету оператор Put изменяет значение первой записи, так что перед закрытием функция LOF возвратит значение 4.

    Но давайте рассмотрим чуть более серьезный пример. Рассмотрим работу с файлом, который назовем "Товары.9". Этот файл будет содержать информацию о товарах. Код товара будет служить ключом записи. Зная код товара, можно будет найти запись в файле. На практике, когда формальным ключом записи, как в нашем случае, является ее порядковый номер, всегда задается некоторая функция, которая реальный ключ, возможно заданный несколькими параметрами, преобразует в порядковый номер. В нашем примере, чтобы не вводить таких преобразований, будем полагать, что код товара изменяется в пределах от 1 до N и, следовательно, он может служить формальным ключом записи. Вот как задается пользовательский тип, описывающий товар:

    Type Товар
    	КодТовара As Integer
    	Наименование As String * 10
    	Цена As Integer
    End Type

    Создадим файл, хранящий записи этого типа. Чтобы можно было проследить за деталями, наш файл будет содержать не более 10 записей. Мы используем случайные числа для генерирования записей этого файла. Вот три процедуры, решающие эту задачу:

    Public Sub CreateRec(Rec As Товар)
    	'Создает запись типа Товар
    	Randomize
    	Rec.КодТовара = Int(Rnd * 9 + 1) 'Код от 1 до 9
    	Rec.Наименование = "Товар"  Rec.КодТовара
    	Rec.Цена = Int(Rnd * 99 + 1) * Rec.КодТовара
    	
    End Sub
    
    Public Sub PrintRec(Rec As Товар)
    	'Печать записи о товаре
    	Debug.Print "Код Товара:", Rec.КодТовара, "Цена:", Rec.Цена
    End Sub
    
    Public Sub CreateRandomFile()
    	Dim i As Integer, NewRec As Товар
    	Open Path  "Товары.9" For Random Access Write As #1 Len = Len(NewRec)
    	'Создаем 10 записей
    	For i = 1 To 10
    		Call CreateRec(NewRec)
    		Call PrintRec(NewRec)
    		Put #1, NewRec.КодТовара, NewRec
    		
    	Next i
    	Debug.Print "Файл Товары.9 успешно создан"
    	Debug.Print "Размер файла", LOF(1)
    Close #1
    End Sub

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

    Код Товара:	7			Цена:			294 
    Код Товара:	3			Цена:			141 
    Код Товара:	3			Цена:			84 
    Код Товара:	1			Цена:			18 
    Код Товара:	6			Цена:			246 
    Код Товара:	8			Цена:			472 
    Код Товара:	8			Цена:			376 
    Код Товара:	7			Цена:			483 
    Код Товара:	2			Цена:			168 
    Код Товара:	8			Цена:			224 
    Файл Товары.9 успешно создан
    Размер файла	112

    Прокомментируем эти процедуры и полученные результаты. Первая процедура генерирует запись, используя датчик случайных чисел ѕ функцию Rnd. Ранее в своих примерах мы уже использовали эту функцию и подробно говорили о том, как она работает. Вторая процедура позволяет вывести запись в окно отладки. Основной является процедура CreateRandomFile, в которой открывается файл "Товары.9" как файл с произвольным доступом, открытый для записи. При открытии, что очень важно, мы не забыли указать длину записи. Поскольку файл предназначен для хранения значений переменных типа "Товар", то этот тип и определяет длину записи, ѕ она равна 14 байтов (10 байтов занимает строка постоянной длины и 4 байта требуется для двух полей типа Integer).В процедуре 10 раз генерируется новая запись и 10 раз выполняется оператор Put, производящий запись в файл. Но как показывают результаты отладочной печати, некоторые коды повторяются, поэтому в таких случаях происходит обновление содержимого записи. В данном примере только 6 записей имеют уникальный код, и 4 раза происходило обновление уже созданных записей. Важно обратить внимание на длину созданного файла. Анализируя эту характеристику, можно понять, что файл содержит 8 записей, а не 10 и не 6. Дело в том, что длина файла произвольного доступа определяется записью с максимальным номером. Пусть в файле есть запись с максимальным номером M. И пусть добавляется новая запись с номером N, большим M. В этот момент в файле будут созданы новые записи с номерами от M+1 до N включительно. Запись с номером N будет действительно записана, а остальные получат значения по умолчанию, в соответствии с соглашениями, принятыми в VBA. Так что, если в файл пишется одна единственная запись с ключом 10000, то это означает, что в файле автоматически появляется 10000 записей и таков будет его размер. В нашем примере максимальный код равен 8,ѕ отсюда и размер файла равен 112 = 8*14

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

    Public Sub PrintFile()
     Dim N As Integer, i As Integer, Code As Integer
    	Dim MyRec As Товар
    	'Открыть файл Товаров для обработки
    	Open Path  "Товары.9" For Random Access Read As #1 Len = Len(MyRec)
    
    	N = LOF(1) \ Len(MyRec) 'Число	записей
    	Debug.Print "Число	записей файла-- ", N
    	For i = 1 To N
    		'Установить позицию
    		Seek 1, i
    		'Получить запись с заданным кодом
    		Get #1,, MyRec
    		Call PrintRec(MyRec)
    	Next i
    	Close #1
    End Sub

    Вот на какие моменты следует обратить внимание. Файл открывается теперь для чтения и при открытии корректно указывается длина записи. Для подсчета числа записей файла используется упоминавшаяся функция LOF. Заметьте, мы используем оператор Seek для установки позиции, но это скорее для проформы, поскольку в данной ситуации Seek не изменяет уже установленной позиции. Вот результаты работы этой процедуры:

    Число	записей файла--		8 
    Код Товара:	1			Цена:			18 
    Код Товара:	2			Цена:			168 
    Код Товара:	3			Цена:			84 
    Код Товара:	0			Цена:			0 
    Код Товара:	0			Цена:			0 
    Код Товара:	6			Цена:			246 
    Код Товара:	7			Цена:			483 
    Код Товара:	8			Цена:			224

    Мы уже пояснили, почему в файле оказалось 8 записей и почему часть из них имеет нулевые значения.

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

    Public Sub WorkWithFile()
    	Dim N As Integer, i As Integer, Code As Integer
    	Dim MyRec As Товар
    	Randomize
    	'Открыть файл Товаров для обработки
    	Open Path  "Товары.9" For Random Access Read As #1 Len = Len(MyRec)
    
    	N = Int(Rnd * 9 + 1) 'Число обрабатываемых записей
    	Debug.Print "Число обрабатываемых записей -- ", N
    	For i = 1 To N
    		Code = Int(Rnd * 9 + 1)
    		'Установить позицию
    		Seek 1, Code
    		'Получить запись с заданным кодом
    		Get #1,, MyRec
    		If MyRec.КодТовара = 0 Then
    			Debug.Print "В файле нет записи с кодом:", Code
    		Else: Call PrintRec(MyRec)
    		End If
    	Next i
    	Close #1
    End Sub

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

    Число обрабатываемых записей --   2 
    Код Товара:   8			Цена:   224 
    В файле нет записи с кодом:   5

    Работа с данными переменной длины

    До сих пор мы усиленно подчеркивали, что при работе с файлом произвольного доступа размер записи фиксирован и это предполагает хранение в таких файлов данных, размер которых фиксирован в момент их объявления. На самом деле, это не так и в таких файлах можно хранить и строки переменной длины и данные типа Variant. В этих случаях просто следует быть более осторожным, поскольку из-за сложности правил хранения таких данных повышается возможность ошибки. Вот некоторые правила, согласно которым оператор Put размещает данные в файле, а оператор Get их читает:

  • Данные всегда помещаются в файл, начиная с границ записей, установленных параметром Len при открытии файла. Если при этом длина данных, записываемых в файл, меньше размера записи, заданного параметром Len, оператор Put заполнит остающееся до длины записи место "случайными" данными из буфера файла, а новые данные поместит, начиная со следующей границы. При попытке записать данные, длина которых больше Len, появится сообщение об ошибке. Таким образом, параметр Len задает максимальный размер записи, внутри которого может храниться значение переменного размера.
  • При записи строк переменной длины, переменных типа Variant, динамических массивов VBA помещает туда же дополнительную информацию для их расшифровки.
  • При записи строки переменной длины Put помещает непосредственно перед ней 2 байта с ее длиной.
  • Два дополнительных байта также помещаются в файл при записи переменной типа Variant. Их содержимое идентифицирует тип значения (как в функции VarType ). Например, если оно равно 3, это соответствует типу Long, и запись такой переменной займет 6 байтов (2 - для типа и 4 - для самого числа). Значение VarType = 8 соответствует строковому типу ( String ). В этом случае Put запишет 2 байта с VarType, затем 2 байта с длиной строки и затем саму строку.
  • В случае динамического массива Put записывает перед его элементами описатель с длиной 2 + 8* (число измерений массива).
  • Массивы фиксированного размера записываются без описателей. Без всякой дополнительной информации записываются и значения остальных типов переменных с фиксированными размерами.
  • Для бинарных файлов параметр Len при записи роли не играет. Put записывает значения переменных подряд без пропусков и не помещает длины динамических строк и дескрипторы массивов.
  • Для переменных с нефиксированными размерами Get выполняет преобразования, обратные тем, что при записи выполнял оператор Put. Например, при чтении в строку переменной длины Get определяет по 2-байтовому описателю длину строки, а затем читает ее содержимое в переменную. Аналогичная расшифровка происходит и при чтении в переменные типа Variant и в динамические массивы.
  • Для бинарных файлов Get читает данные подряд. В переменные фиксированного размера считывается соответствующее их размеру число байтов. При чтении в строку переменной длины в нее считывается число символов, равное длине ее текущего значения.
  • Приведем пример работы с файлом произвольного доступа, записи которого содержат данные не фиксированного размера:

    Public Sub Test2()
    	'Эта процедура работает с файлом, записи которого
    	'содержат строки переменной длины, не превосхоящей максимума,
    	'а также данные типа Variant
    	
    	Dim Fam As String
    	Dim Other As Variant
    	'Открытие файла произвольного доступа для чтения и записи
    	Open Path  "Strings.var" For Random Access Read Write As #5 Len = 20
    	
    	Fam = "Степанов"
    	Put 5, 1, Fam
    	Get 5, 1, Fam
    	Debug.Print Fam, LOF(5)
    	Fam = "Архангельский"
    	Put 5, 2, Fam
    	Get 5, 2, Fam
    	Debug.Print Fam, LOF(5)
    	Fam = "Куц"
    	Put 5, 1, Fam
    	Get 5, 1, Fam
    	Debug.Print Fam, LOF(5)
    	
    	'Запись типа Variant
    	Other = "Петров"
    	Put 5, 3, Other
    	Get 5, 3, Other
    	Debug.Print Other, LOF(5)
    	Other = 125.25
    	Put 5, 4, Other
    	Get 5, 4, Other
    	Debug.Print Other, LOF(5)
    	Other = 125
    	Put 5, 5, Other
    	Get 5, 5, Other
    	Debug.Print Other, LOF(5)
    
    	Close #5
    End Sub

    Вот результаты ее работы:

    Степанов		10 
    Архангельский	        35 
    Куц			35 
    Петров		        50 
    125,25		        70 
    125			84

    Приведем комментарии:

  • Операторы Put и Get позволяют записать и корректно прочитать данные переменного размера, не превосходящего максимальный размер, заданный при открытии файла.
  • При записи первой строки переменной длины размер файла стал равным 10 байтам, из которых 8 байтов занимает сама строка, а два байта, предшествующий ей описатель. Описатель позволяет оператору Get корректно прочитать эту строку.
  • Запись второй строки начинается с 21-го байта, границы первой записи, общий размер файла становится равным 35 с учетом размера второй строки и ее описателя.
  • Третья запись не изменяет размера файла, поскольку происходит изменение значения первой записи, так что файл по-прежнему содержит только две записи.
  • Далее начинается запись данных типа Variant. Теперь при записи строки используются два описателя, занимающие 4 байта.
  • Две последние записи содержат числовые данные Double (8 байтов) и Integer (2 байта) с их описателями.
  • Заканчивая тему работы с файлами произвольного доступа, хотим еще раз обратить внимание на некоторые опасные моменты, возникающие в процессе работы с ними и не контролируемые VBA:

  • Явно задавайте при открытии файла, как при чтении, так и при записи параметр Len ѕ максимальный размер записи. Если этого не сделать, то программа будет работать, но, к сожалению, результаты могут быть не предсказуемыми.
  • Будьте особенно внимательными при работе с данными переменной длины, помните, что к данным автоматически присоединяются описатели.
  • Размер файла определяется записью с максимальным номером. Чтобы избежать большого числа "дырок" в файле стройте разумные функции ключа так, чтобы ключи записей занимали достаточно плотный начальный интервал от 1 до N.
  • Один пример работы с Binary файлом

    Бинарные файлы пишутся и читаются порциями, состоящими из произвольного числа байтов. Доступ возможен к любому байту этого файла в произвольном порядке. Никакого редактирования при операциях с этим файлом не производится и вся ответственность за сохранением структуры информации ложится на программиста. Операции над этим файлом выполняются операторами Get и Put, но можно читать этот файл побайтно, используя и уже упоминавшуюся функцию Input. Мы не будем строить специальный пример для работы с этим файлом, а ограничимся примером чтения ранее созданного текстового файла "readme.txt", который откроем, как Binary файл. В этом примере мы прочитаем последовательно этот файл порциями по 20 байтов. Заодно покажем, как организуется чтение "хвоста" файла ѕ его последней порции. Возможно, с содержательной точки зрения пример не очень хорош, поскольку будет нарушена реальная структура читаемых данных, нов этом то и вся суть дела при работе с такими файлами. Программист сам должен восстановить структуру данных, чем мы, в данном случае, заниматься не будем. Но вот сам пример:

    Public Sub Test3()
    	'Чтение Binary файла
    	Dim MyLocation As Long, MyLoc As Long, MyLine As String
    	
    	Open Path  "readme.txt" For Binary As #1
    	
    	MyLocation = 0
    	Do While MyLocation + 20 < LOF(1)	' Читаем допустимую порцию
    		MyLine = Input(20, #1)
    		MyLocation = Loc(1): MyLoc = Seek(1)	' 2 способа определения текущей позиции
    		Debug.Print MyLine; Tab; MyLocation; Tab; MyLoc
    	Loop
    	'Читаем последнюю порцию
    	MyLoc = LOF(1) - MyLocation
    	If MyLoc > 0 Then
    		MyLine = Input(MyLoc, #1)
    		MyLocation = Loc(1): MyLoc = Seek(1)
    		Debug.Print MyLine, MyLocation, MyLoc, LOF(1)
    	End If
    	Close #1
    End Sub

    Перед комментариями приведем результаты работы:

    "Первая строка файла	        20			21 
    "
    "Зона 1","Зона 2"		40			41 
    
    "Привет,","старик!		60			61 
    "
    "Мама ","мыла ","		80			81 
    раму мылом. "
    #FALS		               100			101 
    E#
    #1999-06-14#
    #N		               120			121 
    ULL#
    3.1416,"3,14",		       140			141 
    "03,142"
    6
    #ERROR                         160			161 
    2000#
      167		               168			167

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

    И последний совет в этой лекции, средств VBA для работы с файлами произвольного доступа достаточно для выполнения основных операций: создания, записи, чтения и модификации данных. Но средства поиска данных в файле (оператор Seek ) весьма примитивны. С их помощью трудно решать задачи упорядочения файлов, поиска записей по значениям отдельных полей и другие задачи, характерные для работы с базами данных. Для небольших файлов можно создавать индексные массивы, в которых номера записей упорядочены в соответствии с требуемым критерием, и поддерживать этот порядок при всех изменениях файла. Для больших - заводить свои индексные файлы и т. п. Но владельцам Office 2000 не следует заниматься программированием собственных СУБД. В их распоряжении - возможность переписать содержимое любого файла произвольного доступа в ячейки рабочего листа Excel или в базу данных (таблицу) Access и использовать для работы с его данными всю мощь этих инструментов.

    Страницы:

    Программный код большинства примеров данной лекции можно найти в проектах, доступных для просмотра: DocOne14.

    Основные виды файлов в Office 2000

    Как правило, приложения Office имеют дело с файлами, хранящими документы этих приложений. Это файлы с документами Word (с расширениями.doc), рабочими книгами Excel (с расширениями.xls), базами данных Access (с расширениями.mdb). Операции по созданию, открытию и записи этих файлов выполняются с помощью соответствующих методов приложений, которые запускаются командами меню File. Но офисные системы должны уметь работать и с файлами других форматов. Например, часто требуется считывать данные из обычных текстовых файлов и заносить информацию, в них хранящуюся, в таблицы Excel или базы данных Access. Возможна и обратная ситуация, когда, используя данные из приложений Office, требуется сохранять полученную информацию в файлах. В общем, есть некоторые ситуации, когда предпочтительнее работать с собственными файлами, а не со стандартными базами данных. По этой причине в VBA включены средства для работы с внешними файлами, позволяющие их создавать, открывать для работы и осуществлять ввод-вывод данных. Файлы VBA делятся на три группы.

  • Файлы последовательного доступа могут открываться в режиме чтения ( Input ), записи ( Output ), или присоединения ( Append ). Специальных средств поиска для них нет, и чтобы получить данные из такого файла, его нужно открыть и прочесть последовательно все данные до нужного места. Изменения (запись данных) в таком файле всегда происходят в его конце. Существенной особенностью этого файла является редактирование данных в процессе записи и чтения. В файл можно записывать данные разных типов, При вводе они будут отредактированы и преобразованы в строку. Таким образом, последовательный файл можно рассматривать, как поток данных, представляющий последовательность строк переменой длины. Признаки конца строк и являются разделителями элементов файла. Доступ к элементам файла только последовательный и этот поток можно только читать или только писать, но нельзя делать то и другое одновременно.
  • Файлы произвольного доступа состоят из записей постоянной длины, что позволяет организовать быстрый поиск данных и их локальное изменение внутри файла без его переписывания. В таких файлах чаще всего хранят записи пользовательского типа, все поля которых имеют фиксированную длину, удобно также хранить числовые данные в двоичном формате. В этой модели файла поток данных представляет последовательность пронумерованных элементов одинаковой длины. Возможен прямой доступ к элементу по его номеру. Разрешается одновременно выполнять операции чтения и записи в файл, можно изменять содержимое отдельных элементов. Эта простая и эффективная в реализации модель файла очень удобна в тех случаях, когда она применима, например, как мы уже говорили для хранения числовых данных. Серьезным ограничением на ее применение является требование постоянства длины элементов файла.
  • Бинарные (или бинарного доступа) файлы позволяют считывать и записывать информацию побайтно; обычно используются для хранения графических данных или любой другой информации в неструктурированном виде. Здесь также как и в предыдущей модели, элементы файла имеют постоянную длину в 1 байт. Существенным отличием является то, что никакого редактирования в момент чтения или записи не производится. Одной операцией может быть записано или прочитано произвольное число байтов.
  • Можно заметить, что среди допустимых моделей файлов VBA нет наиболее общей, когда разрешен прямой доступ к элементам файла, когда элементы могут иметь произвольную длину, когда ключ может быть произвольным и не обязательно должен задавать номер записи. Отсутствие такого типа файлов действительно представляет серьезное ограничение, но этому есть и не менее серьезное оправдание. В таких сложных ситуациях рекомендуется переходить на работу с базой данных. Это тем более оправдано тем, что база данных Access доступна из любого приложения Office 2000.

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

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

    Тип доступа Запись данных Чтение данных
    Последовательный Print#, Write# Input#, Line Input#
    Произвольный Put Get
    Бинарный Put Get

    Открытие и создание файлов

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

    Open имя-файла For режим [Access доступ] [блокировка] As [#]номерФайла [Len=длина-записи]
  • Параметр имя-файла - строковое выражение, задающее имя открываемого (создаваемого) файла, в него может входить также путь ѕ имя диска и имена каталогов (папок) на пути к файлу.
  • Параметр режим - ключевое слово, которое для файлов последовательного доступа может принимать одно из значений: Input (ввод), Output (вывод), Append (присоединение). Для файлов с произвольным доступом в качестве режима нужно указать Random, а для бинарных файлов - Binary.
  • Параметр доступ необязателен, он ограничивает набор операций, которые разрешено выполнять над открываемым файлом; возможные значения: Read (только чтение), Write (только запись), Read Write (и то и другое).
  • Параметр блокировка позволяет ограничить набор операций над открываемым файлом, выполняемых другими процессами; возможные значения: Shared (разделяемый, другим процессам разрешается работать с файлом без всяких ограничений), Lock Read (запретить чтение), Lock Write (запретить запись), Lock Read Write (запретить чтение и запись). Здесь требуется некоторые пояснения. С одним и тем же физическим файлом одновременно могут работать разные приложения. Возможна и ситуация, когда одно приложение одновременно работает с несколькими экземплярами файла, присваивая каждому экземпляру свой уникальный номер. Параметр блокировка позволяет регулировать отношения между клиентами, получающими доступ к файлу.
  • Параметр номерФайла - целое число в интервале от 1 до 511, идентифицирующее файл для других операций. Для каждой операции открытия файла номер-файла должен отличаться от номеров всех открытых в текущий момент файлов. Узнать очередной свободный номер позволяет функция FreeFile.
  • Параметр длина-записи - число не более 32767 - обязателен для файлов произвольного доступа и задает для них длину одной записи файла в байтах. Для последовательных файлов он необязателен (для них он задает размер буфера, создаваемого при открытии), для бинарных файлов он игнорируется.
  • Если последовательный файл, открываемый в режиме вывода ( Output ), существует, его содержимое будет утеряно, и данные будут записываться в его начало. Если его нет, создается новый файл. Точно так же будет создан новый файл, если файл, определяемый параметром имя-файла, не существует, а режим открытия задан как Append, Random или Binary. При попытке открыть несуществующий файл в режиме Input будет получено сообщение об ошибке.

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

    Рассмотрим детали открытия и создания файлов. В первом примере открывается файл последовательного доступа "read.me" для чтения:

    Open "read.me" For Input As #1

    Имя можно передавать и через переменную или более сложное строковое выражение:

    Const Path = "e:\O2000\CD2000\Ch14\"
    
    Public Sub Openfile()
    	Dim NewFile As String, NewNum As Integer
    	NewFile = "read.me"
    	NewNum = FreeFile	' очередной незанятый номер файла
    	Open Path  NewFile For Output As NewNum
    
    End Sub

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

    Файл с последовательным доступом особенно удобен, когда при работе с ним требуется последовательно прочесть и обработать все записи файла. Если же данные из файла обрабатываются в произвольном порядке, или файл открывается для того, чтобы получить доступ к небольшому числу его записей, крайне желательно иметь возможность произвольного (прямого) доступа к нужному элементу файла. Такую возможность и обеспечивают файлы VBA с произвольным доступом, хотя у них есть серьезное ограничение, все записи должны иметь одинаковый размер. Это крайне неудобно в наиболее типичном случае, когда элементами файла с произвольным доступом являются записи, длины которых, естественно, могут быть не постоянны из-за, например, наличия в них строк переменной длины. В таких ситуациях одним из выходов является переход к строкам постоянной длины. Радикальное решение, как мы уже говорили, состоит в том, чтобы в подобных ситуациях переходить на работу с базами данных. Рассмотрим пример открытия двух файлов - файла с произвольным доступом для хранения записей пользовательского типа и бинарного файла для хранения картинки. Вначале дадим объявление пользовательского типа:

    Type Person ' пользовательский тип записи.
    	Возраст As Integer
    	Фамилия As String * 20
    	Имя As String * 20
    End Type

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

    Public Sub OpenTwoFiles()
    	Dim attr As Integer, Num As Integer
    	Dim MyFriend As Person		' объявление переменной
    	' Открываем файл с произвольным доступом для записи:
    	Num = FreeFile
    	Open "Friends" For Random Access Read Write As Num Len = Len(MyFriend)
    	PrintAttr (Num)
    	Debug.Print "Длина Записи:", Len(MyFriend)
    	' Открываем двоичный файл
    	Num = FreeFile
    	Open "pict.bmp" For Binary Access Read Lock Read As Num
    	PrintAttr (Num)
    
    End Sub
    
    Public Sub PrintAttr(ByVal Num As Integer)
    	'Эта процедура по номеру файла определяет его атрибуты
    	'и выводит соответствующее сообщение в окно отладки
    	Dim Msg As String, attr As Integer
    	
    	attr = FileAttr(Num, 1)
    	Debug.Print "файл # ", Num
    	Select Case attr
    		Case 1: Msg = "Input"
    		Case 2: Msg = "Output"
    		Case 4: Msg = "Random"
    		Case 8: Msg = "Append"
    		Case 32: Msg = "Binary"
    		Case Else: Msg = "Таких файлов не бывает"
    	End Select
    	Debug.Print "Открыт для ", Msg
    	
    End Sub

    В этом примере для определения свободного номера файла использовалась функция FreeFile, а для определения длины записи ѕ функция Len. Файл произвольного доступа открыт как для чтения, так и для записи, а бинарный файл только для чтения. Заметьте, что бинарный файл закрыт для чтения другими процессами. Поэтому если повторно запустить процедуру открытия, то файл произвольного доступа будет повторно открыт с другим номером, но возникнет ошибка при повторном открытии бинарного файла. Возможность одновременного открытия под разными номерами нескольких логических экземпляров одного и того же физического файла бывает крайне полезной в ряде ситуаций.

    Функция

    FileAttr(номер-файла, 1)

    по номеру открытого файла возвращает число, указывающее режим открытия: 1 - Input, 2 - Output, 4 - Random, 8 - Append, 32 - Binary. Второй аргумент достался функции FileAttr в наследство от 16-разрядного режима, где при его значении 2, она возвращала системный указатель на файл.

    Мы написали простую процедуру PrintAttr, которая по номеру файла определяет его атрибуты и выводит сообщение в окно отладки. Приведем результаты работы процедуры OpenTwoFiles:

    файл #		 1 
    Открыт для	Random
    Длина Записи:    42 
    файл #		 2 
    Открыт для	Binary

    Закрытие файлов

    Для закрытия файла служит оператор Close, вызываемый:

    Close [список-номеров-файлов]

    Параметр список-номеров-файлов может содержать один или несколько номеров ранее открытых файлов, которые должны быть закрыты. Как обычно, номера в списке разделяются запятыми. Если параметр опущен, то закрываются все открытые в данный момент файлы. После выполнения оператора Close номера закрытых файлов снова можно использовать при открытии в операторе Open.

    Закрыть все открытые оператором Open файлы может также оператор Reset. Как и вызов Close без параметров, он освободит все номера открытых файлов и перенесет содержимое их буферов на диск.

    Запись в файлы последовательного доступа

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

  • Для записи "человеко-ориентированных" данных применяется оператор Print#, представляющий данные в формате, подготовленном для отображения на экране дисплея. По существу, мы уже хорошо знакомы с этим оператором, поскольку ни одна наша программа не обходилась без вывода данных в окно отладки, а применяемый для этих целей метод Print объекта Debug по синтаксису и по действию подобен оператору Print#.
  • Для записи данных, обрабатываемых после их чтения программой, используется оператор Write#. Заметим, в каком - то смысле это более универсальный оператор, чем Print, он, например, не зависит от локализации.
  • Оператор Print# записывает в файл форматированные данные. Его вызов имеет вид:

    Print #номер-файла, [список-вывода]

    Здесь номер-файла - номер файла, открытого в режиме последовательного доступа для записи ( Append или Output ), список-вывода - одно или несколько выражений, разделенных пробелами или точками с запятой, определяющих выводимые данные. Каждое выражение в этом списке может быть снабжено дополнительной информацией о расположении данных, делающее их восприятие более удобным для пользователя. Общий вид такого выражения:

    [{Spc(n) | Tab[(n)]}] [выражение] [симв-поз]
  • Spc(n) задает количество вставляемых пробелов.
  • Альтернативный параметр Tab(n) указывает абсолютный номер n столбца, в котором будет начинаться текст, определяемый параметром выражение. Если не указывать аргумент у Tab, то позицией вставки является следующая зона печати. (Можно полагать, что при выводе экран разделен на зоны, и табуляция позволяет переходить от одной зоны к другой).
  • Параметр выражение задает выражение, значение которого после редактирования и преобразования его в текстовую строку будет записано в файл. На выражения не накладывается особых ограничений, Это могут быть как строковые, так и числовые выражения, они могут содержать данные разных типов. Даты записываются в файл в кратком формате, значения булевых переменных - как ключевые слова Ложь ( False ) или Истина ( True ), пустое значение Empty - как пустое слово, нулевое значение Null выводится как "Null". Данные об ошибке выводятся в виде кода ошибки. Числовые данные выводятся в формате, зависящем от локализации. При выводе можно вызвать для форматирования данных функцию Format.
  • Параметр симв-поз задает положение следующего выражения. Если он равен ";" или пробелу, вставка следующего выражения будет происходить сразу за последним выведенным символом, Tab(n) указывает номер n столбца, начиная с которого будет выводиться выражение. Tab без аргумента или "," - переход в следующую позицию табуляции (зону печати).
  • Создадим теперь открытый ранее файл read.me, записывая в него выражения, позволяющие продемонстрировать все возможности оператора Print:

    Public Sub WritingWithPrint()
    		Dim MyBool As Boolean, MyDate As Date
    		Dim MyNull As Variant, MyFloat As Double, MyErr As Variant
     
    	Print #1, "Первая строка файла"			' запись текстовой строки.
    	'Первая строка файла
    	Print #1, "Зона 1"; Tab; "Зона 2"	' запись в двух позициях табуляции.
    	'Зона 1 Зона 2
    	Print #1, "Зона 1", "Зона 2"		 ' запятая разделяет как табуляции.
    	'Зона 1 Зона 2
    	Print #1, "Привет,"; "старик!"	'; склеивает выражения.
    	'Привет,старик!
    	Print #1, "Привет,"; "старик!"	' пробел действует аналогично.
    	'Привет,старик!
    	Print #1, Spc(5); "5 пробелов "		' вставка 5 пробелов слева.
    	'	 5 пробелов
    	Print #1, Tab(20); "Старик,"; Tab(10); "привет!"	' печать в соответствующих столбцах.
    	'					Старик
    	'		 привет!
    	Print #1, "one"; " "; "two"		'разделение пробелом
    	'one two
    	MyBool = False					 ' булева переменная
    	Print #1, MyBool
    	'False
    	MyDate = #6/14/1999#				' значение даты
    	Print #1, MyDate				 ' печать в кратком формате
    	'14.06.99
    	MyNull = Null						' нулевое значение
    	Print #1, MyNull
    	'Null
    	MyFloat = 3.1416					'вещественное значение
    	' использование функции Format:
    	Print #1, MyFloat, Format(MyFloat, "#.00"), Format(MyFloat, "00.000")
    	'3,1416 3,14 03,142
    	On Error Resume Next
    	Err.Raise 6
    	MyErr = Err
    	Print #1, MyErr
    	MyErr = CVErr(2000)
    	Print #1, MyErr
    End Sub

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

    Public Sub ReadingWithLine()
    	Dim MyStr As String
    	
    	Close #1
    	Open Path  "read.me" For Input As #1
    	Do While Not EOF(1)
    		Line Input #1, MyStr
    		Debug.Print MyStr
    	Loop
    End Sub

    Текст в окне отладки выглядит в полном соответствии с нашими ожиданиями. Вот он:

    Первая строка файла
    Зона 1		Зона 2
    Зона 1		Зона 2
    Привет,старик!
    Привет,старик!
    	 5 пробелов 
    					Старик,
    		 привет!
    one two
    False
    14.06.99 
    Null
     3,1416		3,14			03,142
     6 
    Error 2000

    Обратите внимание, ошибки, возбуждаемые методом Raise и функцией CVErr, редактируются по-разному, в первом случае записывается только код ошибки, во втором ѕ код сопровождается ключевым словом Error, не переводимым в локализованных версиях.

    Заметьте, точно такой же текст можно получить, если в процедуре WritingWithPrint заменить оператор Print на Debug.Print, поскольку этот метод и этот оператор порождают одинаковый результат, разница лишь в том, что один из них помещает результаты в файл, а другой ѕ в окно отладки.

    При выводе данных можно управлять шириной выводимых строк. Ширину выводимых строк для открытого файла устанавливает оператор:

    Width #номер-файла, ширина

    Параметр ширина принимает значения от 0 до 255. Если при печати в файл оператором Print добавление значения очередного выражения в строку приведет к превышению установленной границы, это значение будет печататься с новой строки. При этом значение одного выражения не переносится, даже если его длина превышает установленную границу. Например, если для файла с номером 1 ограничить ширину строки 5 символами:

    Width #1,5

    а затем напечатать строку:

    Str = "раму мылом"
    Print #1, "мама", "мыла", Str

    то в файле окажутся строки:

    мама
    мыла
    раму мылом

    Обращаем внимание и на то, что переход к новой строке может происходить и при встрече с разделителем Tab(n), если n задает позицию меньшую, чем текущая позиция. Пример такой ситуации встречался в нашей процедуре.

    Как было показано, оператор Print вводит в файл данные разных типов, редактируя их в момент ввода и преобразуя в текстовый формат. Попутно он занимается и форматированием текста, приводя его к форме, удобной для отображения на дисплее. Во многих ситуациях форматирование, ориентированное на выдачу на экран дисплея, не является необходимым. В этих случаях для создания последовательного файла применяется Write#. Его синтаксис:

    Write #номер-файла, [список-вывода ]

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

    Также как и оператор Print, оператор Write производит редактирование данных в момент записи. При этом приняты следующие соглашения:

  • В числах в качестве разделителя целой и дробной частей всегда используется точка.
  • В качестве булевых значений используются слова #TRUE# и #FALSE#.
  • При записи дат применяется универсальный формат.
  • Ошибки выводятся в формате #ERROR код-ошибки#.
  • Для пустого значения выражения ( Empty ) ничего не записывается
  • Значение Null записывается как #Null#.
  • В отличие от оператора Print, оператор Write# вставляет при записи запятые между соседними выражениями и заключает строковые значения в кавычки. После записи последнего выражения вставляется перевод на новую строку (Chr(13) + Chr(10)).

    Следующая процедура создает файл с последовательным доступом "readme.txt" и записывает в него данные с помощью оператора Write#. При записи мы старались повторить содержимое предыдущего файла, чтобы можно было сравнить работу, выполняемую операторами Print и Write. Вот текст этой процедуры:

    Public Sub WritingWithWrite()
    	Dim MyStr As String, MyBool As Boolean, MyDate As Date
    	Dim MyNull As Variant, MyFloat As Double, MyErr As Variant
    	
    	'Открытие файла readme.txt
    	Open Path  "readme.txt" For Output As #7
    	
    	'Создание файла
    	Write #7, "Первая строка файла"			' запись текстовой строки.
    	Write #7, "Зона 1", "Зона 2"	' запись двух строк.
    	Write #7, "Привет,", "старик!"	'еще две строки
    	MyStr = "раму мылом. "
    	Write #7, "Мама ", "мыла ", MyStr
    	MyBool = False					 ' булева переменная
    	Write #7, MyBool
    	MyDate = #6/14/1999#				' значение даты
    	Write #7, MyDate					' запись даты
    	MyNull = Null						' нулевое значение
    	Write #7, MyNull
    	MyFloat = 3.1416					'вещественное значение
    	' использование функции Format:
    	Write #7, MyFloat, Format(MyFloat, "0.00"), Format(MyFloat, "00.000")
    	On Error Resume Next
    	Err.Raise 6
    	MyErr = Err
    	Write #7, MyErr
    	MyErr = CVErr(2000)
    	Write #7, MyErr
    
    End Sub

    Результат выполнения этой процедуры - файл "readme.txt" - в текстовом редакторе выглядит так:

    "Первая строка файла"
    "Зона 1","Зона 2"
    "Привет,","старик!"
    "Мама ","мыла ","раму мылом. "
    #FALSE#
    #1999-06-14#
    #NULL#
    3.1416,"3,14","03,142"
    6
    #ERROR 2000#

    Сравните эти строки с теми, что выдаются оператором Print.

    Чтение файлов последовательного доступа

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

  • Input# ѕ данные в файле записаны оператором Write#
  • LineInput# ѕ данные в файле записаны оператором Print#.
  • Оператор Input# вызывается так:

    Input #номер-файла, список-переменных
  • Параметр номер-файла - номер открытого файла,
  • список-переменных - одно или несколько разделенных запятыми имен переменных, в которые будут считываться данные из файла. Переменные могут быть разных типов, естественное требование их тип должен быть согласован с типом записи в момент ее создания. Переменные не должны быть массивами или объектами, но могут быть элементами массивов или полями пользовательских записей.
  • При чтении автоматически выполняются преобразования данных, обратные тем, что использовались при записи оператором Write#. В частности, кавычки вокруг строк, разделяющие запятые и пустые строки, игнорируются. Слово #NULL# дает значение Null, #TRUE# и #FALSE# переводятся в булевы значения True и False, универсальные даты вида #yyyy-mm-dd hh:mm:ss# переводятся в формат соответствующих переменных типа дата/время, #ERROR номер-ошибки# при чтении передает в переменную типа Variant номер ошибки. Если для числовой переменной соответствующие по порядку в файле данные будут не числовыми, ей будет присвоено значение 0.

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

    EOF(номер-файла)

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

    Приведем процедуру чтения записей ранее созданного файла "readme.txt". Заметьте, мы поступаем корректно, читаемые переменные имеют тип, согласованный с типом читаемой записи.

    Public Sub ReadingWithInput()
    	Dim MyStr As String, MyBool As Boolean, MyDate As Date
    	Dim MyNull As Variant, MyFloat As Double, MyErr As Variant
    	Dim i As Integer
    	'Открытие файла readme.txt
    	Open Path  "readme.txt" For Input As #7
    	
    	'Чтение файла
    	'Первые 8 строк
    	For i = 1 To 8
    		Input #7, MyStr
    		Debug.Print MyStr
    	Next i
    	
    	'Данные разных типов
    	Input #7, MyBool, MyDate, MyNull, MyFloat
    	Debug.Print MyBool, MyDate, MyNull, MyFloat
    			
    	'читаем отформатированные числа
    	Input #7, MyStr
    	Debug.Print MyStr
    	Input #7, MyStr
    	Debug.Print MyStr
    		
    	'дважды читаем данные типа Error
    	Input #7, MyErr
    	Debug.Print MyErr
    	Input #7, MyErr
    	Debug.Print MyErr
    	
    	'чтение до конца файла
    	Do While Not EOF(7)
    		Input #7, MyStr
    		Debug.Print MyStr
    	Loop
    End Sub

    В результате будет напечатано:

    Первая строка файла
    Зона 1
    Зона 2
    Привет,
    старик!
    Мама 
    мыла 
    раму мылом. 
    False	 14.06.99   Null   3,1416 
    3,14
    03,142
     6 
    Error 2000

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

    Для построчного ввода данных из файлов с последовательным доступом, созданных оператором Print, вызывается оператор Line Input#:

    Line Input #номер-файла, переменная

    Здесь номер-файла имеет тот же смысл, что и для оператора Input, а переменная - имя строковой переменной или переменной типа Variant, в которую будет прочитана очередная строка файла. Оператор Line Input# считывает данные посимвольно, пока не обнаружит признак конца строки - символ возврата каретки ( Chr(13) ) или пару "возврат каретки - перевод строки" ( Chr(13) + Chr(10) ). Эти признаки в переменную не записываются. Никаких преобразований данных при построчном вводе не производится. Мы уже приводили пример чтения файла "read.me", созданного оператором Print#.

    Следует заметить, что любой файл может быть открыт для чтения и прочитан оператором Line Input#. Другое дело, что, чаще всего, это не даст желаемого результата. Если, например, прочитать файл "readme.txt" с использованием оператора Line Input#, то будут получены следующие результаты:

    "Первая строка файла"
    "Зона 1","Зона 2"
    "Привет,","старик!"
    "Мама ","мыла ","раму мылом. "
    #FALSE#
    #1999-06-14#
    #NULL#
    3.1416,"3,14","03,142"
    6
    #ERROR 2000#

    Несмотря на успех чтения, результаты не приемлемы, поскольку не выполняется нужного редактирования. Сохраняются кавычки, запятые, ограничители, в одной строке содержатся данные разных типов. Поэтому оператор Line Input # следует применять, как правило, для чтения строк из файлов, записанных с помощью оператора Print#.

    Оператор Line Input # позволяет читать файл строка за строкой. Наряду с этим в языке VBA есть возможность чтения из файла произвольного числа символов или произвольного числа байтов. Для чтения символов Input или Binary файлов используется функция Input, имеющая следующий синтаксис:

    Input(размер, [#]номер-файла)

    где размер - количество считываемых символов, номер-файла - номер открытого файла (последовательного или бинарного). Данные, считываемые этой функцией, обычно должны быть записаны в файл операторами Print# или Put.

    Еще один вариант этой функции:

    InputB(размер, [#]номер-файла)

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

    Ввод-вывод для файлов произвольного доступа и бинарных файлов

    Для последовательных файлов запись данных всегда происходит в конец файла, а чтение после открытия производится с самого начала. Для бинарных файлов и файлов произвольного доступа операция чтения выполняется оператором Get, операция записи ѕ оператором Put. Эти операции позволяют читать и записывать записи в произвольном порядке. Установить нужную позицию записи или чтения можно, вызвав оператор Seek:

    Seek [#]номер-файла, позиция

    Параметр номер-файла - номер открытого в режиме Random или Binary файла; позиция - число от 1 до 2 147 483 647, определяющее место в файле, куда будут помещены данные следующим оператором Put или откуда они будут считаны следующим оператором Get. Для файлов произвольного доступа позиция задает номер записи в файле, для бинарных и последовательных файлов - номер байта (символа). Иногда, в процессе поиска позиции необходимо передвинуться вперед или назад на некоторое число записей относительно текущей позиции. Функция Seek может быть использована для того, чтобы определить текущую позицию:

    Seek(номер-файла)

    Она возвращает число в диапазоне от 1 до 2 147 483 647. Для файлов произвольного доступа - это номер следующей считываемой или записываемой записи, для файлов, открытых в остальных режимах, - позиция байта, в которой будет выполняться следующая операция

    Операторы Put и Get позволяют сами установить позицию, переопределив, тем самым, позицию, установленную оператором Seek. Эти операторы имеют одинаковый, простой и ясный синтаксис:

    Put [#]номер-файла, [номер-записи], переменная
    Get [#]номер-файла, [номер-записи], переменная

    Параметры этих операторов имеют следующий смысл:

  • Параметр номер-файла - номер открытого в режиме Random или Binary файла;
  • номер-записи - числовое выражение, определяющее позицию в файле, куда будут помещены вводимые данные или откуда данные будут прочитаны. Для файлов произвольного доступа - это номер записи в файле, для бинарных файлов - порядковый номер байта в файле. Нумерация записей (байтов) в файле начинается с 1. Если параметр номер-записи опустить, данные будут записаны (прочитаны) в текущую позицию. Уже говорилось, что установить текущую позицию можно оператором Seek. Отметим также, что после выполнения операторов Get и Put текущей становится позиция очередной записи (байта), следующей за только что прочитанной записью (байтом). Заметьте, даже если этот параметр опущен, разделяющая запятая все же остается в операторе вызова.
  • Параметр переменная - имя переменной, значение которой должно быть записано в файл (в которую будет прочитана запись).
  • Вот простой пример записи и чтения в файл произвольного доступа:

    Public Sub Test1()
    	Dim N As Integer
    	Open Path  "test.1" For Random Access Read Write As #1 Len = 2
    	N = 1
    	Put 1, 1, N
    	Get 1, 1, N
    	Debug.Print N, LOF(1)
    	N = 2
    	Put 1, 2, N
    	Get 1, 2, N
    	Debug.Print N, LOF(1)
    	N = 3
    	Put 1, 1, N
    	Get 1, 1, N
    	Debug.Print N, LOF(1)
    	Close #1
    		
    End Sub

    Используемая здесь функция LOF(номер-файла) возвращает длину файла в байтах. Заметьте, будет создан файл из двух записей, поскольку третий по счету оператор Put изменяет значение первой записи, так что перед закрытием функция LOF возвратит значение 4.

    Но давайте рассмотрим чуть более серьезный пример. Рассмотрим работу с файлом, который назовем "Товары.9". Этот файл будет содержать информацию о товарах. Код товара будет служить ключом записи. Зная код товара, можно будет найти запись в файле. На практике, когда формальным ключом записи, как в нашем случае, является ее порядковый номер, всегда задается некоторая функция, которая реальный ключ, возможно заданный несколькими параметрами, преобразует в порядковый номер. В нашем примере, чтобы не вводить таких преобразований, будем полагать, что код товара изменяется в пределах от 1 до N и, следовательно, он может служить формальным ключом записи. Вот как задается пользовательский тип, описывающий товар:

    Type Товар
    	КодТовара As Integer
    	Наименование As String * 10
    	Цена As Integer
    End Type

    Создадим файл, хранящий записи этого типа. Чтобы можно было проследить за деталями, наш файл будет содержать не более 10 записей. Мы используем случайные числа для генерирования записей этого файла. Вот три процедуры, решающие эту задачу:

    Public Sub CreateRec(Rec As Товар)
    	'Создает запись типа Товар
    	Randomize
    	Rec.КодТовара = Int(Rnd * 9 + 1) 'Код от 1 до 9
    	Rec.Наименование = "Товар"  Rec.КодТовара
    	Rec.Цена = Int(Rnd * 99 + 1) * Rec.КодТовара
    	
    End Sub
    
    Public Sub PrintRec(Rec As Товар)
    	'Печать записи о товаре
    	Debug.Print "Код Товара:", Rec.КодТовара, "Цена:", Rec.Цена
    End Sub
    
    Public Sub CreateRandomFile()
    	Dim i As Integer, NewRec As Товар
    	Open Path  "Товары.9" For Random Access Write As #1 Len = Len(NewRec)
    	'Создаем 10 записей
    	For i = 1 To 10
    		Call CreateRec(NewRec)
    		Call PrintRec(NewRec)
    		Put #1, NewRec.КодТовара, NewRec
    		
    	Next i
    	Debug.Print "Файл Товары.9 успешно создан"
    	Debug.Print "Размер файла", LOF(1)
    Close #1
    End Sub

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

    Код Товара:	7			Цена:			294 
    Код Товара:	3			Цена:			141 
    Код Товара:	3			Цена:			84 
    Код Товара:	1			Цена:			18 
    Код Товара:	6			Цена:			246 
    Код Товара:	8			Цена:			472 
    Код Товара:	8			Цена:			376 
    Код Товара:	7			Цена:			483 
    Код Товара:	2			Цена:			168 
    Код Товара:	8			Цена:			224 
    Файл Товары.9 успешно создан
    Размер файла	112

    Прокомментируем эти процедуры и полученные результаты. Первая процедура генерирует запись, используя датчик случайных чисел ѕ функцию Rnd. Ранее в своих примерах мы уже использовали эту функцию и подробно говорили о том, как она работает. Вторая процедура позволяет вывести запись в окно отладки. Основной является процедура CreateRandomFile, в которой открывается файл "Товары.9" как файл с произвольным доступом, открытый для записи. При открытии, что очень важно, мы не забыли указать длину записи. Поскольку файл предназначен для хранения значений переменных типа "Товар", то этот тип и определяет длину записи, ѕ она равна 14 байтов (10 байтов занимает строка постоянной длины и 4 байта требуется для двух полей типа Integer).В процедуре 10 раз генерируется новая запись и 10 раз выполняется оператор Put, производящий запись в файл. Но как показывают результаты отладочной печати, некоторые коды повторяются, поэтому в таких случаях происходит обновление содержимого записи. В данном примере только 6 записей имеют уникальный код, и 4 раза происходило обновление уже созданных записей. Важно обратить внимание на длину созданного файла. Анализируя эту характеристику, можно понять, что файл содержит 8 записей, а не 10 и не 6. Дело в том, что длина файла произвольного доступа определяется записью с максимальным номером. Пусть в файле есть запись с максимальным номером M. И пусть добавляется новая запись с номером N, большим M. В этот момент в файле будут созданы новые записи с номерами от M+1 до N включительно. Запись с номером N будет действительно записана, а остальные получат значения по умолчанию, в соответствии с соглашениями, принятыми в VBA. Так что, если в файл пишется одна единственная запись с ключом 10000, то это означает, что в файле автоматически появляется 10000 записей и таков будет его размер. В нашем примере максимальный код равен 8,ѕ отсюда и размер файла равен 112 = 8*14

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

    Public Sub PrintFile()
     Dim N As Integer, i As Integer, Code As Integer
    	Dim MyRec As Товар
    	'Открыть файл Товаров для обработки
    	Open Path  "Товары.9" For Random Access Read As #1 Len = Len(MyRec)
    
    	N = LOF(1) \ Len(MyRec) 'Число	записей
    	Debug.Print "Число	записей файла-- ", N
    	For i = 1 To N
    		'Установить позицию
    		Seek 1, i
    		'Получить запись с заданным кодом
    		Get #1,, MyRec
    		Call PrintRec(MyRec)
    	Next i
    	Close #1
    End Sub

    Вот на какие моменты следует обратить внимание. Файл открывается теперь для чтения и при открытии корректно указывается длина записи. Для подсчета числа записей файла используется упоминавшаяся функция LOF. Заметьте, мы используем оператор Seek для установки позиции, но это скорее для проформы, поскольку в данной ситуации Seek не изменяет уже установленной позиции. Вот результаты работы этой процедуры:

    Число	записей файла--		8 
    Код Товара:	1			Цена:			18 
    Код Товара:	2			Цена:			168 
    Код Товара:	3			Цена:			84 
    Код Товара:	0			Цена:			0 
    Код Товара:	0			Цена:			0 
    Код Товара:	6			Цена:			246 
    Код Товара:	7			Цена:			483 
    Код Товара:	8			Цена:			224

    Мы уже пояснили, почему в файле оказалось 8 записей и почему часть из них имеет нулевые значения.

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

    Public Sub WorkWithFile()
    	Dim N As Integer, i As Integer, Code As Integer
    	Dim MyRec As Товар
    	Randomize
    	'Открыть файл Товаров для обработки
    	Open Path  "Товары.9" For Random Access Read As #1 Len = Len(MyRec)
    
    	N = Int(Rnd * 9 + 1) 'Число обрабатываемых записей
    	Debug.Print "Число обрабатываемых записей -- ", N
    	For i = 1 To N
    		Code = Int(Rnd * 9 + 1)
    		'Установить позицию
    		Seek 1, Code
    		'Получить запись с заданным кодом
    		Get #1,, MyRec
    		If MyRec.КодТовара = 0 Then
    			Debug.Print "В файле нет записи с кодом:", Code
    		Else: Call PrintRec(MyRec)
    		End If
    	Next i
    	Close #1
    End Sub

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

    Число обрабатываемых записей --   2 
    Код Товара:   8			Цена:   224 
    В файле нет записи с кодом:   5

    Работа с данными переменной длины

    До сих пор мы усиленно подчеркивали, что при работе с файлом произвольного доступа размер записи фиксирован и это предполагает хранение в таких файлов данных, размер которых фиксирован в момент их объявления. На самом деле, это не так и в таких файлах можно хранить и строки переменной длины и данные типа Variant. В этих случаях просто следует быть более осторожным, поскольку из-за сложности правил хранения таких данных повышается возможность ошибки. Вот некоторые правила, согласно которым оператор Put размещает данные в файле, а оператор Get их читает:

  • Данные всегда помещаются в файл, начиная с границ записей, установленных параметром Len при открытии файла. Если при этом длина данных, записываемых в файл, меньше размера записи, заданного параметром Len, оператор Put заполнит остающееся до длины записи место "случайными" данными из буфера файла, а новые данные поместит, начиная со следующей границы. При попытке записать данные, длина которых больше Len, появится сообщение об ошибке. Таким образом, параметр Len задает максимальный размер записи, внутри которого может храниться значение переменного размера.
  • При записи строк переменной длины, переменных типа Variant, динамических массивов VBA помещает туда же дополнительную информацию для их расшифровки.
  • При записи строки переменной длины Put помещает непосредственно перед ней 2 байта с ее длиной.
  • Два дополнительных байта также помещаются в файл при записи переменной типа Variant. Их содержимое идентифицирует тип значения (как в функции VarType ). Например, если оно равно 3, это соответствует типу Long, и запись такой переменной займет 6 байтов (2 - для типа и 4 - для самого числа). Значение VarType = 8 соответствует строковому типу ( String ). В этом случае Put запишет 2 байта с VarType, затем 2 байта с длиной строки и затем саму строку.
  • В случае динамического массива Put записывает перед его элементами описатель с длиной 2 + 8* (число измерений массива).
  • Массивы фиксированного размера записываются без описателей. Без всякой дополнительной информации записываются и значения остальных типов переменных с фиксированными размерами.
  • Для бинарных файлов параметр Len при записи роли не играет. Put записывает значения переменных подряд без пропусков и не помещает длины динамических строк и дескрипторы массивов.
  • Для переменных с нефиксированными размерами Get выполняет преобразования, обратные тем, что при записи выполнял оператор Put. Например, при чтении в строку переменной длины Get определяет по 2-байтовому описателю длину строки, а затем читает ее содержимое в переменную. Аналогичная расшифровка происходит и при чтении в переменные типа Variant и в динамические массивы.
  • Для бинарных файлов Get читает данные подряд. В переменные фиксированного размера считывается соответствующее их размеру число байтов. При чтении в строку переменной длины в нее считывается число символов, равное длине ее текущего значения.
  • Приведем пример работы с файлом произвольного доступа, записи которого содержат данные не фиксированного размера:

    Public Sub Test2()
    	'Эта процедура работает с файлом, записи которого
    	'содержат строки переменной длины, не превосхоящей максимума,
    	'а также данные типа Variant
    	
    	Dim Fam As String
    	Dim Other As Variant
    	'Открытие файла произвольного доступа для чтения и записи
    	Open Path  "Strings.var" For Random Access Read Write As #5 Len = 20
    	
    	Fam = "Степанов"
    	Put 5, 1, Fam
    	Get 5, 1, Fam
    	Debug.Print Fam, LOF(5)
    	Fam = "Архангельский"
    	Put 5, 2, Fam
    	Get 5, 2, Fam
    	Debug.Print Fam, LOF(5)
    	Fam = "Куц"
    	Put 5, 1, Fam
    	Get 5, 1, Fam
    	Debug.Print Fam, LOF(5)
    	
    	'Запись типа Variant
    	Other = "Петров"
    	Put 5, 3, Other
    	Get 5, 3, Other
    	Debug.Print Other, LOF(5)
    	Other = 125.25
    	Put 5, 4, Other
    	Get 5, 4, Other
    	Debug.Print Other, LOF(5)
    	Other = 125
    	Put 5, 5, Other
    	Get 5, 5, Other
    	Debug.Print Other, LOF(5)
    
    	Close #5
    End Sub

    Вот результаты ее работы:

    Степанов		10 
    Архангельский	        35 
    Куц			35 
    Петров		        50 
    125,25		        70 
    125			84

    Приведем комментарии:

  • Операторы Put и Get позволяют записать и корректно прочитать данные переменного размера, не превосходящего максимальный размер, заданный при открытии файла.
  • При записи первой строки переменной длины размер файла стал равным 10 байтам, из которых 8 байтов занимает сама строка, а два байта, предшествующий ей описатель. Описатель позволяет оператору Get корректно прочитать эту строку.
  • Запись второй строки начинается с 21-го байта, границы первой записи, общий размер файла становится равным 35 с учетом размера второй строки и ее описателя.
  • Третья запись не изменяет размера файла, поскольку происходит изменение значения первой записи, так что файл по-прежнему содержит только две записи.
  • Далее начинается запись данных типа Variant. Теперь при записи строки используются два описателя, занимающие 4 байта.
  • Две последние записи содержат числовые данные Double (8 байтов) и Integer (2 байта) с их описателями.
  • Заканчивая тему работы с файлами произвольного доступа, хотим еще раз обратить внимание на некоторые опасные моменты, возникающие в процессе работы с ними и не контролируемые VBA:

  • Явно задавайте при открытии файла, как при чтении, так и при записи параметр Len ѕ максимальный размер записи. Если этого не сделать, то программа будет работать, но, к сожалению, результаты могут быть не предсказуемыми.
  • Будьте особенно внимательными при работе с данными переменной длины, помните, что к данным автоматически присоединяются описатели.
  • Размер файла определяется записью с максимальным номером. Чтобы избежать большого числа "дырок" в файле стройте разумные функции ключа так, чтобы ключи записей занимали достаточно плотный начальный интервал от 1 до N.
  • Один пример работы с Binary файлом

    Бинарные файлы пишутся и читаются порциями, состоящими из произвольного числа байтов. Доступ возможен к любому байту этого файла в произвольном порядке. Никакого редактирования при операциях с этим файлом не производится и вся ответственность за сохранением структуры информации ложится на программиста. Операции над этим файлом выполняются операторами Get и Put, но можно читать этот файл побайтно, используя и уже упоминавшуюся функцию Input. Мы не будем строить специальный пример для работы с этим файлом, а ограничимся примером чтения ранее созданного текстового файла "readme.txt", который откроем, как Binary файл. В этом примере мы прочитаем последовательно этот файл порциями по 20 байтов. Заодно покажем, как организуется чтение "хвоста" файла ѕ его последней порции. Возможно, с содержательной точки зрения пример не очень хорош, поскольку будет нарушена реальная структура читаемых данных, нов этом то и вся суть дела при работе с такими файлами. Программист сам должен восстановить структуру данных, чем мы, в данном случае, заниматься не будем. Но вот сам пример:

    Public Sub Test3()
    	'Чтение Binary файла
    	Dim MyLocation As Long, MyLoc As Long, MyLine As String
    	
    	Open Path  "readme.txt" For Binary As #1
    	
    	MyLocation = 0
    	Do While MyLocation + 20 < LOF(1)	' Читаем допустимую порцию
    		MyLine = Input(20, #1)
    		MyLocation = Loc(1): MyLoc = Seek(1)	' 2 способа определения текущей позиции
    		Debug.Print MyLine; Tab; MyLocation; Tab; MyLoc
    	Loop
    	'Читаем последнюю порцию
    	MyLoc = LOF(1) - MyLocation
    	If MyLoc > 0 Then
    		MyLine = Input(MyLoc, #1)
    		MyLocation = Loc(1): MyLoc = Seek(1)
    		Debug.Print MyLine, MyLocation, MyLoc, LOF(1)
    	End If
    	Close #1
    End Sub

    Перед комментариями приведем результаты работы:

    "Первая строка файла	        20			21 
    "
    "Зона 1","Зона 2"		40			41 
    
    "Привет,","старик!		60			61 
    "
    "Мама ","мыла ","		80			81 
    раму мылом. "
    #FALS		               100			101 
    E#
    #1999-06-14#
    #N		               120			121 
    ULL#
    3.1416,"3,14",		       140			141 
    "03,142"
    6
    #ERROR                         160			161 
    2000#
      167		               168			167

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

    И последний совет в этой лекции, средств VBA для работы с файлами произвольного доступа достаточно для выполнения основных операций: создания, записи, чтения и модификации данных. Но средства поиска данных в файле (оператор Seek ) весьма примитивны. С их помощью трудно решать задачи упорядочения файлов, поиска записей по значениям отдельных полей и другие задачи, характерные для работы с базами данных. Для небольших файлов можно создавать индексные массивы, в которых номера записей упорядочены в соответствии с требуемым критерием, и поддерживать этот порядок при всех изменениях файла. Для больших - заводить свои индексные файлы и т. п. Но владельцам Office 2000 не следует заниматься программированием собственных СУБД. В их распоряжении - возможность переписать содержимое любого файла произвольного доступа в ячейки рабочего листа Excel или в базу данных (таблицу) Access и использовать для работы с его данными всю мощь этих инструментов.

    Вернуться к учебному плану