Книга
Книга
Кен Гетц, Майк Джилберт
UpoipDiHjKUMiHiieв НOfc
•Работа со строкамиРуководствопо
Ж^Г^^tWVisual Basic foi
•Операции с числамиApplications
•Поиск и сортировка
а1Ьй8сЙ№"с^
( Для пользователя )
К. Гетц, М. Джилберт
Перевод с английского
VBA Developer's Handbook™
Ken GetzMike Gilbert
San Francisco*Paris*Diisseldorf*Soest
УДК 681.3.06
К. Гетц, М. Джилберт
Программирование в Microsoft Office. Для пользователя: пер. с англ. — К.: Издательская группа BHV, 2000. — 384 с.
ISBN 966-552-058-Х
ISBN 5-7315-0087-8
Книга написана специалистами, которые имеют большой опыт работы с Visual Basic for Applications. С ее помощью вы найдете решения задач, связанных с обработкой строк, числовых данных и дат, а также с работой в сети. Подробно рассмотрены такие важные вопросы, как обработка ошибок и операции с файлами. Отдельная глава посвящена созданию мультимедийных приложений.
Обложка А.А. Стеценко
© SYBEX, Г© SYBEX, ГISBN 5-7315-0087-8ISBN 966-552-058-ХISBN 5-7315-0087-8ISBN 966-552-058-Х© Издательская п© Издательская плев, 2000лев, 2000ISBN 0-7821-1951-4
© SYBEX, Г
© SYBEX, Г
ISBN 5-7315-0087-8
ISBN 966-552-058-Х
ISBN 5-7315-0087-8
ISBN 966-552-058-Х
© Издательская п
© Издательская п
лев, 2000
лев, 2000
Введение
Язык программирования Visual Basic for Application (VBA) начал свою жизнь как средство, которое позволило сначала Excel, а затем и другим приложениям Microsoft Office программно управлять их собственной средой и которое взаимодействовало с другими приложениями, используя механизм автоматизации OLE. В 1996 году, когда фирма Microsoft разрешила сторонним производителям лицензировать ядро и среду языка VBA для их собственных продуктов, мир VBA взорвался. На момент написания книги свыше 40 производителей лицензировали эту захватывающую технологию, позволив пользователям многих продуктов управлять их приложениями и любым сервером автоматизации, используя VBA.
Для разработчика на языке VBA наиболее ценным является то, что навыки, которые он приобретает при работе с одним продуктом, пригодятся при работе с другим поддерживающим VBA продуктом. Среда программирования та же, средства отладки те же и язык тот же. Наконец, программистов на языке Basic (все же VBA — разновидность первоначального Basic) начинают уважать: используя средства, понятные конечным пользователям, вы можете писать приложения, с которыми они могут работать, а также изменять и расширять их.
О книге
Язык VBA стал клеем, позволяющим соединить различные фрагменты многоплат- формных решений, и многие начинающие программисты попадают в ситуации, когда им нужна помощь в программировании. В данной книге вы найдете творческие решения многих программистских задач, включая (но, конечно, не ограничиваясь этим): ■ принципы работы со строками (глава 1);
обработку значений даты и времени (глава 2);
определение наибольшего общего делителя двух чисел (глава 3);
сортировку массива (глава 4);
перехват и обработку ошибок, возникающих при работе приложений (глава 5);
создание нового сетевого ресурса совместного доступа (глава 6);
■ операции с дисками и файлами (глава 7);
проигрывание видеоизображения в рамках приложения (глава 8).
Заменяет ли книга справочное руководство по программированию на VBA? И близко нет! Да она и не предназначена для того, чтобы обеспечить вас полной справочной информацией. В ней представлены идеи, решения и накопленный опыт программирования на языке Basic, на основании которого мы сформировали список тем, по нашему мнению, представляющих интерес и являющихся своего рода вызовом для многих разработчиков.
Прежде всего, эта книга не привязана к какому-либо определенному программному продукту. И вы сможете воспользоваться программами из нее независимо от того, с чем вы работаете: с Microsoft Office 97 или с любым другим программным продуктом, поддерживающим VBA версии 5.0 или более поздней. Мы подчеркиваем еще раз: несмотря на то, что при написании этой книги мы применяли Microsoft Office 97, все приведенные здесь программы должны работать в любом программном продукте, поддерживающем VBA.
По этим причинам мы сосредоточили свое внимание на программах, а не на интерфейсе пользователя, так что в книге вы найдете очень мало примеров, в которых ’’есть на что посмотреть”. Большей частью примеры включают вызов программы из окна Проверка (Immediate). Не рассчитывайте на уйму красивых примеров с формами, которыми вы сможете воспользоваться, — это не было нашей целью. Мы представили полезные программы с объяснениями, чтобы вы могли модифицировать и расширять их, если вам понадобится добавить новые функциональные возможности. Кроме того, рассмотрели множество тем, но даже не пытались сказать последнее слово по той или иной из них. Это потребовало бы увеличить объем книги в 10 раз. Наша книга является лишь отправной точкой на пути исследования новых тем.
Нужна ли вам эта книга?
Данная книга предназначена как для разработчиков-новичков, так и для программистов, которые имеют опыт работы с VBA и нуждаются в помощи в определенных, возникающих при написании программ, ситуациях.
Книга предназначена для пользователей трех групп:
Начинающие и пользователи среднего уровня
Вы написали несколько процедур и пытаетесь создать приложение. Вам нужна помощь при написании определенных процедур, и самостоятельное постижение программы пока еще непреодолимая задача. Вы можете позаимствовать программы из этой книги, опустив скучную часть описания принципов работы программ, и вернуться к работе над своим приложением.
Опытные пользователи
Вы написали множество программ и решали более или менее серьезные задачи при программировании. Вам нужны определенные процедуры, и вы могли бы сами их написать, но существуют другие неотложные дела (например, необходимость закончить приложение вчера). Вы можете позаимствовать программы из этой книги, изменить их в соответствии со своими потребностями и проработать приведенные здесь объяснения, чтобы расширить ваши знания о VBA.
Эксперты
Даже если вы один из наиболее опытных программистов на языке VBA, существуют кое-какие процедуры, которые вы как раз не написали самостоятельно, но они вам нужны. Вы можете использовать представленные здесь программы в качестве отправной точки, украшая и настраивая их по своему желанию. Конечно, вы в состоянии найти более совершенный способ написания приведенных программ, и если это так, мы с удовольствием почитали бы ваши сообщения!
Если вы относитесь к одной из этих трех категорий пользователей, у нас есть программы для вас!
Что вы должны знать
Чтобы втиснуть в книгу как можно больше программ, мы вынуждены обойтись без материала для начинающих. Если же вы, работая в VBA, не знаете, где поместить программу, как создать модуль, или даже то, что представляет собой определенный тип данных, возможно, вам следовало бы отложить эту книгу в сторону на неделю-другую и изучить справочные материалы, поставляемые с базовым приложением
VBA, с которым вы работаете. Прежде чем окунуться в эту книгу, убедитесь, что вы хорошо разбираетесь в следующих темах:
создание модулей;
создание процедур;
переменные и их типы;
синтаксис VBA (включая If...Then, For...Next и другие управляющие структуры).
Если вы потратите время на повторение этих вопросов перед тем, как читать нашу книгу, вы вынесете гораздо больше пользы из приведенного в ней материала.
Принятые в книге соглашения
Работая над рядом проектов, мы пришли к выводу, что единый стиль программирования и определенные соглашения значительно упрощают совместную работу группы программистов над проектом. Чтобы облегчить вам понимание наших программ (а себе — понимание друг друга), мы позаимствовали стандарт на наименования, которого придерживаемся и в этой книге, и в нашей профессиональной работе.
Мы использовали версию 4.0 соглашений Реддика о наименовании объектов VBA (RVBA), которые приняты многими разработчиками приложений на основе Access и VBA в качестве стандарта на наименования. Грег Реддик — известный разработчик и инструктор по VBA и Access. Даже если вы не придерживаетесь RVBA-стандарта, вы, вероятно, оцените тот факт, что он последовательно используется во всей книге.
Кроме соглашений о наименовании мы стремились унифицировать другие обозначения. Так, общие функции, процедуры и пользовательские типы, которые вы, возможно, пожелаете использовать в своих программах, обозначены префиксом dh (от Developer's Handbook), все общие описания функций Windows API — префиксом dh_api, а все глобальные константы — префиксом dhc. Эти соглашения позволяют избежать противоречий в именах с любой существующей в вашем приложении программой. Однако если вы импортируете несколько модулей из примеров разных глав в одно приложение, то могут возникнуть конфликты между именами вследствие использования нами согласованной системы имен по всем главам. В этом случае вам придется ’’закомментировать” конфликтующие API-описания или определенные пользователем типы.
Замечание относительно обработки ошибок: при написании вспомогательных процедур, подобных приведенным в этой книге, всегда возникает вопрос, включать ли пространную обработку ошибок. Мы решили, как ради простоты, так и из-за любви к служебным программам, отображающим сообщения об ошибках, включить незначительную обработку ошибок, исключив случаи, когда процедуры требуют ее для собственных нужд. Это означает, что вашей программе, вызывающей наши процедуры, потребуется перехватывать и обрабатывать ошибки, которые всплывут из приведенных здесь программ. Конечно, если пожелаете, вы можете добавить собственную обработку ошибок в импортируемые процедуры. (Дополнительную информацию об обработке ошибок смотрите в главе 5.)
О содержании книги
Первые четыре главы начинаются с обсуждения встроенных функций VBA, после чего рассматривается их применение в пользовательских VBA-процедурах.
В первой главе описаны принципы работы со строками, включая их синтаксический анализ.
Во второй главе освещены вопросы, касающиеся работы со значениями даты и времени.
Третья глава содержит описание числовых типов данных, методов преобразования данных и других операций с числами.
В четвертой главе вы ознакомитесь со структурой массивов и прочтете о поиске и сортировке.
В пятой главе внимание уделено написанию устойчивых, хорошо работающих VBA-приложений, включающих обработку ошибок, создание протоколов событий и стека вызова процедур для ваших приложений.
В шестой главе находится информация по работе с сетью: создание ресурсов совместного использования, выбор устройств и взаимодействие с пользователями.
В седьмой главе вы прочтете описание функций API, работающих с жесткими дисками и дисководами, а также ряда функций, позволяющих работать с файловой системой вашего компьютера.
Завершает книгу восьмая глава. В ней рассматриваются вопросы, связанные с введением в приложения возможностей мультимедиа (звука, видеофайлов и растровых изображений).
ГЛАВАГЛАВА
ГЛАВА
ГЛАВА
Работа со строками
Как VBA хранит строки?
Встроенные функции для работы со строками
Поиск и замена текста
Сбор информации о строках
Преобразование строк
Работа с подстроками
Практически в любом приложении VBA рано или поздно возникает необходимость работать со строковыми (текстовыми) данными. VBA имеет собственный набор строковых функций, однако возможности этих функций в целом значительно уступают возможностям, которые предоставляют другие языки программирования, ориентированные на работу с текстом в большей степени. В начале главы приведен краткий обзор имеющихся функций, а затем описано несколько полезных процедур для работы со строками, которые можно добавить в свой ’’сундучок” маленьких хитростей. Конечно, для описания всех функций по данной теме не хватит даже самой обширной главы, однако предлагаемый вашему вниманию набор функций послужит хорошей отправной точкой для написания собственных VBA-программ.
Как VBA хранит строки?
Строка в VBA — это набор байтов. Чтобы VBA было проще работать со строками, каждая из них содержит информацию о своей длине. Кроме того, в отличие от других языков программирования, VBA обеспечивает создание, уничтожение и изменение размеров областей памяти, отведенных для хранения строк. Вам нет необходимости беспокоиться о том, каким образом хранятся строковые данные в памяти, как VBA ищет строки, а также о том, как и когда VBA обновляет память, занимаемую строкой.
VBA может работать со строками двух типов: фиксированной и переменной длины. Строки фиксированной длины — это строки, размер которых определен, например:
Dim strFixed As String * 100
В этом случае строка strFixed будет всегда содержать ровно 100 символов, независимо от того, сколько символов в ней хранится (т.е. при определении длины данной строки результат всегда будет 100):
Debug.Print Len(strFixed)
VBA заполняет свободные позиции в строке пробелами, и чтобы использовать строку в других выражениях, потребуется функция Trim. Строки фиксированной длины не могут содержать более 65526 символов.
Строки переменной длины не имеют строго определенного размера. При добавлении (удалении) символов в эти строки VBA заботится о выделении (освобождении) памяти для размещения текста и делает это по мере необходимости. Строки переменной длины можно объявить следующим образом:
Dim strDynamic As String
В этом случае при определении длины строки результат будет в точности соответствовать количеству символов, которые хранятся в данной переменной. Строки переменной длины могут содержать около 2 миллиардов символов.
Как решить, строку какого типа использовать? Строки переменной длины требуют немного больше ресурсов VBA для их обработки и, следовательно, работа с ними происходит несколько медленнее. С другой стороны, потраченное время компенсируется тем, что при работе со строкой вам не придется всякий раз вызывать функцию Trim для удаления лишних пробелов. Из приведенных ниже примеров вы увидите, что строки фиксированной длины применяются только в случаях крайней необходимости.
Например, при работе с единственным символом имеет смысл использовать строку фиксированной длины, объявив ее равной единице, так как известно, что в строке всегда будет только один символ и не придется удалять лишние пробелы. Это явное преимущество применения строки фиксированной длины без дополнительных затрат.
Unicode против ANSI
Среда Windows поддерживает два стандарта хранения символов: ANSI и Unicode. В стандарте ANSI для хранения каждого символа используется один байт. При этом в любом наборе символов ANSI допускается всего 256 символов. В Windows 95 данный стандарт нужен для обратной совместимости с предыдущими версиями Windows. В стандарте Unicode можно закодировать 65536 символов, каждый из которых занимает два байта. Набор символов Unicode объединяет практически все известные письменные символы и идеограммы в одном множестве. Таким образом, приложение, использующее стандарт Unicode, может поддерживать практически любой язык (конечно, если текст приложения переведен на этот язык). Windows NT поддерживает стандарт Unicode.
Независимо от того, с какой операционной системой вы работаете, VBA хранит строки в формате Unicode (т.е. каждый символ занимает два байта памяти). Когда VBA нужно обменяться информацией с Windows 95 (например, при использовании в вашей программе функций Windows API), он сначала должен преобразовать строки в формат ANSI. Это происходит автоматически при вызове ANSI-версий функций Windows API, обрабатывающих строки. И только в одном случае придется побеспокоиться о том, как VBA хранит строки, — при преобразовании строки в массив байтов (полезный метод, используемый нами здесь несколько раз). В этом случае строка длиной пять символов преобразуется в массив из десяти байтов. Например, строка, содержащая текст "Hello”, после преобразования в байтовый массив будет включать следующие десять байтов:
72 0 101 0 108 0 108 0 111 0
Каждая пара байтов (например, 72 и 0 для буквы Н) показывает, как кодируется отдельный символ в формате Unicode. Однако если при работе с Microsoft Excel, например в Корее, набирать текст на корейском языке, второй байт не будет равен 0. Он примет значение, которое в сочетании с первым байтом кодирует набранный символ.
Использование строк и байтовых массивов
Поскольку обрабатывать массивы байтов зачастую быстрее и проще, чем отдельные символы (в настоящей главе этот прием применяется в нескольких примерах), VBA обеспечивает простой способ преобразования строк в байтовые массивы и наоборот. Обычное присваивание строки переменной байтового массива заставляет VBA копировать данные в массив. После обработки массив можно снова присвоить строковой переменной. Например, следующий фрагмент программы копирует данные из строки в массив байтов, обрабатывает массив, а затем копирует данные из массива в строку:
Sub StringToByteArray()
Dim strText As String
Dim aByt() As Byte
Dim inti As Integer strText = "Hello” ' VBA позволяет присвоить строку байтовому массиву ' и наоборот. aByt() = strText For inti = LBound(aByt) To UBound(aByt)
Debug.Print aByt(intl);
Next inti
Debug.Print strText = aByt() Debug.Print strText End Sub
Хотя вам не придется часто пользоваться подобным методом, при необходимости обработать каждый байт строки — это лучшее решение.
Встроенные функции для работы со строками
VBA предоставляет широкий набор функций для работы со строками. В этом разделе приведены многие из них и классифицированы по функциональному назначению. Здесь также обсуждаются принципы применения наиболее полезных встроенных функций. Далее в главе описываются методы, позволяющие посредством комбинаций встроенных функций решать задачи, для которых пришлось бы писать специальные программы.
Сравнение строк
VBA предоставляет три средства для сравнения содержимого строк: операторы сравнения (такие как =, < и т.д.), оператор Like и функцию strComp. Кроме того, можно задать метод сравнения для каждого модуля, используя оператор Option Compare в разделе описаний.
Оператор Option Compare
Оператор Option Compare должен стоять в каждом модуле перед всеми процедурами и указывать VBA, как сравнивать строки в пределах данного модуля. Возможны следующие варианты этого оператора: ■ Option Compare Binary
Сравнение выполняется на основе порядка сортировки символов, определяемого их внутренним двоичным представлением. В таком случае символы интерпретируются с учетом регистра (т.е. символы А и а — это не одно и то же).
Option Compare Text
Сравнение производится на основе порядка сортировки текстов в текущей национальной настройке. Символы, по крайней мере символы английского языка, сравниваются независимо от регистра.
Option Compare Databas
Сравнение выполняется на основе порядка сортировки, определяемого национальной настройкой текущей базы данных.
Совет: Если в Option Compare не указан способ сравнения, VBA по умолчанию использует Option Compare Binary. В этом случае в результате сравнения символов Айа возвращается значение False. Поэтому при сравнении строк убедитесь, что в Option Compare для данного модуля установлен нужный способ сортировки.
Операторы сравнения
Для сравнения двух строк можно использовать обычные логические операторы, например:
If strTextl < strText2 Then ...
В этом случае VBA выполняет посимвольное сравнение в соответствии со способом сортировки, который задан в операторе Option Compare и действует в данном модуле. При изменении способа сортировки в Option Compare результат сравнения, скорее всего, будет другим. Можно использовать набор простых операторов сравнения, представленных в следующей таблице.
Оператор Описание
Меньше
Меньше или равно
Больше
Больше или равно
Равно
Не равно
Кроме Option Compare для сравнения двух строк в VBA имеется оператор Like. В строке-шаблоне этого оператора можно использовать подстановочные символы, списки и интервалы символов, а не только фиксированные символы. В следующей таблице приведены все возможные параметры строки-шаблона, формируемой с помощью оператора Like.
Символы в строке-шаблоне
Совпадающие символы в строке
7
Любой одиночный символ
*
Любое количество символов (от нуля и больше)
#
Любая одиночная цифра (0-9)
^список символов]
[!список символов]
Любой одиночный символ в списке символов
Любой одиночный символ, не входящий в список символов
Совет: Строка, содержащая подстановочные символы, должна находиться справа от оператора Like. Таким образом, в отличие от многих математических операторов, этот оператор не является коммутативным: порядок операндов существенен.
Например, следующий фрагмент программы сравнивает строку с шаблоном, отыскивая в ней почтовые коды Канады:
strTemp = ”W1F 8G7"
If strTemp Like ”[A-Z]#[A-Z] #[A-Z]#" Then
* Вы знаете, что переменная strTemp содержит
’ допустимое значение почтового кода Канады.
End If
Для того чтобы проверить, является ли единственная буква в strTemp гласной, можно воспользоваться следующим выражением:
If strTemp Like "[AEIOUaeiou]" Then
* Вы знаете, что буква в strTemp является гласной.
End If
Чтобы убедиться в том, что слово в strTemp не начинается с гласной буквы, можно использовать выражение вида:
If strTemp Like "[!AEIOUaeiou]*" Then
’ Вы знаете, что слово в strTemp не начинается с гласной буквы.
End If
Оператор Like неоценим при контроле правильности ввода. Вместо того чтобы анализировать строку самостоятельно, можно воспользоваться подстановочными символами, которые позволят задавать различные сочетания символов.
Примечание: Поведение оператора Like зависит от способа сравнения, указанного в Option Compare. По умолчанию в каждом модуле используется Option Compare Binary (сравнение с учетом регистра символов).
Совет: Применяя оператор Like, необходимо принимать во внимание способ сортировки, порядок символов в пределах интервала и т.д. Более подробная информация по этим вопросам приводится в соответствующем разделе справки VBA.
Функция StrComp
Функция StrComp позволяет игнорировать способ сортировки, установленный в Option Compare для данного модуля. При использовании функции StrComp укажите две строки и метод сравнения (binary, text или database). Функция возвращает значение, отражающее результат сравнения двух строк. В общем случае вызов функции StrComp выглядит следующим образом:
intRetVal = StrComp(strTextl, strText2, intCompareOption)
В качестве двух строк текста могут применяться любые строковые выражения, а intCompareOption может принимать одно из значений, приведенных в табл. 1.1, или быть целым числом, которое определяется национальной настройкой и устанавливает порядок сортировки при выполнении операций сравнения. В зависимости от параметров, функция StrComp возвращает одно из значений табл. 1.2.
Совет: Параметр intCompareOption функции StrComp не является обязательным и, если он пропущен, VBA использует опцию, установленную в операторе Option Compare для всего модуля. Когда пропущена опция Option Compare, применяется способ двоичного сравнения (vbBinaryCompare).
Таблица 1.1. Параметры функции StrComp
Константа
Эквивалентный оператор Option Compare
VbBinaryCompare
Option Compare
Binary*
vbDatabaseCompare
Option Compare
Database
vbTextCompare
Option Compare
Text
Таблица 1.2. Значения, возвращаемые StrComp
Если...
StrComp возвращает
strTextl меньше чем strText2
-1
strTextl равен strText2
0
strTextl больше чем strText2
1
Используя функцию StrComp, даже если вы обычно выполняете сравнение с учетом регистра символов, можно проигнорировать это требование для одной операции:
If StrComp(strTextl, strText2, vbTextCompare) = 0 Then
’ Вы знаете, что строки strTextl и strText2 одинаковы.
End If
Преобразование строк
В VBA имеется одна функция для преобразования строк strConv, которая заменяет несколько отдельных функций преобразования из одного формата в другой. Эта функция позволяет задать как строку, так и параметр, определяющий вид преобразования. Вызов функции выглядит следующим образом:
strOutput = StrConv(strinput, intConversion)
Здесь strinput - строка, которую требуется преобразовать, a intConversion - одно из значений следующей таблицы. Возвращает функция StrConv преобразованную строку.
Константа
Описание
vbUpperCase
vbLowerCase
Преобразует все символы строки в прописные
Преобразует все символы строки в строчные
vbProperCase
vbUnicode
Преобразует первую букву каждого слова строки в прописную Преобразует строку в формат Unicode, используя кодовую страницу системы, установленную по умолчанию
vbFromUnicode
Преобразует строку из формата Unicode в кодовую страницу системы, установленную по умолчанию
Функция strConv выполняет две основные задачи: преобразует регистр символов строки и строки из ANSI в Unicode и наоборот.
Формирование строк: функции Space и String
В VBA есть две функции, которые облегчают создание особых строк. Функция Space позволяет сформировать строку, состоящую только из пробелов. Достаточно указать необходимое число пробелов, и VBA выполнит остальные действия автоматически. Синтаксис этой функции выглядит следующим образом:
strOut = Space (intSpaces)
Хотя функция имеет множество различных применений, чаще всего она используется в двух случаях:
для создания строковых буферов при вызове внешних DLL-модулей (в частности, Windows API);
для заполнения строк пробелами, чтобы выровнять их вправо или влево в буфере определенного размера.
Для создания строки из десяти пробелов можно воспользоваться таким выражением:
strTemp = Space (10)
Если требуется большая гибкость, можно применить функцию string для создания строки, состоящей из одинаковых символов. В этом случае указываются количество повторов символа и сам символ или его ANSI-код:
strOut = String (intChars, strCharToRepeat)
или
strOut = String(intChars, intCharToRepeat)
Например, любой из приведенных ниже фрагментов формирует строку, в которой десять раз повторяется буква а. (ANSI-код буквы а — 97.)
strOut = String(10, "а")
strOut = StringdO, 97)
Такая строка вряд ли когда-нибудь понадобится. В следующем фрагменте программы приведен пример формирования строки, которая состоит из одной буквы А, двух букв в, трех букв с и т.д.:
Dim inti As Integer
Dim strOut As String
For inti = 1 To 26
strOut = strOut & String(inti, Asc(”A”) + inti - 1)
Next inti
Вычисление длины строки
Простая, однако очень важная функция Len позволяет определять длину любой строки или строкового выражения. Чтобы использовать эту функцию, необходимо передать ей строку или строковое выражение:
IngCharCount = Len(strin)
Несомненно, вам часто придется определять длину строкового выражения. Но функция Len имеет еще одно преимущество: она выполняется очень быстро! VBA хранит строки так, что запись каждой строки предваряется целым числом типа Long, которое содержит информацию о ее длине. VBA может очень просто найти эти сведения при работе программы. Предположим, нужно проверить, что определенная строка в настоящий момент не содержит ни одного символа. Для этого многие программисты пишут программы, подобные следующей:
If strTemp = ”" Then
* Вы знаете, что строка strTemp пуста.
End If
Поскольку VBA может очень быстро узнать длину строки, то, чтобы определить, что строка пуста, лучше использовать следующий код:
If Len(strTemp) = 0 Then
* Вы знаете, что строка strTemp пуста.
End If
Если медленная операция сравнения выполняется один раз, это существенно не скажется на быстродействии вашей программы, однако когда проверка строк осуществляется достаточно часто, стоит подумать об использовании функции Len.
Форматирование строк
VBA позволяет форматировать строки, используя символы-заполнители, которые задают отдельные символы форматируемой строки. Для преобразования символов исходной строки в прописные или строчные можно применять функцию Format. Символы-заполнители и символы преобразования, представленные в табл. 1.3, служат для форматирования исходной строки.
Таблица 1.3. Символы-заполнители и символы преобразования для функции Format
Символ Описание
@Символ-заполнитель для символа или пробела (если исходная строка содержит
символ в той позиции, которой в строке формата соответствует символ @, то отображается символ исходной строки; в противном случае отображается пробел)
&Символ-заполнитель для символа или "пустоты" (если в исходной строке имеется
символ в том месте, где находится & в строке формата, отображается символ исходной строки; в противном случае не отображается ничего)
<Отображает все символы строчными
>Отображает все символы прописными
!Устанавливает замещение символов-заполнителей реальными символами слева
направо (по умолчанию символы подставляются вместо символов-заполнителей справа налево; символ ! может располагаться в любом месте строки формата)
Например, следующий фрагмент программы возвращает ” ()836-4928”:
strOut = Format("8364928", " (@@@)&&&-&&&&")
А этот фрагмент возвращает строку ” () 836-4928”:
strOut = Format("8364928", "(&&&)&&&-&&&&")
Кроме того, функция Format позволяет форматировать обычные строки одним способом, а пустые, или нуль-строки, — другим. Например, требуется показать пустое значение иначе, чем значение с данными. Используйте для этого две части в строке символов-заполнителей, разделенные точкой с запятой (;). Первая часть будет применяться к непустым строкам, а вторая — к пустым:
strOut = Format(strin, "(@@@)&&&-&&&&;No phone")
Данное выражение возвратит отформатированный номер телефона, если переменная str in содержит непустую строку, и "No phone" (Нет телефона) в случае, когда строка в переменной st г in пуста.
Для того чтобы при форматировании преобразовать символы текста в прописные или строчные, добавьте в строку формата символы > или <. Каждый следующий за этими знаками символ будет преобразован. Например, следующий фрагмент программы преобразует символы исходного текста в прописные и вставляет между ними пробелы:
Format("hello there", ">@ @@@@@@@@@ @")
I liT Совет: Функция Format позволяет пользователю задавать собственные L I у форматы дат и числовых значений. Более подробная информация об этом приведена в главах 2 и 3.
VBA имеет также две простые функции UCase и LCase для преобразования символов строки в прописные или строчные соответственно. В качестве аргумента функции указывается строка, символы которой необходимо преобразовать. В следующем примере переменной strOut присваивается значение "hello":
strOut = UCase("hello")
I Совет: 8 эт°н главе представлено три способа преобразования символов L I у текста в прописные или строчные: функции UCase/LCase, символы > и < в
функции Format и константы vbUpperCase и vbLowerCase в функции s trConv. Используйте тот метод, который является для вас более удобным.
Выравнивание строк
VBA имеет два оператора LSet и RSet (заметьте, что это не функции), которые позволяют выровнять одну строку в пределах области, занимаемой другой строкой. Эти операторы редко используются для таких целей, однако иногда могут пригодиться. Кроме того, оператор LSet обеспечивает большую гибкость при работе с данными, тип которых определен пользователем.
Посредством операторов LSet и RSet можно вставить новый фрагмент текста в начало или в конец существующей строки. Незанятые позиции заполняются пробелами, а текст новой строки, который не помещается в старую строку, обрезается.
Например, после выполнения следующего фрагмента программы строка strOutl будет содержать строку "Hello " (слово "Hello" и три пробела после него), а строка str0ut2 - " Hello" (три пробела, а затем "Hello").
StrOutl = "ABCDEFG" str0ut2 = "ABCDEFG" LSet strOutl = "Hello" RSet strOut2 = "Hello"
Совет: Следует признать, что программисты практически не пользуются операторами LSet и RSet при работе со строками. Это почему-то не при-
нято. Ведь те же результаты можно получить посредством других строковых
функций. Однако использование оператора LSet для данных пользовательского типа является ключевым моментом при передаче данных в переменную другого типа.
Оператор LSet применяется и для других целей: он позволяет перекрывать данные одного пользовательского типа данными другого. Хотя в справочных файлах VBA и указывается, что не рекомендуется поступать подобным образом, этот метод предоставляет значительные возможности. LSet позволяет все данные одной структуры поместить поверх данных другой, не принимая во внимание расположение различных частей структур данных.
Представьте, что вы читаете данные фиксированной длины из текстового файла. Каждый столбец в нем содержит известное число символов. Эти столбцы необходимо передать в определенную пользователем структуру данных, в которой каждому столбцу текстового файла соответствует отдельное поле. Пусть для этого простого примера текстовый файл содержит описанные в следующей таблице столбцы.
Название столбца Ширина
10101010Имя
10
10
10
10
Фамилия
Почтовый индекс 5
Для работы с данными текстового файла создана пользовательская структура данных:
Type TextData
FirstName As String * 10
LastName As String * 10
ZipCode As String * 5 End Type
Путем применения различных функций для работы с файлами произведен выбор строки текста из файла, и переменная strTemp типа string содержит следующий текст:
"Peter Mason 90064"
Как сохранить различные фрагменты текста переменной strTemp в структуре данных TextData? Можно проанализировать символы при помощи других функций работы со строками, однако в этом нет необходимости — подобную работу в состоянии выполнить оператор LSet.
Единственным ограничением при использовании этого метода является то, что LSet нельзя применять для копирования переменной пользовательского типа в переменную встроенного типа и наоборот. Он работает только с двумя элементами данных встроенного или пользовательского типа. Попытка написать программу, подобную следующей, успехом не увенчается:
Dim typText As TextData ' Это не сработает.
LSet typText = strTemp
Чтобы заставить оператор LSet преобразовать данные из одного типа в другой, придется скопировать текстовые данные в еще один пользовательский тип. Однако это должен быть тип данных с единой строкой фиксированной длины, например:
Type TextTemp
strText As String * 25
End Type
Определение этого типа данных — лишь один дополнительный шаг при выполнении преобразования. Необходимо скопировать текст в элемент strText типа данных TextTemp. После этого с помощью оператора LSet можно скопировать байты из временной структуры данных поверх реальной.
Dim typTest As TextData
Dim typTemp As TextTemp
’ Копирование данных во временную структуру данных, а из нее — в реальную. typTemp.strText = strTemp
LSet typTest - typTemp
* Проверка успешного преобразования данных.
Debug.Print typTest.FirstName
Debug.Print typTest.LastName
Debug.Print typTest.ZipCode
Как вы убедились, оператор LSet весьма специфичен, однако при необходимости перенести большое количество полей из текстовой строки в структуру данных его применение существенно сократит число строк в программе.
Поиск строк
Во многих примерах этой главы понадобится проверить строку на наличие в ней других строк. Функция instr может определить, содержит ли одна строка другую, причем поиск начинается с определенной позиции строки. Кроме того, можно дополнительно задать поиск с учетом регистра символов.
В общем случае синтаксис функции inStr имеет следующий вид: intLocation = InStr([intStart,] strSearched, strSought[, intCompare])
В табл. 1.4 описаны параметры и возвращаемые значения функции inStr.
Таблица 1.4. Параметры функции InStr
Параметр
Описание
intstart
(необязательный)
Задает начальную позицию поиска (если параметр опущен, поиск начинается с первого символа; параметр intstart обязателен при использовании параметра intCompare)
strSearched
(обязательный)
Строковое выражение, в котором ведется поиск
strSought
(обязательный)
Искомое строковое выражение
intCompare
(необязательный)
Задает тип сравнения строк (может быть опущен или принимать одно из значений табл. 1.1; если параметр опущен, тип сравнения определяется установкой Option Compare для данного модуля)
Возвращаемое значение
Возвращается 0 — если строка strSought не обнаружена в строке strSearched; позиция символа, с которого начинается первое вхождение строки strSought В строку strSearched — если strSought обнаружено; intstart — если strSought имеет нулевую длину (или 1, когда аргумент intstart опущен)
Результатом выполнения этого примера будет число 3:
intPos = InStr("This is a test", "is")
А следующий пример начинает поиск не с начала строки и возвращает значение 6: intPos = InStr (4, "This is a test", "is")
Совет: Хотя это и документировано, легко упустить из виду, что при задании параметра intCompare для InStr необходимо указать и некоторое значение для параметра intstart. Однако параметр intCompare используется не очень часто. Обычно функция instr вызывается без этих параметров, поскольку они являются необязательными.
Работа с фрагментами строк
Многие строковые операции предполагают извлечение части строки. VBA облегчает эту задачу за счет использования ряда функций, осуществляющих выборку любой части строки. В комбинации с функцией inStr можно находить подстроки и при необходимости извлекать их.
VBA имеет три простые функции для работы со подстроками: Left, Mid и Right. Функция Left позволяет извлечь левую часть строки:
Left Astrin, intChars)
и возвращает первые несколько символов строки str in (количество символов задано в переменной intChars). Например, этот фрагмент программы возвращает первые две буквы заданной строки:
strLeft2 = Left("This is a test", 2)
Следующий фрагмент возвращает первое слово строки strin:
’ Эта программа не сработает, если в строке strin нет
’ пробелов. Вы не сможете применить функцию Left для
’ первых -1 символов строки!
strWord = Left (strin, InStr(strin, ” ") - 1)
Функция Right выполняет те же действия, однако символы отсчитываются с правой стороны строки. Следующий фрагмент программы добавляет обратную косую черту (\) к имени файла, которое хранится в переменной strFileName, если она там отсутствует:
If Right(strFileName, 1) <> ”\" Then strFileName = strFileName & "\"
End If
Функция Mid несколько сложнее, поскольку она выполняет больше действий. Она позволяет отыскать любой заданный фрагмент строки. Нужно задать строку, начальное положение и количество искомых символов, a VBA сделает все остальное. Если количество символов не указать, в результате будут получены все оставшиеся символы. Синтаксис функции Mid выглядит так:
strOut = Mid(strln, intStart[,intLen])
Например, в результате выполнения следующей строки программы:
strOut = Mid("This is a test", 6, 2)
переменная strOut будет содержать текст ”is". Приведенный ниже пример помещает весь текст, кроме первого слова, из строки strin в строку strRest:
strRest = Mid(strln, InStr(strIn, " ") + 1)
I С°вет: Никогда не делайте того, что мы сделали в этих примерах! Ни в коем I I У случае не передавайте непроверенное значение в функции Left, Right или Mid, если ваша процедура не содержит средств обработки ошибок. В тех примерах, где выполняется поиск первого слова или всего текста после первого слова, весьма вероятно, что переменная в действительности нс будет содержать пробела, и функция inStr возвратит 0. Тогда функциям Left или Mid будет передано значение -1, и им это наверняка не понравится. Убедитесь в том, что возвращаемое функцией inStr значение проверяется перед вызовом Left или Mid.
Чаще всего функция Mid применяется при циклической обработке отдельных символов в строках. Например, следующий цикл печатает отдельные символы строки:
Dim strTest As String
Dim inti As Integer strTest = "Look at each character" For inti = 1 To Len(strTest)
Debug.Print Mid(strTest, inti, 1)
Next inti
Дополнительно к использованию функций Left, Mid и Right для выделения фрагментов строк может понадобиться удалить начальные или конечные пробелы в
исходной строке. Для решения подобных задач в VBA предусмотрены функции LTrim, RTrim и Trim. Каждая из них выполняет только одну операцию: LTrim удаляет начальные пробелы, RTrim — конечные пробелы, a Trim — как начальные, так и конечные пробелы. В следующем примере продемонстрирован принцип применения, а также результаты работы этих функций:
Dim strTest As String
strTest = " This is a test ”
strTest = RTrim(strTest)
’ Теперь строка strTest имеет вид ” This is a test".
strTest = LTrim(strTest)
’ Теперь строка strTest имеет вид "This is a test".
strTest = " This is a test "
strTest = Trim(strTest)
Теперь строка strTest имеет вид "This is a test".
При желании обращение к функции Trim можно заменить следующим: LTrim(RTrim(strTest)).
Замена фрагментов строк
В этой главе приводятся примеры процедур, при помощи которых можно легко заменить любой фрагмент строки другим текстом. Однако в VBA выполнение большинства операций такого рода возможно с использованием одного оператора. Оператор Mid (да, он имеет то же название и те же параметры, что и функция Mid) позволяет заменить заданный текст в пределах одной строки текстом другой строки.
Чтобы заменить подстроку в пределах строки, следует применить оператор Mid с левой стороны строки программы. Синтаксис оператора Mid таков:
Mid^strText,intStart[,intLength]) = strReplace
Значение параметра intStart указывает позицию, с которой в строке strText будет выполняться замена, а значение параметра intLength — количество символов, которое требуется поместить из строки strReplace в строку strText, начиная с позиции intStart.
Например, после вызова следующей программы:
Dim strText As String
strText = "That car is fast."
Mid(strText, 6, 3) = "dog"
переменная strText будет содержать текст "That dog is fast". Хотя оператор Mid и применяется, но достаточно редко, поскольку невозможно контролировать, какая часть исходной строки будет заменена. Можно задать только длину используемой части заменяющей строки. Поэтому при выполнении следующей программы:
Dim strText As String
strText = "That car is-fast."
Mid(strText, 6, 4) = "fish"
вы получите фразу "That fishis fast", поскольку не существует возможности производить замену слов разной длины. Функции, представленные в разделе "Поиск и замена текста" настоящей главы, позволяют заменять целые слова.
Коды ANSI
Набор символов операционной системы устанавливает соответствие между числами, представляющими текстовые символы, и самими символами. При использовании набора символов ANSI Windows ставит в соответствие 256 символам значения от 0 до 255, а при использовании Unicode — значения от 0 до 65535. В стандарте ANSI каждый символ представлен кодом от 0 до 255. VBA имеет две функции — As с и Chr — для преобразования кодов в соответствующие символы и наоборот. Эти функции выполняют противоположные действия, т.е. при их последовательном вызове будет получено исходное значение.
Функция As с возвращает код, который соответствует первому символу строкового выражения, заданного в качестве аргумента, а функция chr — символ, который соответствует цифровому значению, заданному в качестве аргумента. В следующем фрагменте программы продемонстрировано, как использовать эти функции:
Dim intCh As Integer
Dim strCh As String * 1
intCh = Asc ("This is a test")
1 Сейчас переменная intCh содержит значение 84, которое соответствует символу Т.
StrCh = Chr(intCh)
’ Сейчас переменная strCh содержит букву Т, которой соответствует значение 84.
Размышления о быстродействиипри использованиифункций Asc и Chr
Приведенные логические выражения эквивалентны:
If Asc(strChar) = intANSIValue Then
и
If strChar = Chr(intANSIValue) Then
Однако лучше использовать первую конструкцию, поскольку в действительности эффективнее выполнить вычисление Asc, чем вызывать функцию Chr. При сравнении большого количества символов с определенными ANSI-кодами можно быть уверенным в том, что лучше преобразовывать символ в ANSI-код, а не наоборот. Подобная оптимизация, как правило, существенно экономит время процессора, особенно, если функция применяется часто.
Работа с байтами
В дополнение ко всем функциям, которые VBA предоставляет для работы со стандартными строками, существует ряд функций для работы с байтами внутри строк, а также для работы с символами в строках формата Unicode.
При работе с байтами, образующими строку символов, можно воспользоваться функциями: Lef tB, RightB, MidB, LenB, AscB, InStrB и ChrB. Каждая из них выполняет ту же операцию, что и соответствующая обычная функция, однако при этом
работает с байтами, а не с символами (рис. 1.1). Например, для строки из десяти символов функция Len возвращает число 10, а функция LenB — 20 (поскольку каждый символ занимает два байта). Первый фрагмент программы, которая приведена в примере 1.1, выполняет в цикле, организованном по строке символов, печать каждого символа в окне отладки. Второй цикл программы обрабатывает все байты строки, распечатывая каждый из них. В этом случае результат содержит нули между байтами, поскольку дополнительные байты для символов английского языка равны 0.
Пример 1.1. Цикл по символам и цикл по байтам
Sub DumpBytesO
' Печать текста "Hello" в окне отладки сначала по символам, * а затем — по байтам.
Dim inti As Integer
Dim strTest As String
strTest = "Hello"
For inti = 1 To Len (strTest)
Debug.Print Asc(Mid(strTest, inti, 1));
Next inti
Debug.Print
For inti = 1 To LenB(strTest)
Debug.Print AscB(MidB(strTest, inti, 1));
Next inti
End SubEnd SubDebug.Print
End Sub
End Sub
Совет: Вообще говоря, вряд ли вы будете писать программу с использованием функции MidB, подобную той, которая приведена в примере 1.1. Вместо этого целесообразно преобразовать строку в байтовый массив и обрабатывать каждый его элемент. Другие функции для работы с байтами необходимы для того, чтобы извлекать из строки нужные байты.
Рис. 1.1. Сравнение программ в циклах по байтам и по символамРис. 1.1. Сравнение программ в циклах по байтам и по символам
Рис. 1.1. Сравнение программ в циклах по байтам и по символам
Рис. 1.1. Сравнение программ в циклах по байтам и по символам
О функциях, оканчивающихся символом $
В VBA имена функций, которые возвращают строковые значения, бывают двух видов — со знаком доллара ($) на конце и без него. Разница в том, что функции без знака доллара возвращают данные типа Variant, а со знаком доллара — данные типа string. Функции, возвращающие значения типа Variant, могут передавать Null-значение, используя выражение, а функции, возвращающие string, — нет. Иными словами, если аргументом функции является переменная типа Variant, содержащая Null, функция без знака доллара возвратит Null, а функция со знаком доллара выдаст ошибку выполнения. С другой стороны, функции, возвращающие String, работают быстрее, поскольку они не требуют преобразований типов данных.
Какой вариант функции использовать? Если требуется достичь максимального быстродействия программы, и вы уверены в том, что в качестве аргументов этих функций не попадутся Null-значения, тогда, конечно, используйте функции, которые возвращают данные типа String.
Комбинирование функций
Познакомившись с основными функциями для работы со строками, можно начинать объединять их для решения более сложных задач. Остальная часть этой главы, в которой описаны методы, созданные на основе наших собственных библиотек функций для использования в реальных ситуациях, разбита на четыре раздела:
Поиск и замена текста
Сбор информации о строках
Преобразование строк
Работа с подстроками
Поиск и замена текста
В этом разделе приведены процедуры поиска и замены текста в строках. Хотя по сравнению с остальными программами данной главы они более объемны, они используются во многих последующих примерах, поэтому целесообразно рассмотреть их первыми.
В частности, здесь приведены решения таких задач:
замена символа в указанном списке другим символом;
удаление лишних пробелов с сохранением одного пробела между словами;
удаление из строки конечных нуль-символов;
замена одной подстроки другой;
замена меток в строке (в соответствии с позицией в переданном массиве).
Замена символа в списке другим символом
При редактировании текста часто возникает необходимость заменить некоторый символ списка другим символом. Например, если вы хотите подсчитать количество слов в предложении, можно заменить все знаки препинания в нем пробелами, а затем
подсчитать количество пробелов. Или может возникнуть потребность просто удалить из строки лишние символы, например при преобразовании телефонного номера вида (213)555-1212 в формат 2135551212. Функция dhTranslate упрощает выполнение таких задач (пример 1.2).
При помощи функции dhTranslate можно заменить все знаки препинания пробелами следующим образом:
strText = dhTranslate(strText, " ,.!:;<>?", " ")
а удаляют лишние символы так:
strText = dhTranslate("(213)555-1212", "О-", "")
Однако функция dhTranslate выполняет и другие задачи: если вы зададите соответствие между набором искомых символов и набором символов для замены, то функция заменит искомые символы требуемыми. Допустим, необходимо заменить буквы в телефонном номере на цифры. Вам сообщили телефонный номер 1-800-CALLKEN, однако вы хотите сохранить его в цифровом формате. Для задания заменяющих цифры символов можно применить функцию dhTranslate следующим образом:
strPhone = dhTranslate("1-800-CALLKEN",
"ABCDEFGHIJKLMNOPRSTUVWXY", "222333444555666777888999")
При таком вызове функция заменит каждую букву соответствующей ей цифрой.
Если строка с символами для замены короче строки, содержащей искомые символы, функция dhTranslate удлиняет ее до нужной длины. Таким образом, при вызове функции dhTranslate с короткой заменяющей строкой:
strText = dhTranslate(strText, " ,.!:;<>?", " ")
она преобразует третий параметр в строку с тем же количеством символов, что и у второго параметра, будто вы вызвали функцию следующим образом:
strText = dhTranslate(strText, " ,.!:;<>?", "")
Итак, каждому символу второй строки поставлен в соответствие пробел в качестве заменяющего символа.
Если вы хотите вызвать функцию dhTranslate самостоятельно, задайте три обязательных параметра и один необязательный:
strText = dhTranslate(strin, strMapin, strMapOut[, fCaseSensitive])
Параметры функции dhTranslate означают следующее:
■ strin — строка, которую нужно изменить;
■ strMapin — строка, которая содержит символы, подлежащие замене;
■ strMapOut — строка, содержащая нуль или более символов для замены соответствующих символов строки StrMapin (если эта строка короче строки strMapin, функция дополняет ее последним символом строки strMapOut).
■ fCaseSensitive — необязательный параметр. Чтобы при поиске учитывался регистр символов, его значение должно быть установлено в True (если регистр символов учитывать не требуется, установите значение этого параметра в False — значение по умолчанию).
Результатом выполнения функции является исходная строка (st г in) с требуемыми изменениями.
Пример 1.2. Преобразование одного набора символов в другой
Function dhTranslate(ByVai strin As String, _
ByVai strMapIn As String, ByVai strMapOut As String, _ Optional fCaseSensitive As Boolean = True) As String 1 Списку символов в strMapIn ставятся в соответствие 1 символы в strMapOut и производится замена символов в строке strin. Dim inti As Integer Dim intPos As Integer Dim strChar As String * 1 Dim strOut As String Dim intMode As Integer ’ Если список символов для замены не задан, 1 продолжать выполнение функции не имеет смысла. If Len(strMapIn) > 0 Then
’ Установите режим сравнения. If fCaseSensitive Then
intMode = vbBinaryCompare Else
intMode = vbTextCompare End If ’ Дополнение справа набора символов strMapOut. If Len(strMapOut) > 0 Then
strMapOut = Left$(strMapOut & String(Len(strMapIn), _ Right$(strMapOut, 1)), Len(strMapIn)) End If For inti = 1 To Len(strin) strChar = Mid$(strin, inti, 1) intPos = InStr(1, strMapIn, strChar, intMode) If intPos > 0 Then
Если строка strMapOut пуста, это не приведет к ошибке, ’ поскольку функция Mid допускает обработку пустых строк.
strOut = strOut & Mid$(strMapOut, intPos, 1) Else strOut = strOut & strChar End If Next inti End If dhTranslate = strOut End Function
Прежде чем производить какие-либо операции, функция dhTranslate проверяет, содержит ли параметр strMapIn некоторый текст. Если это не так, значит, выполнять нечего, и функция завершает работу.
Подобно многим другим функциям, рассмотренным в этой главе, на первом шаге выполнения функция dhTranslate устанавливает переменную, определяющую характер поиска с помощью функции inStr. Переменная intMode в зависимости от значения параметра fCaseSensitive будет содержать одну из двух констант: vbBinaryCompare или vbTextCompare. Данный программный фрагмент встречается в этой главе несколько раз: If fCaseSensitive Then intMode = vbBinaryCompare
Else
intMode = vbTextCompare End If
Далее функция dhTranslate обеспечивает, чтобы параметры strMapOut и strMapin содержали одинаковое количество символов. Для этого она выбирает крайний правый символ строки strMapOut, формирует из него строку, равную по длине строке strMapin, добавляет ее к строке strMapOut, а затем обрезает полученную строку до длины strMapin:
Дополнение строки символов strMapOut справа.
If Len(strMapOut) > 0 Then
strMapOut = Left$(strMapOut & String(Len(strMapin), _
Right$(strMapOut, 1)), Len (strMapin)) End If
Например, если строка strMapin имеет значение "1234567890", а строка strMapOut — значение "ABODE", программа формирует строку из десяти символов Е (такой же длины, что и строка strMapin), присоединяет ее к концу строки strMapOut (таким образом, strMapOut преобразуется в "ABCDEEEEEEEEEEE"), а затем обрезает полученную строку до длины строки strMapin (десять символов, или "abcdeeeeee"). Такой метод дает возможность заменять ряд символов, заданных в строке strMapin, одним символом из строки strMapOut.
Наконец, функция dhTranslate производит замену. Она пытается найти символы из исходной строки в строке strMapin:
For inti = 1 То Len (strin)
strChar = Mid$(strin, inti, 1)
intPos = InStr(1, strMapin, strChar, intMode)
Программа продолжается...
Next inti
Когда inStr обнаруживает совпадение символов, значение intPos будет больше нуля. Функция dhTranslate находит соответствующий символ замены в строке strMapOut и присоединяет его к концу результирующей строки. Если inStr не удалось обнаружить совпадение, dhTranslate просто присоединяет символ исходной строки к концу результирующей строки.
If intPos > 0 Then
Если строка strMapOut пуста, это не приведет к ошибке, поскольку
’ функция Mid допускает обработку пустых строк.
strOut = strOut & Mid$(strMapOut, intPos, 1)
Else
strOut = strOut & strChar
End If
Таким образом, функция dhTranslate копирует по одному либо символы исходной строки в результирующую строку, либо соответствующие символы из строки strMapOut. В любом случае результатом выполнения функции является строка strOut.
Многие функции "ждут", что функция dhTranslate выполнит часть работы за них. Несомненно, вы также найдете множество применений для этой функции в ваших программах.
Удаление лишних пробелов
Если требуется удалить из строки лишние пробелы (безусловно, эта операция необходима при выполнении функции dhCountWords, которая подсчитывает слова в строке и описана в этой главе ниже), вам поможет функция dhTrimAll. Она просматривает строку и формирует новую выходную строку, копируя в нее только по одному пробелу всякий раз, когда находит в исходной строке один или несколько пробелов. При желании вы можете заставить функцию dhTrimAll удалять и символы табуляции.
Например, при вызове функции:
strOut = dhTrimAll(" This is a test" & _
" of how this works")
строке strOut присваивается значение "This is a test of how this works". По умолчанию функция удаляет как пробелы, так и символы табуляции. Если вы хотите, чтобы функция игнорировала символы табуляции и удаляла только пробелы, задайте для второго параметра значение False. Функция dhTrimAll приведена в примере 1.3.
Пример 1.3. Удаление лишних пробелов из строки
Function dhTrimAll(ByVai strText As String, _
Optional fRemoveTabs As Boolean = True) As String
* Удаление пробелов в начале и в конце строки и замена любого
’ количества пробелов (и символов табуляции) внутри строки
’ одиночными пробелами.
Dim strTemp As String
Dim strOut As String
Dim inti As Integer
Dim strCh As String * 1
’ Удаление пробелов в начале и в конце строки.
* Сначала все символы табуляции преобразуются
* в пробелы. В противном случае функции RTrim и LTrim пропустят их.
If fRemoveTabs Then
strText = dhTranslate (strText, vbTab, " ")
End If
strTemp = Trim(strText)
For inti = 1 To Len(strTemp)
’ Поочередный просмотр каждого символа.
strCh = Mid$(strTemp, inti, 1)
Если данный символ — пробел, а предыдущий добавленный
символ также был пробелом, он игнорируется;
’ в противном случае, он добавляется в строку.
If Not (strCh = " " And Right$(strOut, 1) = " ") Then strOut = strOut & strCh
End If
Next inti
dhTrimAll = strOut
End Function
Как работает функция dhTrimAll? Она начинает с того, что преобразует, если это необходимо, все символы табуляции в пробелы, используя функцию dhTranslate:
If fRemoveTabs Then
strText = dhTranslate (strText, vbTab, " ")
End If
После того как символы табуляции удалены, вызывается функция Trim. Эта функция позаботится об удалении всех пробелов в начале и в конце исходной строки: strTemp = Trim(strText)
Последний шаг — самый трудоемкий, поскольку цикл выполняется для каждого символа в отдельности. Символ, если он не является пробелом и предыдущий добавленный к строке символ также не являлся пробелом, попросту присоединяется к результирующей строке. Если же текущий и предыдущий символы — это пробелы, текущий символ пропускается. Таким образом, в результирующей строке не будет встречаться несколько пробелов подряд.
For inti = 1 То Len(strTemp)
Поочередный просмотр каждого символа.
StrCh = Mid$(strTemp, inti, 1)
’ Если данный символ — пробел, а предыдущий добавленный
символ также был пробелом, он игнорируется;
в противном случае, он добавляется к строке.
If Not (strCh = " " And Right$(strOut, 1) = " ") Then strOut = strOut & strCh
End If Next inti
Удаление из строки конечных нуль-символов
Хотя вы будете использовать функцию dhTrimNull при работе с Windows API, она пригодится вам и в других случаях. Функции API не знают, что является исходной строкой, и имеют тенденцию помещать оканчивающиеся нуль-символом строки в буферы, которые вы им передаете. К сожалению, VBA требует задания длины строки, поэтому вам необходимо найти в строке первый Null символ (Chr$(0) или vbNullChar) и обрезать строку при помощи функции Left. В примерах, приведенных в следующих главах, такая функция применяется, и важно подготовить ее для этого.
Функция dhTrimNull из примера 1.4 принимает строку и возвращает ее, обрезав перед первым нуль-символом.
Пример 1.4. Отсечение строк перед первым нуль-символом
Function dhTrimNull(ByVai strValue As String) As String
’ Находит первый символ vbNullChar в строке и возвращает в качестве
’ результата все символы, предшествующие ему. Данная функция
чрезвычайно полезна в сочетании с функциями Windows API.
Dim intPos As Integer
intPos = InStr(strValue, vbNullChar)
Select Case intPos
Case 0
He найден вообще, поэтому просто
возвращается исходное значение. dhTrimNull = strValue
Case 1
Обнаружен в первой позиции, поэтому
1 возвращается пустая строка.
dhTrimNull = ””
Case Is > 1
’ Обнаружен в строке, поэтому возвращается * фрагмент строки до нулевого символа.
dhTrimNull = Left$(strValue, intPos - 1) End Select
End Function
Для своей работы функция dhTrimNull вызывает функцию InStr, передавая ей в качестве аргументов исходную строку и константу vbNullChar. В зависимости от возвращаемого функцией inStr значения (которое присваивается переменной intPos), функция выполняет одно из трех действий:
если значение intPos равно 0, функция возвращает в качестве результата исходную строку (в строке нс оказалось нулевых символов);
если значение intPos равно 1, т.с. первый символ оказался нулевым, функция возвращает в качестве результата пустую строку;
если значение intPos больше 1, функция dhTrimNull вызывает функцию Left, которая выделяет часть строки до первого нулевого символа, не включая его.
Во всех трех случаях исключается возможность задания неправильной начальной позиции для функции Left.
Замена одной подстроки на другую
Допустим, нужна процедура для замены в строке всех вопросительных знаков на восклицательные или всех кавычек на апострофы. Можно каждый раз писать программы из нескольких строк, но можно использовать обобщенную функцию, выполняющую любую требуемую замену. Функция dhReplaceAll написана с целью облегчить замену одного текста другим.
Она позволяет нс только выполнить замену, но и задать, с какого вхождения искомой строки следует начать се и сколько замен нужно произвести. В следующем списке описаны параметры, которые можно задать для функции dhReplaceAll (первые три параметра являются обязательными), и возвращаемое значение:
strText — задаст текст, в котором следует проводить поиск;
strFind — задаст искомый текст;
strReplace — задаст текст, которым будет заменен текст strFind;
intFirst (необязательный параметр) — целое число, задающее вхождение, с которого следует производить замену (если значение параметра превышает количество вхождений искомого текста в строку, нс выполняется ни одной замены; если его значение равно -1, заменяется последнее вхождение, если -2 — предпоследнее и последнее вхождение и т.д.; по умолчанию значение параметра равно 1, т.е. программа выполняет замену, начиная с первого вхождения);
intcount (необязательный параметр) — представляет собой целое число, указывающее на количество замен, которые необходимо выполнить (если значение параметра превышает число вхождений искомого текста, искомый текст во всех позициях заменяется; если равно 0 или меньше -1, — нс производится ни одной замены; по умолчанию устанавливается значение dhcNoLimit (-1), т.с. функция заменяет все вхождения искомого текста);
fCaseSensitive (необязательный параметр) — определяет, будет ли поиск чувствительным к регистру символов (значение параметра по умолчанию False, т.с. при поиске регистр символов учитывается);
Возвращаемым значением функции является копия исходной строки strText, в которой выполнены требуемые замены.
В табл. 1.5 продемонстрированы некоторые примеры использования функции dhReplaceAll. Во всех примерах значения первых трех параметров одинаковы, что позволяет проследить, как влияют на результаты поиска последние три параметра. Процедура замены, полностью приведенная в примере 1.5, является довольно сложной.
Таблица 1.5. Примеры вызова функции dhReplaceAll
Пример
Результат
Комментарий
DhReplaceAll (’’This IS а ”is”, нХи)
test", "ThX X a test"
Заменяет все вхождения
DhReplaceAll (’’This IS a "is", "X", 2)
test", "This X a test"
Начинает замену со второго вхождения
DhReplaceAll("This IS a "is", "X", -1)
test", "This X a test"
Начинает замену с последнего вхождения
DhReplaceAll("This IS a "is", "X", 1, 1)
test", "ThX IS a test"
Начинает замену с первого вхождения и делает одну замену
DhReplaceAll("This IS a "is", "X", fCaseSensitive:
test", "ThX IS a test" =True)
При поиске учитывается регистр символов
Пример 1.5. Замена всех вхождений подстроки
Function dhReplaceAll( _
ByVai strText As String, _
ByVai strFind As String, _
ByVai strReplace As String, _
Optional ByVai intFirst As Integer = 1, _
Optional ByVai intCount As Integer = dhcNoLimit, _
Optional ByVai fCaseSensitive As Boolean = False) As String
* Замена всех вхождений строки strFind в строку strText на strReplace.
Dim intLenFind As Integer
Dim intLenReplace As Integer
Dim intPos As Integer
Dim intStart As Integer
Dim inti As Integer
Dim intFound As Integer
Dim intLast As Integer
Dim intMode As Integer
On Error GoTo HandleErr
* В случае, если какой-либо параметр имеет недопустимое
* значение, происходит выход из программы. Нестандартный ' метод, однако в данном случае он срабатывает.
If Len(strText) = 0 Then GoTo ExitHere
If Len(strFind) = 0 Then GoTo ExitHere
If intFirst = 0 Then GoTo ExitHere
If intCount = 0 Then GoTo ExitHere
* Параметры, должно быть, приемлемы, если вы достигли этой точки.
’ Обработка трех необязательных параметров.
If intFirst < 0 Then
’ -1 == начинать с последнего вхождения.
’ -2 == начинать с предпоследнего вхождения и т.д. intFound = dhCountln(strText, strFind) intFirst = intFound + intFirst + 1 If intFirst < 1 Then intFirst = 1 End If If intCount > dhcNoLimit Then intLast = intFirst + intCount End If If fCaseSensitive Then
intMode = vbBinaryCompare
Else intMode = vbTextCompare
End If
’ Сохранение длины искомого и замещающего текста для ’ ускорения выполнения последующих операций.
intLenFind = Len(strFind) intLenReplace = Len(strReplace)
intPos = 1
inti = 1 Do
intPos = InStr (intPos, strText, strFind, intMode) If intPos > 0 Then
Обнаружено ли совпадение? Если совпадение обнаружено, проверяются ’ другие параметры (место первой замены и количество замен) .
If (inti >= intFirst) And _
((intCount = dhcNoLimit) Or (inti < intLast)) Then
Если номер текущего вхождения б'ольше или равен номеру
первого вхождения, которое должно быть заменено, 1 и. . .
’ число замен не задано, либо номер текущей замены
меньше номера последней замены, которую вы хотите произвести, 1 то замена выполняется.
strText = Left$(strText, intPos - 1) & _ strReplace & Mid$(strText, intPos + intLenFind)
Новый текст пропускается. intPos = intPos + intLenRseplace
Else
* Найденное вхождение пропускается. intPos = intPos + intLenFind
End If inti = inti + 1 * Если известно, что все замены уже выполнены, нет необходимости * продолжать просмотр строки — следует просто выйти из цикла!
If (intCount о dhcNoLimit And inti >= intLast) Then Exit Do
End If End If
Loop Until intPos = 0
ExitHere:
dhReplaceAll = strText
Exit Function
HandleErr:
При возникновении ошибки текст возвращается
без изменений.
Select Case Err.Number
Case Else
* MsgBox "Error: " & Err.Description & " (" & Err.Number & ")"
End Select
Resume ExitHere
End Function
Эта функция, одна из самых длинных в главе, не будет слишком громоздкой, если разбить ее на несколько шагов. Хотя процедура кажется ужасно сложной, она настолько полезна, что излишняя сложность вполне "окупается". Кроме того, многие другие процедуры данной главы используют эту функцию, и вы с ней еще не раз столкнетесь. Ниже приведены основные шаги программы.
Проверка правильности параметров.
Обработка трех необязательных параметров.
Поиск.
Выяснение, нужно ли производить замену.
Выполнение замены, если это необходимо.
Выход из цикла.
В последующих разделах данная процедура рассмотрена подробно.
Проверка правильности параметров
Хотя приведенный здесь подход несколько необычен, в данном случае он целесообразен. Потому что имеется ряд ситуаций, когда выполнение функции прекращается:
если строка, где производится поиск, пуста;
если строка, которую нужно найти, пуста;
если вы указали функции осуществлять замену, начиная с нулевого вхождения;
если вы указали функции произвести нуль замен:
If Len(strText) = 0 Then GoTo ExitHere
If Len(strFind) = 0 Then GoTo ExitHere
If- intFirst = 0 Then GoTo ExitHere
If intCount = 0 Then GoTo ExitHere
Обработка трех необязательных параметров
Если функции dhReplaceAll задать отрицательное число в качестве номера вхождения, с которого следует начинать замену, то замена будет выполняться в обратном направлении, начиная с последнего вхождения. (-1 означает, что начать следует с последнего вхождения, -2 — с предпоследнего и т.д.) Для определения суммарного числа вхождений программа вызывает функцию dhCountin (см. раздел "Подсчет числа вхождений подстроки в строку" данной главы), добавляет к этому числу значение intFirst и использует полученный результат в качестве нового значения номера
вхождения, с которого следует начинать замену. Если полученное значение меньше единицы, программа, естественно, должна начинать с первого вхождения: If intFirst < 0 Then
' -1 == начинать с последнего вхождения.
* -2 ==■ начинать с предпоследнего вхождения и т.д.
intFound = dhCountln(strText, strFind)
intFirst = intFound + intFirst + 1
If intFirst < 1 Then intFirst = 1 End If
Если вы указали число замен в параметре intCount, рассчитывается номер последней замены:
If intCount > dhcNoLimit Then intLast = intFirst + intCount End If
Как и во многих последующих примерах, определяется значение локальной переменной intMode, которая используется функцией InStr:
If fCaseSensitive Then
intMode = vbBinaryCompare
Else
intMode = vbTextCompare End If
Нужно ли производить замену?
Обнаружив совпадение, программа должна определить, нужно ли заменять текст. Замена выполняется только в том случае, если номер данного вхождения больше или равен номеру первого заменяемого вхождения (inti >= intFirst) и если либо все вхождения подлежат замене (intCount = dhcNoLimit), либо номер этого вхождения меньше номера последнего заменяемого вхождения (inti < intLast). После завершения процесса замены программа переносит позицию начала поиска в конец вставленного фрагмента — нет смысла искать следующее совпадение в тексте, в котором только что выполнена замена! Если замена нс была произведена, программа просто перемещает позицию начала поиска в конец найденного фрагмента. И, наконец, программа увеличивает на единицу число обнаруженных совпадений:
* Обнаружено ли совпадение? Если да, то проверяются другие
* параметры (место первой замены и количество замен).
If (inti >= intFirst) And _
((intCount = dhcNoLimit) Or (inti < intLast)) Then
’ Фрагмент программы опущен. В этом месте выполняется замена.
intPos intPos т intLenReplace
Else
' Найденное вхождение пропускается.
IntPos = intPos + intLenFind
End If
inti = inti + 1
* Фрагмент программы опущен.
End If
Выполнение замены, если это необходимо
Этот шаг программы самый простой. Берется часть текста до обнаруженного совпадения, к нему добавляется заменяющая строка, а затем присоединяется часть исходной строки после совпадения, и результат помещается назад в переменную, содержащую текст, в котором осуществляется поиск:
strText = Left$ (strText, intPos - 1) & _
strReplace & Mid$(strText, intPos + intLenFind)
Выход из цикла
Простая оптимизация приводит к преждевременному выходу из цикла, если совпадений больше не встречается. Другими словами, из цикла пора выходить, когда выполнено указанное количество замен.
If (intCount о dhcNoLimit And inti >= intLast) Then Exit Do
End If
Замена нумерованных меток в строке
Если вы создаете текст, который должен быть переведен на другие языки, или просто хотите заменить ряд меток в строке набором текстовых строк, вам поможет функция из примера 1.6. Она позволяет указать список текстовых строк для замены нумерованных меток (%1, % 2 и т.д.) в текстовой строке.
Если вы отделяете программный код вашего приложения от пользовательского интерфейса, то намного легче подготовить приложение для международного использования. Однако некоторые строки неизбежно должны содержать заменяемые параметры. Применение функции dhTokenReplace значительно облегчает осуществление этих замен в процессе работы программы. Например, в результате выполнения следующего фрагмента:
strText = dhTokenReplace("Unable to add file %1 to %2", _
"C:\AUTOEXEC.BAT", "C:\FOO.ZIP")
текст "Unable to add file C:\AUTOEXEC.BAT to C:\FOO.ZIP" будет помещен в переменную strText. Предполагается, что строка "Unable to add. . . " извлекается из таблицы, файла ресурсов или какого-либо другого источника, внешнего по отношению к вашему приложению, и переведена на другие языки. Но что, если в некотором языке данная фраза должна быть построена так: "C:\FOO.ZIP is unable to contain С: \AUTOEXEC. BAT"? В этом случае переводчик должен изменить текст следующим образом: "%2 is unable to contain % 1", и программа будет работать правильно.
Даже если вы не создаете локализованные приложения, функция dhTokenReplace все равно поможет вам. Возможность замены нескольких подстрок за один проход может ускорить работу ваших приложений и наверняка ускорит их написание.
;Использование ParamArray
для передачи массива параметров
Хотя параметр ParamArray имелся и в предыдущих версиях VBA, немногие программисты работали с ним. Применяется он нечасто, однако иногда он со- : вершенно необходим. В данном случае возможность передавать фактически । неограниченное количество параметров позволяет написать одну функцию, | которая способна обрабатывать бесконечное число ситуаций.|
Чтобы использовать эту возможность, следует объявить функцию таким образом, чтобы она разрешала использовать параметр, описанный как ParamArray:
Public Function dhTokenReplace(ByVai strin As String, _ParamArray varltemsO As Variant) As String
Теперь при вызове функции после обязательных параметров ей можно передавать сколько угодно элементов. VBA преобразует их в массив и передаст процедуре. Ваша процедура получает этот массив с помощью параметра, который вы описали как ParamArray, и вы можете воспользоваться любыми способами обработки массивов для работы с данными параметрами.
Параметр, описанный как ParamArray, должен:
быть последним в списке параметров функции при ее объявлении;
не использоваться с ключевыми словами Optional, ByVai и ByRef ;
быть объявлен как массив типа Variant.
Пример 1.6. Замена нумерованных меток в строке
Public Function dhTokenReplace(ByVai strin As String, _ ParamArray varltemsO As Variant) As String
Замена %1, %2, %3 и т.д. значениями, переданными в varltems. On Error GoTo HandleErr Dim intPos As Integer Dim strReplace As String Dim inti As Integer
For inti = LBound(varltems) To UBound(varltems) strReplace = "%" & (inti + 1) intPos = InStr (1, strin, strReplace) If intPos > 0 Then
strin = Left$ (strin, intPos - 1) & _ varltems(inti) & Mid$(strin, intPos + Len(strReplace)) End If
Next inti
ExitHere:
dhTokenReplace = strin
Exit Function
HandleErr:
1 При возникновении ошибки строка возвращается
* без изменений.
Select Case Err.Number
Case Else
* MsgBox "Error: " & Err.Description & " (" & Err.Number & ")"
End Select
Resume ExitHere
End Function
Функция dhTokenReplace очень похожа на функцию dhReplaceAll, но несколько проще. Нет необходимости проверять параметры (если массив variterns пуст, сразу же произойдет выход из цикла и возврат в точку вызова). Кроме того, не нужно выполнять цикл для замены всех вхождений строки в массив. Для каждой строки входного массива может быть сделана только одна замена.
Для своей работы функция dhTokenReplace организует цикл по всем элементам входного массива, начиная с нижней границы и заканчивая верхней:
For inti = LBound(varltems) To UBound(varltems)
* Фрагмент программы опущен.
Next inti
Для каждого элемента массива программа формирует новый номер (типа "%Y\ "%Т и т.д.), а затем выполняет поиск таких строк в тексте:
strReplace = "%" & (inti + 1)
intPos = IntStr(l, strin, strReplace)
Если inStr находит совпадение (т.е. значение intPos больше нуля), dhTokenReplace модифицирует исходную строку таким образом, что в результате она содержит весь текст до обнаруженного совпадения, далее текст замены, а затем — весь текст после него:
If intPos > 0 Then
strin = Left$(strin, intPos - 1) & _
varltems(inti) & Mid$(strin, intPos + Len(strReplace)) End If
Вот и все! Повтор этих операций для каждого элемента исходного массива приводит к замене всех меток текстом.
Сбор информации о строках
В этом разделе описаны методы получения информации о текстовых строках, а именно:
Определение атрибутов символа
Подсчет числа вхождений подстроки в строку
Определение атрибутов символа
При проверке правильности текста часто возникает необходимость проверить каждый символ строки. Вам может понадобиться информация о том, каков данный символ: буквенный (A-Z, в английском языке), цифровой (0-9) либо буквенно-цифровой (A-Z, 0-9 в английском языке). Первые две проверки быстрее всего выполняются с помощью обращения к API, а последнюю можно произвести несколькими способами.
Является ли символ буквой?
Когда нужно убедиться, что данный символ — буква (а не цифра, знак препинания, специальный символ и т.д.), то первое, что приходит в голову, — проверить, попадает ли он в диапазон A-Z или a-z. Это ошибочно по двум причинам:
■ если вы хотите, чтобы ваше приложение было пригодно для распространения в других странах, то почти наверняка такая программа нс сможет работать с другими языками;
■ использование VBA для этой цели — самый неудачный способ из всех возможных.
Лучше поручить решение этой задачи Windows. Вызовите API-функцию I sCharAlpha, и Windows определит, является ли данный символ буквой. В таком случае проверка выполняется быстрее, и вам не придется беспокоиться о языковых проблемах — Windows знает, считается данный символ буквой в конкретной локализованной среде или нет.
Чтобы использовать API-функцию, се сначала нужно объявить.
Private Declare Function IsCharAlphaA Lib "USER32" _ (ByVai bytChar As Byte) As Long
Для применения API-функции IsCharAlphaA можно вызвать функцию dhlsCharAlpha:
Function dhlsCharAlpha(strText As String) As Boolean
* Является ли первый символ строки strText буквой?
dhlsCharAlpha - CBool (IsCharAlphaA(Asc(strText))) End Function
Эта простая функция преобразует первый символ исходного текста в цифровой код (посредством функции Asc), вызывает IsCharAlphaA и преобразует результат в значение типа Boolean.
Чтобы убедиться, что первый символ строки, заданной пользователем, является буквой, можно применить функцию dhlsCharAlpha следующим образом:
If dhlsCharAlpha(strText) Then
* Вы знаете, что первый символ строки strText - буква.
End If
Является ли символ буквенно-цифровым?
Для определения того, является ли символ буквенно-цифровым, Windows предоставляет простую API-функцию IsCharAlphaNumericA. Объявите се следующим образом:
Private Declare Function IsCharAlphaNumericA Lib ’’USER32" _ (ByVai bytChar As Byte) As Long
Как и в предыдущем примере, для упрощения вызова этой API-функции мы предлагаем простую функцию:
Function dhlsCharAlphaNumeric(strText As String) As Boolean
Является ли первый символ строки strText буквенно-цифровым?
dhlsCharAlphaNumeric = CBool(IsCharAlphaNumericA(Asc(strText))) End Function
Функция возвращает значение True, если первый символ переданной вами строки — буква или цифра.
Является ли символ цифрой?
Несмотря на то, что определить, является ли заданный символ цифрой, довольно просто, для выбора оптимального решения необходимо попробовать различные подходы. Мы остановились на двух методах, почти идентичных по эффективности, а вам остается выбрать один из них на свое усмотрение.
Первый метод использует оба предыдущих решения — т.е. символ является цифровым, если он буквенно-цифровой, но нс является буквой. Таким образом, первый способ реализуется с помощью функции dhlsCharNumeric:
Function dhlsCharNumeric(strText As String) As Boolean
'Является ли первый символ строки strText цифрой?
dhlsCharNumeric = dhlsCharAlphaNumeric(strText) _
And Not dhlsCharAlpha(strText)
End Function
В качестве альтернативного метода предлагается использовать оператор Like, который описан ранее. Если проверка осуществляется только для того, чтобы определить, что указанный символ — цифра, такое решение является наилучшим. В этом случае не вызываются API-функции, и нет необходимости вводить дополнительные объявления. Если вы уже работаете с двумя другими API-функциями, можете использовать их и здесь. Альтернатива заключается в сравнении первого символа исходной строки с символами из диапазона 0-9:
Function dhlsCharNumericl(strText As String) As Boolean
' Является ли первый символ цифрой?
* Выполняется почти также быстро, как и вызов двух API-функций.
dhlsCharNumericl = (strText Like ’’[0-9]*")
End Function
j Работа с расширенными наборами символов
К сожалению, предложенные здесь методы с вызовом API-функций Windows нс сработают, если ваша версия Windows использует 2-байтовую кодировку символов или программа будет работать на компьютере, использующем 2-байтовые символы. В этих случаях необходимо выполнить дополнительные j действия.
Проще всего поступить следующим образом: определить максимальную ширину символа в выбранном наборе символов и на основе этого вызывать соответствующую API-функцию. Спецификация 32-разрядного Windows API включает по две версии для большинства функций, предназначенных для j работы со строками: одну для работы в среде ANSI, а другую — в среде DBCS и | Unicode. В приведенных примерах вызываются функции ANSI-версий, I поскольку они работают для текстов на английском языке.j
Для того чтобы определить, нужно ли использовать альтернативные API-вызо- | вы, можно воспользоваться функцией dhlsCharsetWide. От возвращаемого j этой функцией значения будет зависеть, API-функцию какой версии вызывать: , ANSI или Unicode.|
| Например:
i Function dhlsCharAlphaNumeric(strText As String) As Boolean If dhlsCharsetWide() Then
dhlsCharAlphaNumeric = _
■CBool(IsCharAlphaNumericW(AscW(strText)))
Else
dhlsCharAlphaNumeric = _
|CBool(IsCharAlphaNumericA(Asc(strText)))
\End If
j End Function
Обратите внимание, что вы должны также вызывать функцию AscW, когда
i работаете с API-функциями "расширенных” версий.
Подсчет числа вхождений подстроки в строку
Встроенная функция VBA InStr предоставляет информацию о том, встречается ли заданная подстрока в другой строке (instr возвращает позицию искомой подстроки, если она там есть, и 0, когда ее там нет). Однако эта функция не позволяет определить число вхождений подстроки в строку. Для этого можно воспользоваться функцией dhCountln из примера 1.7.
Пример 1.7. Определение количества вхождений подстроки
Function dhCountln(strText As String, strFind As String, _
Optional fCaseSensitive As Boolean = False) As Integer
Определение числа появлений strFind в strText.
Dim intCount As Integer
Dim intPos As Integer
Dim intMode As Integer
Если не задана строка для поиска, то, естественно,
ничего не может быть найдено, таким образом, возвращается 0.
If Len(strFind) > 0 Then
Установка режима сравнения.
If fCaseSensitive Then
intMode = vbBinaryCompare
Else
intMode = vbTextCompare
End If
intPos = 1
Do intPos = InStr(intPos, strText, strFind, intMode) If intPos > 0 Then.
intCount = intCount + 1
intPos = intPos + Len(strFind) End If
Loop While intPos > 0
Else intCount = 0 End If
dhCountln = intCount End Function
Очевидно, что если не задан объект поиска, функция возвращает 0:
If Len(strFind) > 0 Then
1 (фрагмент программы опущен) Else intCount = 0 End If
Как и многие другие функции в этой главе, функция dhCount in сначала устанавливает значение переменной, определяющей способ поиска функцией inStr. Переменная intMode может принимать значения vbBinaгyCompare или vbTextCompare, в зависимости от значения параметра fCaseSensitive:
If fCaseSensitive Then
intMode = vbBinaryCompare Else
intMode = vbTextCompare End If
Теперь, наконец, можно производить поиск. Программа выполняет циклический поиск заданной подстроки в исходном тексте до тех пор, пока совпадений больше не обнаруживается (т.е. пока возвращаемое функцией instr значение не станет равным 0). При обнаружении каждого следующего совпадения, значение переменной intCount увеличивается на единицу, и позиция начала поиска смещается в исходной строке к символу, следующему после конца найденной подстроки. Это не только ускоряет поиск (зачем просматривать текст, начиная прямо со следующего символа после обнаруженного совпадения, если искомый текст имеет длину, скажем, в четыре символа?), но и предотвращает обнаружение перекрывающихся совпадений. Ниже представлен фрагмент программы, который выполняет основную часть работы: intPos = 1
Do
intPos = InStr(intPos, strText, strFind, intMode) If intPos > 0 Then
intCount = intCount + 1 intPos = intPos + Len(strFind) End If
Loop While intPos > 0
Для определения количества гласных букв в строке можно написать такую программу:
intVowels = dhCountln(strText, "А") + dhCountln(strText, "E") + _ dhCountln(strText, "I") + dhCountln(strText, "O") + _ dhCountln(strText, "U")
Преобразование строк
В этом разделе представлен ряд методов, предназначенных для преобразования строк из одной формы в другую, и рассмотрены следующие вопросы:
Реверсия строки
Представление десятичных чисел римскими цифрами
Кодирование и декодирование текста при помощи XOR-кодировки
Дополнение строки слева или справа до заданной длины
Сравнение строк при помощи алгоритма Саундекса
Реверсия строки
Скорее всего, вам нечасто понадобится осуществлять реверсию строк. Однако при необходимости полезно иметь функцию для выполнения этой операции.
Примечание: На самом деле функция dhReverse нужна для выполнения функции dhRinstr, которая описана далее в данной главе. Нам пришлось написать и функцию dhReverse, она также представлена здесь.
Кроме того, при написании данной функции была возможность попрактиковаться в работе с байтовыми массивами. Ниже представлен простой вариант этой функции. Окончательный, более сложный, вариант работает приблизительно в два раза быстрее по сравнению с первоначальной версией.
Функция dhReverse из примера 1.8 принимает в качестве аргумента строку текста и возвращает строку, в которой символы следуют в обратном порядке. Например:
dhReverse("This is a test")
возвращает значение "tset a si sihT".
Пример 1.8. Реверсия строки с использованием байтового массива
Function dhReverse(ByVai strin As String) As String ' Посимвольная реверсия строки. Dim abytln () As Byte Dim strTemp As String Dim inti As Integer Dim intMax As Integer Dim bytTemp As Byte Dim intNew As Integer If Len(strin) = 0 Then dhReverse = strin Else abytln = strin 1 Реверсия всех символов. intMax = UBound(abytln) For inti = LBound(abytln) To (intMax \ 2) Step 2 intNew = intMax - inti 1 Обмен первыми байтами. bytTemp = abytln(inti) abytln(inti) = abytln(intNew - 1) abytln(intNew - 1) = bytTemp
Обмен вторыми байтами. bytTemp = abytln(inti + 1) abytln(inti + 1) = abytln(intNew) abytln(intNew) = bytTemp Next inti dhReverse = abytln End If End Function
Иногда целесообразно скопировать строку в байтовый массив, провести в нем необходимую обработку, а затем скопировать результат обратно в строку. Копировать строку в байтовый массив очень просто: abytln = strin
В результате переноса текста в байтовый массив он приобретает вид, как слово ’’Hello” на рис. 1.2. Как видите, массив состоит из десяти байтов, пронумерованных от нуля до девяти, причем каждая буква исходного слова занимает по два байта.
Рис. 1.2. Строка "Hello", преобразованная в байтовый массив
Чтобы выполнить реверсию, dhReverse продвигается по массиву, работая одновременно с двумя байтами:
For inti = LBound(abytln) To (intMax \ 2) Step 2
’ (Фрагмент программы опущен) Next inti
В данном случае цикл выполняется от 0 до 4 с шагом 2 (значение intMax равно 9, поэтому intMax \ 2 равно 4). Как видно из рис. 1.2, на первом шаге цикла программа меняет местами нулевой и восьмой байты, а затем первый и девятый. На следующем шаге переставляются байты второй и шестой, а затем третий и седьмой. Наконец, программа меняет местами байты четвертый и пятый, а затем пятый и четвертый (лишняя операция, однако выполнить ее быстрее, чем пропустить этот шаг, рассматривая специальный случай для строк с нечетным количеством символов).
)Pv|Tl Совет: Программно перестановка двух элементов местами всегда состоит I I У из трех шагов. Для этого необходимо первый элемент скопировать во врс- менную переменную, второй — на место первого, а первый (находящийся во временной переменной) — на место второго. Эти шаги выполняются независимо от языка, на котором написана программа. Если вам не приходилось поступать подобным образом, данный пример будет для вас полезен.
Если вы впервые сталкиваетесь с такой задачей, как реверсия строки, можно написать простую функцию, подобную приведенной в примере 1.12. Хотя эта функция существенно короче, чем функция dhReverse, на самом деле она выполняется гораздо медленнее. Почему? Применение строковых функций в VBA является весьма дорогостоящим удовольствием с точки зрения времени, требуемого для их выполнения. С другой стороны, работа с байтовыми массивами происходит быстро. В данном случае программа может работать отдельно с каждым байтом, поэтому целесообразнее использовать байтовый массив, нежели производить посимвольную выборку посредством функции Mid. При написании функций, предназначенных для обработки данных, вам следует решить, можно ли в вашем случае выполнять побайтовую обработку с применением байтового массива или необходимо осуществлять посимвольную обработку, вызывая более медленные строковые функции.
Пример 1.9. Более медленный вариант функции
Function SlowReverse(strin As String) As String
Dim strOut As String
Dim inti As Integer
For inti ^ Len(strin) To 1 Step -1 strOut = strOut & Mid$(strin, inti, 1) Next inti
SlowReverse =«strOut
End Function
Кроме реверсии строк в процессе выполнения функции dhRinstr, которая описана ниже в этой главе, как еще использовать функцию dhReverse? Например, если вы хотите проверить, является ли строка палиндромом, можно написать такое выражение:
If dhTranslate(strText, " ", "") = _ dhReverse(dhTranslate(strText, " ", "")) Then
’ Вы знаете, что строка strText является палиндромом.
End If
Представление десятичных чисел римскими цифрами
Если вашей задачей является создание юридической документации или при выполнении работы вы сталкиваетесь со ссылками на источники, вам, вероятно, потребуется представить целые десятичные числа римскими. Хотя подобная потребность возникает нечасто, все же на этот случай лучше иметь готовое программное решение.
Функция dhRoman из примера 1.10 может принимать в качестве аргумента целое число из диапазона от 1 до 3999 (у римлян не было понятия нуля), и возвращает это значение, представленное римскими цифрами. Например:
dhRoman(1997)
возвращает "mcmxcvii", а
dhRoman(3999)
— "MMMCMXCIX".
Пример 1.10. Представление чисел римскими цифрами
Function dhRoman(ByVai intValue As Integer) As String * Представление десятичного числа из ' диапазона от 1 до 3999 римскими цифрами. Dim varDigits As Variant Dim intPos As Integer Dim intDigit As Integer Dim strTemp As String
Создание массива римских цифр.
varDigits = Array("I"f "V", "X", "L", "C", "D", ”M”) intPos = LBound(varDigits) strTemp = "” Do While intValue > 0 intDigit = intValue Mod 10 intValue = intValue \ 10 Select Case intDigit Case 1 strTemp = varDigits(intPos) & strTemp Case 2 strTemp = varDigits(intPos) & _ varDigits(intPos) & strTemp Case 3 strTemp = varDigits(intPos) & _ varDigits(intPos) & varDigits(intPos) & strTemp Case 4
strTemp = varDigits(intPos) & _ varDigits(intPos + 1) & strTemp Case 5 strTemp = varDigits(intPos + 1) & strTemp
Case 6 strTemp = varDigits(intPos + 1) & _ varDigits(intPos) & strTemp Case 7 strTemp = varDigits(intPos + 1) & _ varDigits(intPos) & varDigits(intPos) & strTemp Case 8 strTemp = varDigits(intPos + 1) & _ varDigits (intPos) & varDigits(intPos) & _ varDigits(intPos) & strTemp Case 9 strTemp = varDigits(intPos) & _ varDigits(intPos + 2) & strTemp End Select intPos = intPos + 2 Loop dhRoman = strTemp End Function
Каким образом работает функция dhRoman? Как вам, наверное, известно, все числа из диапазона от 1 до 3999 представляются посредством семи римских цифр: I, V, X, L, С, D и М. Цифры I, X, С и М соответствуют 1, 10, 100 и 1000; V, L и D — 5, 50
и 500. Программа выполняет цикл по всем цифрам исходного числа справа налево, поочередно отделяя их при помощи оператора Mod:
Do While intValue > 0
intDigit = intValue Mod 10
intValue = intValue \ 10
(Фрагмент программы опущен)
intPos = intPos + 2
Loop
На каждом шаге цикла переменная intDigit содержит крайнюю правую цифру числа, а переменная intValue становится на одну цифру короче. Например, в следующей таблице представлено изменение значений этих двух переменных при обра-
ботке функцией dhRoman числа 1234.
IntValue
intDigit
intPos
Символ
123
4
0
I
12
3
2
X
1
2
4
С
0
1
6
М
Кроме того, переменная intPos указывает на то, какой элемент массива используется для формирования строки по мере перемещения цикла программы по разрядам единиц, десятков, сотен и тысяч в числе.
Исходя из значения intDigit, посредством конструкции Select Case программа осуществляет выбор символов для формирования выходной строки. (Функция dhRoman конструирует выходную строку справа налево, добавляя по мере работы элементы к строке слева.) Например, в строке ”1234" функция dhRoman выбирает цифру 4 при значении переменной intPos, равном 0. В этом случае программа использует
strTemp = varDigits(intPos) & _ varDigits(intPos + 1) & strTemp
Так как intPos равно 0, в результате получается строка "IV" (varDigits (0) & varDigits (1)). Если бы цифра 4 находилась в разряде сотен (например, при преобразовании числа 421), то значение переменной intPos было бы равно 2. При этом использовалось бы выражение varDigits (4) & varDigits (5), и результатом для этой цифры была бы строка "CD".
Вам не придется пользоваться данной функцией каждый день. Однако когда вам все же понадобится представить число римскими цифрами, она будет ждать вас.
Кодирование и декодирование текста при помощи XOR-кодировки
Если для вашей программы понадобится простой метод кодирования текста, функция dhXORText (пример 1.11) может сделать то, что вам нужно. Она как кодирует, так и декодирует текстовые строки. Действительно, обе задачи выполняются одной процедурой.
Чтобы закодировать текст, функции dhXORText следует передать кодируемый текст и пароль, служащий маской для кодирования. Для декодирования текста функцию dhXORText нужно запустить снова с теми же параметрами. Например, следующий фрагмент: dhXORText(dhXORText("This is a test", "Password"), "Password")
возвращает значение "This is a test", тот же самый текст, закодированный, а потом декодированный.
Пример 1.11. Использование оператора XOR для кодирования текста
Function dhXORText (strText As String, strPWD As String) As String ’ Кодирование или декодирование строки при помощи оператора XOR. Dim abytTextO As Byte Dim abytPWD () As Byte Dim intPWDPos As Integer Dim intPWDLen As Integer Dim intChar As Integer abytText = strText abytPWD = strPWD
intPWDLen=LenB(strPWD) For intChar = 0 To LenB (strText) - 1
Получение следующего значения в диапазрне от 0 до intPWDLen - 1. intPWDPos = (intChar Mod intPWDLen) abytText (intChar) = abytText(intChar) Xor abytPWD(intPWDPos) Next intChar dhXORText = abytText End Function
Для кодирования функция dhXORText использует встроенный оператор XOR (исключающее ИЛИ), который сравнивает каждый бит двух выражений и вычисляет результат по следующим правилам:
Первый бит
Второй бит
Результат
1
1
0
1
0
1
0
1
1
0
0
0
Почему используется именно XOR? Дело в том, что этот оператор обладает одним очень важным свойством — если применить оператор XOR к двум значениям, а затем к результату и любому из исходных значений, вы вновь получите второе исходное значение. Вот почему функция dhXORText применяет его. Для того чтобы убедиться в вышесказанном, зададим в качестве первого байта исходного текста значение 74, а в качестве первого байта пароля — значение ПО.
Выражение
74 XOR 110
дает в результате значение 36, которое становится первым байтом закодированного текста. Теперь выполним оператор XOR еще раз:
36 XOR 110
и снова получим значение первого байта исходного текста 74. Если повторить эту операцию для всех байтов текста, будет произведено его кодирование и декодирование.
Для выполнения этой работы функция dhXORText копирует исходную строку и пароль в массивы байтов. (Другой пример использования байтовых массивов приведен ранее в разделе "Реверсия строк".) Теперь достаточно выполнить цикл по всем байтам массива, который содержит исходную строку, повторяя пароль снова и снова до тех пор, пока не будет достигнут конец исходного текста. Для каждого байта исходной строки и соответствующего байта пароля с помощью оператора XOR формируется байт результирующей строки.
На рис. 1.3 представлен небольшой пример, в котором в качестве исходной строки используется текст "Hello Tom", а в качестве пароля — строка "ab". Над каждым байтом исходной строки и соответствующим байтом пароля выполняется оператор XOR, причем пароль повторяется до тех пор, пока не будут обработаны все символы исходного текста.
Исходный текст:
H
0 e
0
I ' 0
I
0
o,O
о I T
0
о । 0 , m
0
72
0 '101
0
1081 0
108
0
111; 0
32
Q i 84
0
111; о 109
0
Пароль (повторяется):
a
0 1 b
0
a 0
b
0
a
0
b
0 a
0
b 0 a
0
97
0 ; 98
0
97 1 0
98
0
97
0
98
0 97
0
98 i 0 97
0
Результат:__
Рис. 1.3. Выполнение XOR над каждым байтом исходной строки и повторяющегося пароля
Программа выполняет цикл по всем символам исходной строки.
For intChar = 0 То LenB(strText) - 1
* Фрагмент программы опущен.
Next intChar
Сложность состоит в том, чтобы правильно определить байт пароля для выполнения операции XOR с очередным байтом исходной строки: для этого в программе используется оператор Mod. Он возвращает остаток отделения первого операнда на второй, причем результат, несомненно, будет числом в диапазоне от нуля до значения, меньшего на единицу, чем второй операнд. Короче говоря, это соответствует чередованию байтов пароля, как показано в табл. 1.7 (нулевые байты игнорируются). При длине пароля пять байтов столбец Позиция Mod 2 (он в этом случае мог бы иметь имя Позиция Mod 5) содержал бы значения от 0 до 4, которые повторялись бы столько раз, сколько необходимо.
* Получение следующего значения в диапазоне от 0 до intPWDLen - 1. intPWDPos = (intChar Mod intPWDLen) abytText(intChar) = abytText(intChar) Xor abytPWD(intPWDPos)
Таблица 1.6. Этапы кодирования текста примера
Символ исходной строки
Позиция
Позиция Mod 2
Символ пароля
XOR
Н (72)
0
0
а (97)
41
с (I01)
1
1
b (98)
7
I (108)
2
0
а (97)
13
1 (108)
3
1
b (98)
14
о(1Н)
4
0
а (97)
14
(32)
5
1
b (98)
66
Т (84)
6
0
а (97)
53
о(1Н)
7
1
b (98)
13
m (109)
8
0
а (97)
12
Совет: Как вы, наверное, поняли, результат работы функции dhXORText зависит от регистра символов пароля, и тут уж ничего не поделаешь. Предупреждайте пользователей о том, что пароли в вашей программе необходимо вводить, учитывая регистр символов.
* .-..........................mm......^^ ..........
Внимание: Хотя алгоритм кодирования с оператором XOR не является абсолютно надежным, чем длиннее заданный пароль, тем меньше шансов специалисту по "взлому" программ его подобрать. Короткий пароль "аЬ" использовался выше лишь в качестве примера. Пароли должны быть длиной, по крайней мере, четыре-пять символов — чем длиннее, тем лучше.
Дополнение строки слева или справа до заданной ширины
При формировании списка, например, телефонных номеров, вам может понадобиться дополнять номера телефонов точками слева, так чтобы список выглядел следующим образом:
(310) 123-4567 555-1212
Кроме того, иногда требуется заполнять числовые поля слева нулями, так, чтобы, например, все числа были шестиразрядными, т.е. строка "1234" должна быть преобразована в строку "001234". Может понадобиться также сформировать поток данных, имеющих фиксированную длину, путем заполнения свободных позиций пробелами. В подобных случаях нужна функция, которая дополняла бы строку справа или слева заданными символами. Эта задача выполняется двумя простыми функциями dhPadLeft и dhPadRight (пример 1.12).
При вызове любой из них в качестве аргументов задаются строка, содержащая исходный текст, целое число, определяющее длину результирующей строки, и символ-заполнитель (последний задавать необязательно). Если последний аргумент не задан, функция использует в качестве символа-заполнителя пробел.
Например: dhPadLeft("Name", 10, ". ") возвращает "Name" (слово "Name", предваряемое шестью точками). dhPadRight ("Hello", 10) возвращает "Hello " (слово "Hello", после которого следуют пять пробелов).
^1 Примечание: Ни dhPadLeft, ни dhPadRight не обрезают исходную стро- V ку. Если длина заданной строки больше указанного вами значения строки результирующей, программа возвратит исходную строку без изменений.
Пример 1.12. Дополнение строки символами справа или слева
Function dhPadLeft(strText As String, intWidth As Integer, _
Optional strPad As String = " ") As String
* Дополнение строки strText слева таким образом, чтобы
результирующая строка насчитывала не менее intWight символов.
' Если строка strText длиннее, чем значение intWight, просто
* возвращается исходная строка. Если strPad содержит несколько
символов, в качестве символа-заполнителя используется только первый. If Len(strText) > intWidth Then
dhPadLeft = strText
Else
dhPadLeft = Right$(String(intWidth, strPad) & _ strText, intWidth)
End If
End Function
Function dhPadRight(strText As String, intWidth As Integer, _ Optional strPad As String = " ") As String
* Дополнение строки strText справа таким образом, чтобы
результирующая строка насчитывала не менее intWight символов.
Если строка strText длиннее, чем значение intWight, просто
* возвращается исходная строка. Если strPad содержит несколько
символов, в качестве символа-заполнителя используется только первый.
If Len(strText) > intWidth Then dhPadRight = strText
Else
dhPadRight = Left$ (strText & _ String(intWidth, strPad), intWidth)
End If
End Function
Обе функции используют одну и ту же методику дополнения исходных строк: они создают строку, состоящую из символов-заполнителей, количество которых равно числу символов в результирующей строке, присоединяют эту строку к исходной слева или справа, а затем при помощи функций Left$ или Right$ усекают результирующую строку до требуемого размера. Например, при вызове функции dhPadLeft следующим образом:
dhPadLeft("123.45", 10, "$")
программа формирует строку из десяти знаков доллара и присоединяет ее слева к исходной строке. Затем посредством функции Right$ строка укорачивается:
Right$ ("$$$$$$$$$$123.45", 10)
' возвращает "$$$$123.45"
Сравнение строк при помощи алгоритма Саундекса
Задолго до изобретения компьютеров люди, которым приходилось работать с информацией, содержащей имена, сталкивались с трудностями при написании фамилий. Поэтому был нужен способ группировки имен по фонетическому, а не по орфографическому принципу. Алгоритм, представленный в настоящем разделе, основан на алгоритме Рассела Саундекса — стандартной методике, используемой во многих приложениях баз данных.
Внимание: Алгоритм Саундекса разработан для фамилий и надежно работает только с ними. Его можно использовать и для произвольных строк, однако эффективность алгоритма падает с увеличением длины текста. Он предназначен для сопоставления различных вариантов написания близких по звучанию фамилий, и его мощность распознавания максимальна для коротких слов, содержащих три и более согласных букв.
Алгоритм Саундекса основан на таких предположениях:
многие согласные буквы английского языка звучат похоже;
гласные буквы влияют на общее ’'звучание” фамилии не так сильно, как согласные;
самой важной является первая буква фамилии;
четырехсимвольное представление является оптимальным для сравнения двух фамилий.
Например, три следующих примера возвращают строку "Р252":
dhSoundex("Paszinslo") dhSoundex("Pacinslo") dhSoundex ("Pe j inslo")
Три приведенные варианта сложной фамилии различаются орфографически, однако все они возвращают одинаковую Саундексову строку. При совпадении первых букв у вас хорошие шансы установить соответствие, используя преобразование Саундекса.
Идея заключается в том, что когда вы пытаетесь определить местоположение фамилии, вы запрашиваете ее у пользователя, преобразуете в представление Саундекса и сравниваете с Саундексовыми представлениями фамилий в вашей базе данных. В результате пользователю предоставляется список возможных соответствий, из которого он может выбрать правильный вариант.
Алгоритм Саундекса включает следующие шаги:
Первая буква строки сохраняется в исходном виде.
Остальные символы кодируются с использованием информации из табл. 1.7.
Повторяющиеся значения, т.е. символы с одинаковыми значениями (табл. 1.7) пропускаются, если они не разделены одним или несколькими символами-разделителями (символами со значением 0).
После того как Саундексова строка будет содержать четыре символа, поиск прекращается.
Если Саундексова строка содержит менее четырех символов, она дополняется до четырех конечными нулями.
Функция dhSoundex, которая приведена в примере 1.13, выполняет эти операции при создании Саундексового представления исходной строки.
Таблица 1.7. Значения для символов в Саундексовой строке
Буква
Значение
Комментарий
W,H
Игнорируются
A,E,1,O,U,Y
0
Эти буквы, хотя и удаляются из результирующей строки, служат разделителями между значимыми согласными
B,P,F,V
1
C,G,J,K,Q,S,X,Z
2
D,T
3
L
4
M,N
5
R
6
Пример 1.13. Преобразование строки символов в эквивалентное Саундексово представление
Const dhcLen = 4
Function dhSoundex(ByVai strin As String) As String ' Формирование эквивалентной Саундексовой * строки для исходного текста.
Dim strOut As String
Dim inti As Integer
Dim intPrev As Integer
Dim strChar As String * 1
Dim intChar As Integer
Dim fPrevSeparator As Boolean
strOut = "" strin = UCase(strin) fPrevSeparator = True
strOut = Left$(strin, 1)
For inti = 2 To Len(strin)
Если результирующая строка заполнена, происходит выход из программы.
If Len(strOut) >= dhcLen Then Exit For
End If
Поочередная выборка каждого символа.
Если символ является буквой, он обрабатывается.
strChar = Mid$(strin, inti, 1)
If dhlsCharAlpha(strChar) Then 1 Преобразование символа в его код. intChar = CharCode(strChar) ' Если символ не является пустым и не совпадает с ' предыдущим, он присоединяется к концу строки. If (intChar > 0) Then
If fPrevSeparator Or (intChar <> intPrev) Then strOut = sErOuE & inEChar intPrev = inEChar
End If End If fPrevSeparator = (intChar = 0) End If Next inti * Возвращает строку, дополненную справа нулями. dhSoundex = dhPadRight(sErOut, dhcLen, ”0”) End Function
Private Function CharCode(strChar As String) As Integer Select Case strChar
Case "A”, "E", ”1", "0", "U", "Y" CharCode = 0
Case "C", "G", "J”, "K", "Q", "S", "X”, "Z" CharCode = 2
Case "D", "T" CharCode = 3
Case "M", "N" CharCode = 5
Case "B", "F", ”P”, "V"
CharCode = 1
Case ”L” CharCode = 4 Case "R" CharCode = 6
Case Else CharCode = -1 End Select End Function
Что теперь делать с найденной вами Саундексовой строкой, соответствующей заданной фамилии? Можно сформировать проградуированную шкалу соответствия. Это имеет смысл, поскольку, возможно, вам не требуется точное совпадение, а достаточно знать, насколько одна фамилия похожа на другую. Функция dhSoundsLike (пример 1.14) предоставляет общий метод определения степени такого соответствия. При вызове данной функции в качестве аргументов задаются две строки, пока не преобразованные по алгоритму Саундекса. Функция dhSoundsLike возвращает число от О до 4 (4 соответствует максимальному совпадению), которое указывает уровень соответствия строк. Если хотите, можно задать две Саундексовы строки, и dhSoundsLike уже не будет выполнять преобразование исходных строк в Саундексовы. В этом случае следует установить необязательный параметр flsSoundex в значение True.
Пример 1.14. Применение функции dhSoundsLike для сравнения двух Саундексовых строк
Function dhSoundsLike(ByVai strlteml As String, _
ByVai strltem2 As String, _
Optional flsSoundex As Boolean = False) As Integer
* Возвращает значение из диапазона от 0 до 4, которое указывает на степень
' совпадения Саундексовых представлений двух исходных строк. Dim inti As Integer If Not flsSoundex Then
strlteml = dhSoundex(strlteml) strltem2 = dhSoundex(strltem2) End If For inti = 1 To dhcLen
If Mid$(strlteml, inti, 1) <> Mid$(strltem2, inti, 1) Then Exit For
End If
Next inti
dhSoundsLike = (inti - 1) End Function
Трудно представить себе более простой метод для выполнения этой задачи. Функция dhSoundsLike поочередно проходит по всем четырем символам обеих Саундексовых строк. Пока совпадения встречаются, она идет дальше. Как только она обнаруживает несовпадение двух символов, происходит выход из цикла и возвращается количество совпадающих символов. Чем больше символов совпадают, тем выше уровень соответствия.
Для проверки работы функции dhSoundsLike можете выполнить следующие примеры:
dhSoundsLike("Smith", "Smitch")
возвращает значение 3, или
dhSoundsLike ("S125", "S123", True)
который также возвращает 3. Вы, вероятно, не будете вызывать функцию dhSoundsLike со строковыми литералами. Скорее всего, вы используете ее, задавая строковые переменные и сравнивая их содержимое.
Примечание: Существуют различные варианты данного алгоритма, которые уступают по эффективности описанному выше. Эти варианты (хотя они и несколько проще) не учитывают повторяющихся согласных, разделенных гласными буквами, что слишком упрощает формирование Саун- дексовой строки для данной фамилии. Представленный здесь алгоритм сложнее, однако дает более надежные результаты, которые соответствуют результатам, получаемым от функции Soundex, используемой Microsoft SQL Server.
Работа с подстроками
В завершение этой главы мы представляем несколько методов синтаксического анализа и выделения подстрок из строк. Вы узнаете, как решить следующие задачи:
Возвращение определенного слова строки по индексу
Выборка первого или последнего слова в строке
Обнаружение последнего вхождения подстроки в строку
Возвращение определенного слова строки по индексу
Из всех функций, описанных в этой главе, функция dhExtractString нашла более широкое применение в наших приложениях. Она позволяет выбрать фрагмент строки, задав разделитель (или несколько символов-разделителей), исходя из положения фрагмента в строке. Вместо написания собственных программ для анализа строк вы можете использовать функцию dhExtractString, которая позволяет выделить нужный вам фрагмент строки. Например, если нужно рассмотреть следующую строку (взятую из гипотетического INI-файла):
ItemsToBuy=Milk,Bread,Peas
и выбрать по отдельности названия продуктов, можно либо написать программу для анализа строки, либо вызвать функцию dhExtractString в цикле:
Function TestExtract(strlniText As String)
Dim inti As Integer
Dim strText As Spring
inti = 2
Do While True
strText = dhExtractString(strlniText, inti, " = ,")
If Len(strText) = 0 Then Exit Do
End If
Debug.Print strText inti = inti + 1
Loop
End Function
К использованию функции dhExtractString подходите творчески. Можно вызвать ее один раз с одним набором разделителей, а затем вызвать снова с другими разделителями. Например, предыдущую задачу можно было бы решить, выделив сначала текст справа от знака равенства в виде отдельного фрагмента:
strVals = dhExtractString(strlniText, 2, " = ")
' Возвращает "Milk"* Возвращает "Bread"* Возвращает "Peas"' Возвращает "Milk"* Возвращает "Bread"* Возвращает "Peas"Затем можно выделить части строки strVals, разделенные запятыми:
' Возвращает "Milk"
* Возвращает "Bread"
* Возвращает "Peas"
' Возвращает "Milk"
* Возвращает "Bread"
* Возвращает "Peas"
strlteml = dhExtractString(strVals, 1, ",") strltem2 = dhExtractString(strVals, 2, ”,”) strltem3 = dhExtractString(strVals, 3, ",")
Таким образом, можно задавать как один символ-разделитель, так и список разделителей.
Полностью функция dhExtractString приведена в примере 1.15.
Внимание: Результат выполнения функции dhExtractstring может оказаться обманчивым. Если исходная строка содержит два символа-разделителя подряд, функция dhExtractstring воспринимает данный фрагмент как пустую строку, ограниченную этими двумя символами. Это означает, что вы не можете вызывать в цикле dhExtractstring, задав в качестве условия выхода из цикла возврат пустой строки (за исключением тех случаев, когда вы уверены, что исходная строка не содержит несколько разделителей подряд). Сначала следует вызвать функцию dhCountin, которая позволит определить, сколько подстрок содержится в строке, а затем выполнить цикл соответствующее число раз. Пример использования подобной методики приведен в данной главе ниже.
Совет: Если не задать функции dhExtractstring разделитель или список разделителей, по умолчанию она использует стандартные текстовые символы-разделители, определяемые константой dhcDelimiters. Конечно, вы можете изменить этот набор, модифицировав константу в программе.
Пример 1.15. Выделение заданной подстроки
Function dhExtractstring(ByVai strin As String, _
ByVai intPiece As Integer, _
Optional ByVai strDelimiter As String = dhcDelimiters) As String
Выделяет лексемы из списка с разделителями.
Список содержится в переменной strln,
а параметр intPiece определяет, какой из фрагментов выделять.
Dim intPos As Integer
Dim intLastPos As Integer
Dim intLoop As Integer
Dim intPosl As Integer
intPos = 0
intLastPos = 0 intLoop = intPiece
' Если имеется несколько видов разделителей,
* все они заменяются первым из списка.
If Len(strDelimiter) > 1 Then strin = dhTranslate (strln, strDelimiter, Left$(strDelimiter, 1)) End If
Do While intLoop > 0 intLastPos = intPos intPosl = InStr (intPos + 1, strln, Left$(strDelimiter, 1)) If intPosl > 0 Then intPos = intPosl intLoop = intLoop - 1
Else intPos = Len(strln) + 1 Exit Do End If
Loop
' Если искомой строки не обнаружено, 1 данная итерация не была первой
(в этом случае значения intLoop и intPiece были бы равны)
и intLoop > 1, то вы закончили просмотр лексем, не найдя нужную.
Это означает, что номер лексемы был слишком большим. ' В таком случае возвращается "".
If (intPosl = 0) And. (intLoop <> intPiece) And (intLoop > 1) Then dhExtractString = "" Else dhExtractString = Mid$(strin, intLastPos + If _ intPos - intLastPos - 1) End If End Function
Прежде всего, функция dhExtractString заменяет различные разделители первым элементом из списка. Таким образом, если задан набор разделителей, например запятая, пробел, дефис, функция вначале заменяет в исходной строке все эти символы запятой: If Len(strDelimiter) > 1 Then strin = dhTranslate(strin, strDelimiter, _ Lefts (strDelimiter, 1)) End If
Затем функция dhExtractString осуществляет просмотр строки в цикле до тех пор, пока не будет обнаружен нужный разделитель. Если вы задали поиск четвертой лексемы в исходной строке, цикл продолжается до тех пор, пока не будет обнаружен четвертый разделитель. Кроме того, функция запоминает позицию последнего найденного разделителя (переменная intLastPos) и позицию текущего разделителя (переменная intPos). Если на данном этапе поиска посредством inStr разделитель не найден (функция возвращает 0), программа устанавливает текущую позицию на один символ правее конца исходной строки и выходит из цикла:
Do While intLoop > 0 intLastPos = intPos intPosl = InStr(intPos + 1, strin, LeftS(strDelimiter, 1)) If intPosl > 0 Then intPos = intPosl intLoop - intLoop - 1 Else intPos = Len(strin) + 1 Exit Do End If Loop
Определить, возвращать пустую строку или фрагмент исходной строки, достаточно сложно (возможно, даже слишком сложно). Для того чтобы функция dhExtractString возвратила пустую строку, должны одновременно выполниться три условия:
intPos 1=0 — указывает на то, что в исходной строке разделители закончились до того, как программа остановилась в поисках лексемы. Конечно, это может произойти, если вы затребовали последнюю лексему в строке (в таком случае после нее просто больше не может быть разделителей, и значение intPosl будет равно нулю).
intLoopointPiece — значение переменной intLoop после обнаружения очередного разделителя в исходной строке уменьшается на единицу, начиная со значения intPiece. Если значение intLoop равно intPiece, значит, в строке имеется единственная лексема, а разделителей нет вообще. В этом случае функция dhExtractString возвращает не пустую, а исходную строку полностью.
intLoop > 1 — если значение переменной intLoop равно единице, значит, цикл прошел все разделители в исходной строке, и вы, возможно, выбрали в ней последнюю лексему. Если значение переменной intLoop больше единицы, то указан номер лексемы, который превышает количество лексем в исходной строке. (Например, вы запросили шестое слово в предложении, которое состоит всего из четырех слов; в этом случае функция должна возвратить пустую строку, что она и сделает, поскольку оба другие условия также выполняются.)
Если все три условия не выполняются одновременно, программа выделяет подстроку, начинающуюся с позиции intLastPos + 1 и содержащую intPos - intLastPos - 1 символов:
If (intPosl = 0) And (intLoop <> intPiece) And (intLoop > 1) Then dhExtractString = ””
Else
dhExtractString = Mid$(strin, intLastPos + 1, _
intPos - intLastPos - 1)
End If
Совет: Следует помнить о том, что функция dhExtractString восприни- I I И мает наличие последовательных разделителей так, будто между ними находится пустая лексема. Например, запросив вторую лексему из строки "This;; is; a; test" с символами точки с запятой в качестве разделителей, вы получите в результате пустую строку.
Функция dhExtractString используется для работы некоторых других функций, описанных в этом разделе. Мы уверены, что она вам очень пригодится при написании любой программы, связанной с извлечением фрагментов текстовых строк.
Выборка первого или последнего слова в строке
Каждая из двух функций — dhFirstWord и dhLastWord — разбивает исходную строку на два фрагмента: выбранное слово и оставшуюся часть строки. При вызове функции dhFirstWord (пример 1.16) возвращается первое слово исходной строки, а необязательному параметру присваивается оставшаяся часть строки. При вызове функции dhLastWord (пример 1.17) возвращается последнее слово исходной строки, а необязательному параметру присваивается предшествующая часть строки. Например, при выполнении следующего фрагмента программы:
Dim strRest As String
dhFirstWord("First words are mighty important", strRest)
возвращается слово "First", а переменной strRest присваивается строка " words are mighty important" (обратите внимание на то, что она начинается с пробела). С другой стороны:
Dim strRest As String
dhLastWord("First words are mighty important", strRest)
возвращает слово "important”, а переменной strRest присваивается строка "First words are mighty " (обратите внимание на то, что она заканчивается пробелом).
Пример 1.16. Возврат первого слова строки
Function dhFirstWord(ByVai strText As String, _ Optional ByRef strRest As String = ”") As String ' Выборка первого слова строки.
* Переменной strRest присваивается оставшаяся часть строки. Dim strTemp As String * Это просто!
* Выборка первого слова.
strTemp = dhExtractString(strText, 1)
* Выделение остатка строки после первого слова
и присвоение его переменной strRest. strRest = Mid$(strText, Len(strTemp) + 1) * Возврат первого слова.
dhFirstWord = strTemp
End Function
Пример 1.17. Возврат последнего слова строки
Function dhLastWord(ByVai strText As String, _
Optional ByRef strRest As String = "") As String
* Выборка последнего слова строки.
Переменной strRest присваивается оставшаяся часть строки.
Dim intCount As Integer
Dim strTemp As String
* Это уже не так просто.
* Определяется количество слов, а затем выделяется последнее слово. intCount = dhCountWords (strText)
strTemp = dhExtractString(strText, intCount)
* Выделение остатка строки до последнего слова
* и присвоение его переменной strRest.
strRest = Left$(strText, Len(strText) - Len(strTemp)) dhLastWord = strTemp
End Function
Функция dhFirstWord проще, поскольку она может использовать функцию dhExtractString, которая описывалась ранее в этой главе. Прежде всего, она выделяет первое слово:
strTemp = dhExtractString(strText, 1)
Затем она присваивает остаток строки переменной strRest:
strRest = Mid$(strText, Len (strTemp) + 1)
—^i Примечание: В функциях dhFirstWord и dhLastWord не требуется явно | проверять, была ли задана переменная для параметра strRest. Если вы не • задали данный параметр при вызове функции, VBA просто не передает ничего обратно. Это не может нанести вреда и избавляет от выполнения дополнительной логической операции проверки состояния параметра.
Функция dhLastWord несколько сложнее, поскольку программа должна сначала определить количество слов в строке, а затем выделить правильное слово:
intCount = dhCountWords(strText) strTemp = dhExtractString(strText, intCount)
Получив последнее слово, программа выделяет предшествующую часть строки и присваивает ее переменной strRest:
strRest = Left$(strText, Len(strText) - Len(strTemp))
Конечно, имея столь богатый запас строковых функций, вы можете создать собственные альтернативные решения этих задач. Возможно, вам более интересны другие методы, и ваши решения окажутся даже более эффективными!
Поиск последнего вхождения подстроки в строку
VBA имеет встроенную функцию instr, позволяющую найти первое вхождение заданной подстроки в некоторую строку. Однако она не поможеФ в том случае, когда нужно найти последнее вхождение подстроки. Такую возможность предоставляет функция dhRlnsrt.
Например, при анализе имен файлов вам требуется найти последнее вхождение символа обратной косой черты в строку. Для этого вы можете воспользоваться функцией dhRlnsrt. Если вы объявили переменную strFileName, которая содержит полный путь, например "C:\winnt\system32\other\etc\lmhost", то, вызвав функцию таким образом:
dhRlnsrt(strFileName, ”\”)
вы получите в результате число 28, которое соответствует позиции последнего символа обратной косой черты в строке.
Кроме того, можно задать значение позиции в строке, до которой вы хотите осуществлять поиск. Допустим, вы хотите найти предпоследний символ обратной косой черты (т.е. символ, предшествующий символу, который найден в предыдущем примере). Тогда следует видоизменить вызов функции:
dhRlnsrt(strFileName, ”\”, 27)
В этом случае результатом будет число 24, соответствующее позиции предпоследнего символа обратной косой черты в строке.
Функция dhRlnsrt (пример 1.18) допускает задание четырех параметров:
strSearch — определяет строку, где будет производиться поиск;
strSought — задает искомую подстроку;
intEndAt (необязательный параметр) — задает последнюю позицию в строке, до которой будет осуществляться поиск (если значение этого параметра не указано (или равно 0 — значение по умолчанию), поиск будет охватывать всю строку);
f CaseSensitive (необязательный параметр) — указывает на то, будет ли поиск чувствительным к регистру символов (по умолчанию используется значение False).
Пример 1.18. Поиск последнего вхождения подстроки
Function dhRInsrt(ByVai strSearch As String, ByVai strSought Ad String, _ Optional intEndAt As Integer = 0, _ Optional fCaseSensitive As Boolean = False) As Integer
’ Обратная версия InStr:
’ возвращает позицию последнего вхождения строки strSought в
* строку strSearch. Если значение intEndAt больше нуля, оно указывает ’ на последнюю просматриваемую позицию строки.
Dim intMode As Integer Dim intPos As Integer
Определение режима сравнения. If fCaseSensitive Then intMode = vbBinaryCompare Else intMode = vbTextCompare End If
Если при вызове указана конечная позиция * поиска, исходная строка обрезается. If intEndAt > 0 Then
strSearch = Left$ (strSearch, intEndAt) End If ’ Реверсия обеих строк.
strSearch = dhReverse(strSearch) strSought = dhReverse(strSought)
* Поиск реверсной искомой строки в реверсной исходной строке. intPos = InStr (1, strSearch, strSought, intMode) ’ Если текст искомой строки обнаружен, * номер позиции пересчитывается. If intPos > 0 Then intPos = Len(strSearch) - intPos + 1 End If dhRInstr = intPos End Function
Хотя программа, реализующая функцию dhRInsrt, несколько необычна, она нс так уж и сложна. Вначале, как и некоторые другие функции, dhRInsrt устанавливает режим сравнения: If fCaseSensitive Then
intMode = vbBinaryCompare Else
intMode = vbTextCompare End If
Далее, если задано значение параметра intEndAt, функция обрезает просматриваемую строку:
If intEndAt > 0 Then strSearch = Left$ (strSearch, intEndAt) End If
Для проведения поиска функция dhRInstr выполняет реверсию обеих строк. Да, это несколько странный подход, однако, так как у нас уже имеется функция dhReverse, опа обеспечивает возможность использования встроенной функции
inStr — осуществив реверсию обеих строк, вы производите поиск реверсной искомой строки в реверсной исходной строке при помощи функции instr. Первое вхождение в реверсной строке соответствует последнему вхождению в исходной строке:
strSearch = dhReverse(strSearch)
strSought = dhReverse(strSought)
intPos = InStr (1, strSearch, strSought, intMode)
Если найденная позиция совпадения больше нуля, ее необходимо "перевернуть", т.е. чтобы получить смещение этой позиции от другого конца строки, следует вычесть номер позиции, в которой обнаружено совпадение, из полной длины строки:
’ Если текст искомой строки обнаружен, * номер позиции пересчитывается.
If intPos > 0 Then
intPos = Len(strSearch) - intPos + 1
End If
Такое решение, конечно, необычно, однако реверсия строк дает возможность вызывать встроенные функции. Эта идея достойна внимания — какой бы эффективной ни была ваша программа на VBA, встроенные функции работают быстрее. Не забывайте — встроенные функции написаны на языке C++, а ваша программа — нет. Используйте преимущества встроенных функций там, где это только возможно, и ваше приложение наверняка будет работать быстрее.
Резюме
По-видимому, ни в одном приложении, написанном на VBA, невозможно обойтись без выполнения операций над строками. В этой главе представлен обзор встроенных строковых функций VBA, а также большой список процедур, предоставляющих дополнительные функциональные возможности. В частности, здесь рассмотрены такие вопросы:
как VBA хранит и использует строки;
многие встроенные строковые функции и операторы для:
сравнения строк;
преобразования строк;
создания строк;
вычисления длины строк;
форматирования строк;
выравнивания строк;
поиска строк;
работы с фрагментами строк и их замены;
использования ANSI-кодов и байтов;
дополнительные функции для:
поиска и замены текста;
сбора информации о строках;
преобразования строк;
работы с подстроками.
ГЛАВА 2
Работа со значениями даты и времени
Что такое дата и как она хранится?
Встроенные функции VBA для работы с датами
Определение конкретной даты
Операции со значениями даты и времени
Работа с промежутками времени
В этой главе описаны операции со значениями даты и времени. Хотя VBA предоставляет пользователю богатый набор встроенных функций для работы со значениями даты и времени, существует множество программистских проблем, которые для своего решения требуют иных функций, нежели имеющиеся в VBA.
Что такое дата и как она хранится?
Оставив другие определения в стороне, с точки зрения VBA дата — это 8-байтовая величина с плавающей точкой, которая содержит информацию, указывающую на определенный момент времени. В частности, целая часть данной величины представляет собой количество дней, прошедших с 30 декабря 1899 года. В дробной части содержится информация о соответствующей части суток. Например, если сейчас 3:00 дня 22 мая 1999 года, то представление этого значения в VBA выглядит следующим образом: 36302.625, т.е. текущая дата является 36302 днем с 12/30/1899 года, а текущее время 3:00 дня составляет 625/1000 полных суток. Вообще говоря, вам не нужно заботиться о внутреннем механизме хранения дат; VBA легко выполняет преобразование из внутреннего формата с плавающей точкой в формат для внешнего отображения даты и наоборот.
Примечание: Мы используем принятый в США формат записи дат, т.е. месяц/день/год.
Дополнительные преимущества
Поскольку в VBA даты хранятся в виде чисел, это дает дополнительные преимущества, позволяя при необходимости обрабатывать даты как числовые значения в выражениях. Хотя VBA имеет специальную функцию DateAdd, которая подробно описана в разделе "Выполнение простых вычислений" этой главы, нет необходимости использовать ее, если вам нужно добавить определенное количество дней к заданному значению даты. Например, вы хотите получить завтрашнюю дату. В таком случае достаточно прибавить к текущей дате единицу:
dtmTomorrow = Date() + 1
Date — это встроенная функция VBA, которая возвращает составляющую даты (целую часть) текущего значения даты и времени, предоставляемого операционной системой Windows. Если прибавить к этому числу единицу, получится значение даты следующего дня.
Аналогичный механизм применим и для вычитания двух дат. Хотя VBA имеет специальную встроенную функцию DateDiff, которая вычисляет интервал между двумя значениями даты и времени, если вам нужно узнать количество дней между двумя датами, следует просто вычесть значение одной даты из значения другой. Например, чтобы определить количество дней между 5/22/1999 и 1/10/1999, можно воспользоваться следующим выражением:
intDays = #5/22/99# - #1/10/99#
После выполнения этой операции переменная intDays будет содержать значение 132, т.е. количество дней между 22 мая и 10 января.
Задание значений даты
Механизм выражений VBA способен "переваривать" даты в любом допустимом формате. Если заключить литерал даты между двумя символами # и отформатировать его, VBA "поймет", что вы имеете в виду.
VBA распознает следующие форматы даты:
#Janu^ry 1, 1999#
#Jan 1 1999#
#l-Jan-99#
#1 Jan 1999#
#1 1 99#
Совет: Для анализа введенного значения даты VBA использует международные настройки Windows. Конечно, это может привести к путанице, когда во введенной дате явным образом не видно, какая цифра обозначает день, а какая — месяц. (Откуда VBA знает, что, например, значение #5/1/99# соответствует 1 мая, а не 5 января?) Для того чтобы полностью исключить неоднозначность трактовки значений дат, особенно в тех случаях, когда ваше приложение должно работать в локализованных средах, придется отказаться от использования в программе литералов даты и применять функцию DateSerial (см. раздел "Объединение составляющих даты"). Эта функция принимает в качестве аргументов три значения (год, месяц и день определенной даты) и возвращает значение типа Date, представляющее указанную дату. При таком подходе вы никогда не столкнетесь с проблемами неправильной трактовки дат программами локализованных версий.
Преобразовывая данные других типов в формат даты, VBA сохраняет часть числа слева от десятичной точки (целую часть) как дату, а часть числа справа от десятичной точки (дробную часть) — как время. Например, если вы написали такую программу:
Dim dbl As Double Dim dtm As Date dbl = 3005 /12.6 dtm = dbl Debug.Print dbl Debug.Print dtm
то получите:
238.492063492063
8/25/1900 11:48:34 AM
Судя по результатам, 8/25/1900 — 238 день после 12/30/1899, a .4920634... соответствует приблизительно 11:48:34 утра.
Встроенные функции VBA для работы с датами
Хотя VBA предоставляет множество встроенных функций, среди них нет большого количества логических групп, так тесно связанных, как VBA-функции, выполняющие
операции со значениями даты и времени. Несколько следующих разделов содержат описания и примеры использования встроенных функций для решения простых задач. В оставшейся части главы рассмотрен процесс выполнения более сложных операций, требующих применения элементарных "строительных блоков".
Текущая дата и время
VBA имеет три встроенные функции, которые позволяют определить текущую дату и время на вашем компьютере. Эти функции — Now, Date и Time — опрашивают системный таймер и возвращают установленное текущее значение или его часть. Они не требуют Задания параметров, и их назначение описано в таблице ниже.
Функция
Возвращаемое значение
Now
Текущее значение даты и времени
Date
Часть значения, представляющая дату
Time
Часть значения, представляющая время
Может показаться, что эти функции отчасти дублируют друг друга, однако каждая из них выполняет свою задачу. Например, если вам необходимо отобразить только текущее время, не указывая дату, проще воспользоваться функцией Time, чем вызывать функцию Now и удалять составляющую даты.
Совет: Для установки текущих значений даты и времени можно воспользоваться операторами Date и Time. Чтобы установить новые значения системной даты и времени, необходимо поместить соответствующее ключевое слово в выражении слева от знака равенства.
Например, приведенный ниже фрагмент программы проверяет текущее время и, если уже больше 1:00 дня, выполняет некоторые операции:
If Time > #1:00 PM# Then
’ Эти операции выполняются только в том
* случае, если уже больше часа дня.
End If
С другой стороны, следующее сравнение в этом плане совершенно бессмысленно, поскольку значение функции Now (что-то вроде 34565.2345) будет наверняка больше значения #1:00 PM# (0.5416666667):
If Now > #1:00 PM# Then
’ Эти операции выполняются только в том
' случае, когда уже больше часа дня.
End If
Примечание: В отличие от других функций, синтаксис функций Now, Date и Time не требует круглых скобок. Более того, если вы введете скобки, VBA в большинстве случаев любезно удалит их из текста программы.
А если вам нужна только составляющая даты или времени?
Для выделения только даты из значения даты и времени можно воспользоваться встроенной функцией DateValue (см. раздел "Преобразование текста в формат даты и времени" данной главы). В качестве аргумента она принимает значение типа String или Date и возвращает только составляющую даты. При помощи функции Datevalue можно сравнить часть переменной типа Date, задающую дату, с указанным значением даты, например:
If DateValue(dtmSomeDate) = #5/14/70# Then
* Вы знаете, что составляющая даты в переменной
dtmSomeDate содержит значение 5/14/70. End If
Но если в значении даты и времени вас интересует только составляющая времени. можно воспользоваться функцией Timevalue. Она позволяет написать программу, которая сравнивает составляющую времени переменной типа Date с определенным значением времени, например:
If TimeValue(dtmSomeDate) > #1:00 PM# Then
’ Вы знаете, что составляющая времени переменной
’ dtmSomeDate содержит время после 1:00 РМ. End If
Выделение составляющих даты
Разумеется, если вы работаете с датами, вы работаете и со значениями лет, месяцев, дней, часов, минут и секунд. Перед вами может стоять задача интерпретировать ту или иную дату, например выяснить, в каком квартале она находится или на какой день недели выпадает. VBA предоставляет ряд простых и полезных функций, предназначенных для получения данной информации и даже больше.
Выделение необходимой части даты
Для начала обратите внимание, что функции, приведенные в табл. 2.1, полезны для извлечения простой информации из значений даты и времени. Каждая из них принимает в качестве аргумента значение даты и времени, а в результате возвращает целое число, содержащее требуемую часть информации. Кроме того, для получения любого из этих значений можно воспользоваться функцией DatePart (см. раздел "Одна функция выполняет все" данной главы). Однако если вас интересует только одна составляющая даты и времени, проще вызвать соответствующую функцию (табл. 2.1).
Таблица 2.1. Простые функции для работы со значениями даты и времени
Функция
Результат
Year
Составляющая даты и времени, представляющая год
Month
Составляющая даты и времени, представляющая месяц
Day
Составляющая даты и времени, представляющая день
Hour
Составляющая даты и времени, представляющая часы
Minute
Составляющая даты и времени, представляющая минуты
Second
Составляющая даты и времени, представляющая секунды
Любая из этих функций возвращает соответствующую составляющую даты и времени. Например, в результате выполнения следующего фрагмента отображается значение текущего года:
MsgBox ’’The current year is " & Year (Now)
а такой фрагмент отображает месяц и день:
MsgBox "Month: " & Month(Now) & " Day: " & Day(Now)
Приведенная ниже программа проверяет текущее время и в 1:12 пополудни позволяет выполнить определенные операции:
If Hour(Time) = 13 And Minute(Time) = 12 Then
’ Вы знаете, что сейчас 1:12 PM.
End If
!!Внимание: He задавайте функцию Date в качестве аргумента функций, возвращающих одну из составляющих времени. Поскольку результат выполнения функции Date не содержит информации о времени (его дробная часть равна 0), функции Hour, Minute и Second возвратят 0. Это касается и функций Day, Month и Year: не задавайте в качестве их аргумента функцию Time, поскольку результат ее выполнения не содержит информации о дате.
!
!
Какой это день недели?
В дополнение к сведениям о месяце и дне иногда требуется информация о дне недели, соответствующем определенной дате. Конечно, это значение можно рассчитать и самостоятельно (в литературе приведено множество алгоритмов вычисления дня недели по заданной дате), однако зачем выполнять лишнюю работу? VBA дает возможность легко получить ответ на этот вопрос при помощи встроенной функции WeekDay. Туже самую информацию можно получить, используя функцию DatePart, которая описана в следующем разделе.
Чтобы узнать день недели, соответствующий определенной дате, следует вызвать функцию WeekDay. Задайте в качестве ее аргумента значение даты, и функция возвратит день недели, который выпадает на эту дату. Например:
Debug.Print WeekDay(#5/16/56#)
возвратит число 4, указав тем самым, что 16 мая 1956 года было средой.
Одна функция выполняет все
В дополнение к функциям, описанным в предыдущих разделах, VBA имеет встроенную функцию DatePart, которая позволяет выбрать любую составляющую даты и времени, а также выполнить некоторые несложные вычисления. С ее помощью можно определить, к какому кварталу года относится заданная дата, а также получить другую информацию.
Вызывая функцию DatePart, необходимо указать, какие сведения вы хотите получить, а также задать значение даты. В табл. 2.2 приведен список возможных значений аргумента Interval функции DatePart.
Таблица 2.2. Значения аргумента Interval функции Date Part
Значение
Описание
УУУУ
Год
q
Квартал
m
Месяц
У
День в году
d
День
w
День недели
ww
Неделя
h
Час
n
Минута
s
Секунда
Например, эти две программные строки эквивалентны:
Debug.Print Day(Date)
Debug.Print DatePart(”d”, Date)
Однако следующие строки эквивалентов не имеют:
’ Возвращает порядковый номер текущего дня в году.
Debug.Print DatePart("у”, Date)
’ Возвращает номер квартала (1, 2, 3 или 4), к которому относится * текущая дата.
Debug.Print DatePart("q", Date)
Функция DatePart (подобно функции WeekDay) позволяет в качестве третьего необязательного параметра указать первый день недели. Кроме того, в качестве четвертого необязательного параметра можно указать и первую неделю года. В одних странах, в том числе и в США, первой неделей года считается та, на которую выпадает 1 января, в других — та, в которой к новому году относится не менее четырех дней, а в третьих — первая полная неделя нового года.
Простые вычисления
VBA имеет две функции, DateAdd и DateDif f, которые позволяют выполнять сложение и вычитание интервалов дат и времени. Конечно, и об этом говорилось выше, если вы работаете только с днями, применять эти функции нет необходимости — значения дат можно просто складывать и вычитать.
Добавление интервалов к дате
Функция DateAdd позволяет добавлять к значению даты и времени любое количество интервалов произвольной длины. Например, можно вычислить дату за 100 дней до текущей даты или время за 35 минут до текущего времени. Функция использует значения трех обязательных параметров, представленных в табл. 2.3. В табл. 2.4 приведен список возможных значений параметра interval.
Таблица 2.3. Параметры функции DateAdd
Параметр
Описание
Interval
Строковое выражение, задающее добавляемый интервал времени
Number
Количество добавляемых интервалов (может быть положительным — для получения даты в будущем, или отрицательным — для получения даты в прошлом)
Date
Дата, к которой добавляется указанный интервал
Таблица 2.4. Возможные значения параметра Interval функции DateAdd
Значение
Описание
УУУУ
Год
q
Квартал
m
Месяц
У d
День в году
День
w
День недели
ww
Неделя
h
Час
n
Минута
s
Секунда
Например, чтобы определить дату, которая будет ровно через год, лучше использовать следующее выражение:
DateAdd("уууу", 1, Date)
а не добавлять 365 дней к текущей дате (традиционное, хотя и неправильное, решение поставленной задачи). А как определить время через два часа? Это тоже несложно:
DateAdd("h", 2, Now)
Функция DateAdd никогда не возвратит недопустимого значения даты. Однако если вы укажете значение, в результате обработки которого должна получиться дата до 1/1/100 или после 12/31/9999, VBA выдаст сообщение об ошибке.
ееВнимание: Запомните! Сокращенным обозначением минут в значении даты и времени является буква п, а не ш, как можно предположить. Буква m в VBA применяется для обозначения месяцев. Многие разработчики приложений на VBA по невнимательности используют m и не замечают своей ошибки до тех пор, пока их программа не поступит в распоряжение пользователей.
е
е
Вычитание дат
Если требуется определить количество интервалов между двумя датами (причем интервалом может служить любой элемент из табл. 2.4), можно воспользоваться функцией Dat eDiff. В табл. 2.5 приведен список параметров этой функции.
Таблица 2.5. Параметры функции DateDiff
Параметр
Тип данных
Описание
Interval
(обязательный)
String
Временной интервал, используемый при вычислении разности между Datel И Date2
Datel, Date2
(обязательный)
Date
Значения двух дат, используемых в вычислении
FirstDayOfWeek
(необязательный)
Integer
(константа)
Первый день недели (по умолчанию — воскресенье)
FirstWeekOfYear
Integer
Первая неделя года (по умолчанию — неделя, на ко
(необязательный)
(константа)
торую выпадает 1 января)
Например, для подсчета количества часов между двумя переменными даты и времени, dtmValuel и dtmValue2, можно написать такое выражение:
DateDiff(”h”, dtmValuel, dtmValue2)
Иногда результат выполнения функции DateDiff может быть неожиданным. Вообще говоря, эта функция не выполняет округления, однако результат будет различным для переменных разных типов. Например:
DateDiff("h", #10:00#, #12:59:59#)
возвращает число 2, поскольку между двумя заданными значениями времени прошло только два полных часа.
При работе со значениями месяцев или лет функция DateDiff возвращает количество границ месяцев или лет (смена месяца или года) между двумя датами. Например, можно было бы ожидать, что результатом выполнения следующего выражения будет 0, поскольку заданный промежуток времени не содержит ни одного полного месяца. Однако функция возвращает 1, так как промежуток включает переход от одного месяца к другому:
DateDiff("m", #11/15/99#, #12/1/99#)
То же самое происходит и в следующем выражении. Функция возвращает значение 1 (один год), хотя заданный промежуток времени состоит всего из одного дня:
DateDiff(”уууу”, #12/31/98#, #1/1/99#)
Результаты работы функции DateDiff с недельными интервалами могут быть совсем странными. VBA трактует интервалы "w" (день недели) и "w” (неделя) по-разному, однако в обоих случаях возвращается количество недель между двумя датами. Если при указании интервала использовать параметр "w", VBA подсчитывает количество дней, на который выпадает Date 1, до тех пор, пока не будет достигнута Date2. При этом Datel в расчет нс принимается, a Date2 — принимается, если на нее выпадает соответствующий день недели (рис. 2.1). Например:
DateDiff(”w”, #11/5/99#, #11/18/99#)
возвращает 1, поскольку в промежутке с 11/5/99 по 11/18 имеется единственная среда. С другой стороны,
DateDiff("w", #11/5/99#, #11/19/99#)
возвратит 2, так как в соответствующий диапазон попадает две среды (11/12 и 11/19).
При задании параметра "ww” функция DateDiff подсчитывает календарные недели (т.е. значение счетчика изменяется каждое воскресенье). Таким образом, в обоих приведенных выше примерах функция DateDiff возвратит 2, поскольку соответствующие интервалы содержат по два воскресенья. Как и при использовании параметра "w", VBA учитывает конечную дату интервала, если она выпадает на воскресенье, однако никогда не принимает во внимание начальную дату, даже если это воскресенье. Поэтому при условии, что Datel — воскресенье, DateDiff выдает один и тот же результат, независимо от указанного параметра ("w" или "ww”).
Рис. 2.1. Иллюстрация к расчетам, выполняемым функцией DateDiffРис. 2.1. Иллюстрация к расчетам, выполняемым функцией DateDiff
Рис. 2.1. Иллюстрация к расчетам, выполняемым функцией DateDiff
Рис. 2.1. Иллюстрация к расчетам, выполняемым функцией DateDiff
Совет: При использовании литералов даты (например, #5/1/99#) VBA берет для расчетов точное значение даты. Однако если данный параметр задается строкой, где указаны только месяц и дата (например, "5/1"), VBA при выполнении программы вставляет значение текущего года. Это позволяет писать программы, работающие независимо от года. Конечно, в таком слу
чае затруднительно сравнивать даты двух разных лет, поскольку нет возможности задать иной год, кроме текущего. Но если нужно выполнить расчеты, сравнивая даты в пределах текущего года, применение данного метода сэкономит ваше время.
Преобразование текста в формат даты и времени
Вашей программе придется работать и со значениями дат, хранящихся в виде строк. Допустим, вы получили данные из внешнего источника и должны преобразовать их в формат даты или пользователь ввел значение, с которым необходимо работать как с датой. Для выполнения преобразований VBA предоставляет три функции: DateValue, TimeValue и CDate. Назначения этих функций несколько отличаются, однако это не видно из справки VBA.
Функции DateValue и TimeValue принимают один аргумент (обычно, строковое выражение) и преобразуют его в дату и время соответственно. Как упоминалось ранее, эти функции можно также использовать для извлечения из комбинированного значения даты и времени составляющей только времени или только даты. Функция DateValue может преобразовывать любые строки, которые соответствуют внутреннему формату даты, а также любые распознаваемые текстовые названия месяцев. Если значение аргумента содержит и составляющую времени, функция DateValue просто удаляет эту информацию из возвращаемого значения.
Например, следующие выражения возвращают одно и то же значение (переменная intDate имеет значение 30):
DateValue("12 30 99")
DateValue("December 30 1999")
DateValue("December " & intDate & " 1999")
DateValue("12/30/99 5:00 PM")
DateValue("30/12/999")
Результат выполнения последнего примера (30 декабря) нс зависит от того, в какой стране производятся вычисления. Однако в данном случае так происходит лишь потому, что само значение даты не допускает неоднозначного толкования. Попробуйте выполнить тот же пример, допустим, с датой 12/1/99. Результат будет зависеть от настройки операционной системы: в США вы получите 1 декабря, а в других странах — 12 января.
Функция TimeValue работает аналогично функции DateValue. В качестве аргумента ей можно передать строку, которая содержит любое допустимое выражение, а она возвратит значение времени. Если функции TimeValue задать в качестве аргумента строку, содержащую информацию о дате, она игнорирует эту информацию при формировании результата.
Например, все приведенные ниже примеры возвращают одинаковое значение времени:
TimeValue("5:15 РМ")
TimeValue("17:15")
TimeValue("12/30/97 5:15 PM")
Функция CDate преобразует любой заданный аргумент в значение даты и времени, если это возможно. В отличие от функций DateValue и TimeValue, она возвращает полное значение даты и времени, т.е. всю переданную ей информацию. Кроме того, она может преобразовывать числовые значения в даты. Например, все следующие примеры возвращают одно и то же значение. (Последний пример, конечно, лишний, но он работает.)
CDate("12/30/99 5:15 РМ")
CDate(35794.71875)
CDate(#12/30/99 5:15 РМ#)
Чаще всего функция CDate применяется для преобразования текста в полное значение даты и времени, а для получения составляющих даты и времени лучше использовать функции DateValue и TimeValue соответственно.
Объединение составляющих даты и времени
Что делать, если вместо текста у вас есть только составляющие даты или времени в виде числовых значений? Для их объединения вы могли бы воспользоваться одной из функций, описанных в предыдущем разделе, сначала сформировав составное строковое выражение, а затем вызвав соответствующую функцию. Однако в этом случае лучше применить функцию DateSerial или Time Seri al. Каждая из них имеет по три аргумента: DateSerial — значения года, месяца и дня, которые задаются в указанном порядке; TimeSerial — значения часов, минут и секунд также в указанном порядке. Функции возвращают значение даты или времени соответственно, подобно тому, как функции DateValue и TimeValue поступают с единым выражением, используемым в качестве аргумента. Многие описанные в этой главе функции используют DateSerial или TimeSerial для формирования значений даты и времени из трех составляющих.
Например, как узнать первый день текущего месяца? Простейшее решение этой задачи — написать функцию с таким выражением:
dhFirstDaylnMonth = DateSerial(Year(dtmDate), Month(dtmDate), 1)
Как вы увидите, точно такой же метод использует функция dhFirstDaylnMonth, которая на основе текущих значений года и месяца, а также единицы в качестве значения числа создает новое значение даты, соответствующее первому дню текущего месяца.
Функция TimeSerial работает точно так же. Вы задаете значения часов, минут и секунд, а она формирует соответствующее значение времени. Для формирования полного значения даты и времени можно использовать обе функции одновременно, если у вас есть шесть значений, представляющих год, месяц, число, часы, минуты и секунды. Для этого необходимо составить выражение, подобное следующему:
DateSerial(intYear, intMonth, intDay) + _
TimeSerial(intHour, intMinutes, intSeconds)
Поскольку значение даты и времени — это сумма целой части числа, представляющей дату, и дробной части, представляющей время, вы можете использовать обе функции вместе для получения полного значения даты и времени.
Полезным свойством встроенных функций VBA, работающих с датой, является то, что они никогда не возвращают недопустимого значения. Например, если задать функцию DateSerial (1999, 2, 35), т.е. описать несуществующую дату, будет выдан результат 3/7/99. Эту возможность мы используем при определении того, является ли год високосным?
Отображение величин в удобном для вас виде
Вероятно, в своих приложениях вы захотите отображать даты в различных форматах. VBA имеет функцию Format, которая позволяет форматировать величины типа Date нужным образом. Кроме того, эту функцию можно использовать для форматирования числовых и строковых значений. Более подробная информация об этом приведена в справке VBA.
При использовании функции Format задается выражение, которое необходимо отформатировать (в данном случае значение даты и времени), а также строковое выражение, определяющее формат. Кроме того, в качестве необязательных параметров можно задать первый день недели и константу, указывающую, какую неделю года считать первой. Более подробная информация об этих параметрах приведена в табл. 2.5.
В качестве спецификатора формата используется либо встроенная, либо заданная вами строка. В табл. 2.6 приведен список встроенных форматов даты и времени.
Таблица 2.6. Стандартные форматы даты и времени для функции Format
Название формата
Описание
Региональные установки
General Date
Отображает дату и (или) время в зависимости от значения первого параметра, используя форматы Short Date и Long Time вашей системы
Использует
Long Date
Отображает дату (без времени) в соответствии с форматом Long Date вашей системы
Использует
Medium Date
Отображает дату (без времени), используя формат Medium Date, соответствующий языковой версии поддерживающего VBA приложения
Не использует
Short Date
Отображает дату (без времени) согласно формату short Date вашей системы
Использует
Long Time
Отображает время (без даты) согласно формату Long Time вашей системы: часы, минуты и секунды
Использует
Medium Time
Отображает время (без даты) в 12-часовом формате: часы, минуты и индикатор АМ/РМ
Использует
Short Time
Отображает время (без даты) в 24-часовом формате (например, 17:45)
Использует
Для проверки описанных форматов совершим путешествие в некую воображаемую страну. Региональные установки времени в Windows представлены на рис. 2.2, а установки даты — на рис. 2.3. Окно, изображенное на рис. 2.4, содержит несколько примеров использования функции Format с различными форматами даты и времени.
I ЬОЙ< ИМ И Uh И ( 1 аНД.ifm-i
: Регион и язык | Числа ] Денежная единица Время | Дата | |i||||ili|iii|illw
3 :3 :; Образец
3 :
3 :
’ . !■ [ч@мм@сс 00
Разделитель компонент оу времени |@ ~"
Обозначение времени до полудня (АМ):
0TWH<r |
Рис. 2.2. Региональные установки времени в воображаемой стране
Если вы хотите поэкспериментировать или вас не устраивают ограниченные возможности форматов даты и времени, вы можете создать собственные форматы, используя для этого параметры из табл. 2.7. Сформировав строку, содержащую комбинацию таких символов, можно отформатировать значение даты и времени по собственному желанию. На рис. 2.5 представлено несколько образцов форматов, которые вы можете создавать самостоятельно, используя символы из табл. 2.7.
Гитни на Я 1ык и vidHM.ipiu
Регион и язык) Числа | Денежная единица | Время Дата |
^Календарь: [
: ■ Краткое представление дагм ■ --
Образец.
Кр^кий Формат даты*
; Разделитель компонентов даты:
Полный Фермат даты
Полный Формат даты;
Отмена |
7Ж*7Ж*Рис. 2.3. Региональные установки даты в той же воображаемой стране
7Ж*
7Ж*
Выражениеj Значение
||||i||||||||j|illiili|^^
iillliihi
? Format(#12/15/99 1:15
PH#, "General Date")
12815099 1015000 уу
? Format(#12/15/99 1:15
PM#, "Long Date")
15 декабря, 1999
? Format(#12/15/99 1:15
PH#, "Medium Date")
15-дек-99
? Format(#12/15/99 1:15
PM#, "Short Date")
12015099
? Format(#12/15/99 1:15
PM#, "Long Time")
1015000 уу
? Format(#12/15/99 1:15
PM#, "Medium Time")
01015 уу
? Format(#12/15/99 1:15
PM#, "Short Time")
13015
< [ |
i Локальные | Контрольные |
:. i Окно ОТЛаДКИ
Рис. 2.4. Проверка региональных форматов даты и времени в окне отладки Microsoft Access
i|i|ii||i|i||||ii||iMl|^
. j
бь^'-кение13 качание
j Тил
: ? Format(#12/15/99 1:15 РИ#, i 13:15:00 дек 15, 1999
"hh:mm:ss гагат dd,
УУУУ”)
'? Format(#12/15/99 1:15 РИ#,
' Пн, 15 декабря, 1999
"ddd, dd ratittn, yyyy")
Д11
Рис. 2.5. Применение функции Format с определенными пользователем форматами
Таблица 2.7. Пользовательские форматы даты и времени для функции Formate
Символ
Описание
Региональные установки
Комментарий
Разделитель составляющих времени (часов, минут и секунд)
Использует
В некоторых национальных настройках вместо двоеточия может применяться другой символ (результирующее значение определяется региональными установками)
/
Разделитель составляющих даты (дней, месяцев и лет)
Использует
В некоторых национальных настройках вместо косой черты может применяться другой символ (результирующее значение определяется региональными установками)
c
Отображает дату как ddddd и время как ttttt в указанном порядке
Использует
Идентичен стандартному формату General Date
d
Отображает номер дня, содержащий одну или две цифры (1-31)
Не использует
dd
Отображает номер дня, содержащий две цифры (01-31)
Не использует
ddd
Отображает сокращенное название дня недели (Пн-Вс)
Использует
dddd
Отображает полное название дня недели
Использует
ddddd
Отображает соответствующую числу полную дату (день, месяц и год)
Использует
Идентичен стандартному формату Short Date
dddddd
Отображает соответствующую числу полную дату (день, месяц и год)
Использует
Идентичен стандартному формату Long Date
Символ
Описание
Региональные установки
Комментарий
w
Отображает номер дня недели (от 1 для воскресенья до 7 для субботы)
Не использует
Результат зависит от значения параметра FirstDayOfWeek
ww
Отображает номер недели года (1-54)
Не использует
Результат зависит от значения параметра FirstWeekOfYear
m
Отображает номер месяца, содержащий одну или две цифры (1-12)
Не использует
Если символ m следует сразу после символа h или символов hh, отображается число минут
mm
Отображает номер месяца, содержащий две цифры (01-12)
Не использует
Если символы mm следуют сразу после символа h или символов hh, отображается число минут
mmm
Отображает сокращенное название месяца (Янв-Дек)
Использует
mmmm
Отображает полное название месяца (Январь-Декабрь)
Использует
q
Отображает номер квартала года (1-4)
Не использует
У
Отображает номер дня года (1-366)
Не использует
УУ
Отображает номер года, состоящий из двух цифр (00-99)
Не использует
УУУУ
Отображает полный номер года (100-9999)
Не использует
h
Отображает число часов, состоящее из одной или двух цифр (0-23)
Не использует
hh
Отображает число часов, состоящее из двух цифр (00-23)
Не использует
n
Отображает число минут, состоящее из одной или двух цифр (0-59)
Не использует
nn
Отображает число минут, состоящее из двух цифр (00-59)
Не использует
s
Отображает число секунд, состоящее из одной или двух цифр (0-59)
Не использует
ss
Отображает число секунд, состоящее из двух цифр (00-59)
Не использует
ttttt
Отображает полное время (часы, минуты и секунды)
Использует
Идентичен стандартному формату Long Time
Символ
Описание
Региональные установки
Комментарий
АМ/РМ
Использование 12-часового формата
Не использует
К значению времени добавляются прописные буквы АМ (до полудня) или РМ (после полудня)
am/pm
Использование 12-часового формата
Не использует
К значению времени добавляются строчные буквы ат (до полудня) или рт (после полудня)
А/Р
Использование 12-часового формата
Не использует
К значению времени добавляются прописные буквы А (до полудня) или Р (после полудня)
а/р
Использование 12-часового формата
Не использует
К значению времени добавляются строчные буквы а (до полудня) или р (после полудня)
АМРМ
Использование 12-часового формата и отображение строкового литерала АМ/РМ в соответствии с установками вашей системы
Использует
Регистр символов индикатора АМ/РМ определяется системными настройками
Если вы хотите включить в строку форматирования текст, воспользуйтесь одной из следующих возможностей:
введите перед каждым символом обратную косую черту (\);
заключите всю текстовую вставку в кавычки внутри строки.
Если в текстовой вставке много символов, применение первого метода затрудняет чтение. Второй метод требует выполнения дополнительных операций, поскольку необходимо вставить кавычки внутри строки, которая и так уже взята в кавычки.
Допустим, значение даты и времени должно отображаться следующим образом:
Мау 22, 1999 at 12:01 АМ
Это можно осуществить двумя способами. Использовать символы обратной косой черты в строке форматирования (в соответствии с первым методом):
Format(#5/22/99 12:01 AM#, "mmm dd, уууу \a\t h:mm АМ/РМ")
или заключить слово at внутри строки форматирования в кавычки (в соответствии со вторым методом). Для этого необходимо вставить два символа кавычек там, где требуется один. VBA воспринимает две последовательные кавычки как единый литеральный символ кавычки и выполняет правильные действия:
Format(#5/22/99 12:01 AM#, "mmm dd, уууу ""at"" h:mm АМ/РМ")
В обоих случаях результаты одинаковы.
Приближение рубежа столетий|
Каким образом VBA решает проблему 2000 года? В действительности, довольно | изящно. Обычно пользователи задают значение года в виде двух последних | цифр, а это, конечно, вызвало большую компьютерную полемику в последние j годы XX столетия. VBА интерпретирует двухразрядное значение года достаточно I рационально: если ввести дату из диапазона от 1/1/00 до 12/31/29, VBA интер- j претирует ее как дату XXI столетия, а если — из диапазона от 1/1/30 до 12/31/99, | VBA интерпретирует ее как дату XX столетия. Хотя было бы неплохо предоста- | вить возможность определения порядка интерпретации пользователю, все же та- | кое жесткое решение намного лучше, чем вообще никакого. Далее показано, как { VBA обрабатывает значения дат с двухразрядным значением года:|
даты с 1/1/00 по 12/31/29 интерпретируются как даты XXI столетия | (1/1/2000 - 12/31/2029);|
даты с 1/1/30 по 12/31/99 интерпретируются как даты XX столетия | (1/1/1930 - 12/31/1999).|
Более сложные задачи
Теперь, имея на вооружении набор встроенных функций VBA, вы сможете решать множество других задач, связанных с работой со значениями даты и времени. В остальной части главы представлен ряд примеров решения задач, требующих создания специальных процедур. Эти процедуры сгруппированы по своему функциональному назначению. Далее рассматриваются три аспекта работы со значениями даты и времени:
Определение конкретной даты
Операции со значениями даты и времени
Работа с промежутками времени
Определение конкретной даты
Здесь приведены решения многих простых задач, требующих определения даты.
В частности, рассмотрены следующие процедуры:
Определение начала или конца месяца
Определение начала или конца недели
Определение начала или конца года
Определение начала или конца квартала
Определение следующего или предыдущего дня недели
Определение следующей годовщины события
Какого числа будет пятая пятница месяца?
Операции с рабочими днями
Использование необязательных параметров
Многие процедуры, описанные в последующих разделах, допускают один или несколько необязательных параметров. Если при вызове функции значение параметра не задано, функция присваивает параметру некоторое значение. В большинстве случаев это позволяет опускать параметр даты, при этом функция использует текущее значение даты. При указании необязательных параметров имеется две возможности:
использовать параметр типа Variant и проверять его наличие при помощи функции IsMissing;
использовать параметр строго заданного типа и присваивать ему значение по умолчанию.
Мы выбрали второй способ задания параметров, поскольку он позволяет проверить тип при вызове процедуры. С другой стороны, такой вариант исключает возможность использования функции IsMissing для проверки пропуска параметра. Поскольку значение, присваиваемое параметру при формальном описании процедуры, может быть только константой, а не значением функции, при работе с датами мы использовали значение 0, чтобы указать, что параметр даты опущен.
Описание функции может иметь такой вид:
Function dhFirstDaylnMonth(Optional dtmDate As Date = 0) As Date
Такой вариант подразумевает, что процедура проверяет наличие нулевого I значения и заменяет его текущей датой:
If dtmDate = 0 Then
' Задал ли пользователь значение даты?
Если нет, используется текущая дата. dtmDate = Date
End If
Предполагаем, что вряд ли вам когда-либо придется в действительности использовать дату 0 (12/30/1899) в качестве параметра одной из этих процедур. Если вы на самом деле попытаетесь передать какой-либо процедуре, до- ■ пускающей необязательный параметр даты, значение 12/30/1899, процедура воспримет ее как текущую дату. Если же задание этой даты все-таки не исключено, то необходимо либо удалить необязательный параметр, либо найти другое решение данной проблемы.
Определение начала или конца месяца
Определить первый день указанного месяца легко. Для этого можно воспользоваться функцией DateSerial, задав ей значения искомых месяца и года и цифру 1 в качестве значения дня. Функция dhFirstDaylnMonth в примере 2.1 вызывает функцию DateSerial, проверяя сначала значение исходного параметра и при необходимости
преобразовывая его в значение текущей даты. В результате следующего вызова функции
dhFirstDaylnMonth(#5/7/70#)
возвращается значение 5/1/70.
Определение последнего дня месяца требует использования не вполне очевидных, хотя и документированных особенностей функции DateSerial. Дело в том, что любой параметр (или все три параметра одновременно) функции DateSerial может быть числовым выражением. Поскольку VBA никогда не выдаст в качестве результата недопустимого значения даты, можно запросить день перед первым днем месяца, увеличив значение месяца на единицу и уменьшив значение дня на это же число. Функция dhLastDaylnMonth из примера 2.1 поступает именно так. Используя выражение: DateSerial(Year(dtmDate), Month(dtmDate) + 1, 0)
она находит нулевой день следующего месяца, который, конечно же, является последним днем требуемого месяца.
Пример 2.1. Определение первого или последнего дня месяца
Function dhFirstDaylnMonth(Optional dtmDate As Date = 0) As Date ’ Определяет первый день указанного месяца. If dtmDate = 0 Then ’ Задал ли пользователь значение даты? ’ Если нет, используется текущая дата. dtmDate = Date End If dhFirstDaylnMonth = DateSerial(Year(dtmDate), _ Month(dtmDate), 1) End Function
Function dhLastDaylnMonth(Optional dtmDate As Date = 0) As Date ’ Определяет последний день указанного месяца. If dtmDate = 0 Then ’ Задал ли пользователь значение даты? ’ Если нет, используется текущая дата. dtmDate = Date End If dhLastDaylnMonth = DateSerial(Year(dtmDate), _ Month(dtmDate) + 1, 0) End Function
Определение начала или конца недели
Метод определения первого или последнего дня недели основан на том, что из значения даты можно вычитать целые числа и анализировать новое значение даты. Если заданная дата приходится на воскресенье, для определения первого дня недели из значения даты достаточно вычесть 0 (при условии, что первым днем недели считается воскресенье). Если эта дата выпадает на понедельник, следует вычитать 1, если на вторник — 2 и т.д. Поскольку функция WeekDay возвращает число из диапазона от
1 до 7, достаточно вычесть из заданного значения даты значение, возвращаемое функцией WeekDay, и затем прибавить 1. Описанная процедура реализована в функции dhFirstDayinWeek, которая приведена в примере 2.2.
Примечание: Чтобы все было корректно, функции dhFirstDayinWeek и dhLastDaylnWeek задают первый день недели для функции WeekDay, используя константу vbUseSystem. Таким образом, первый и последний дни недели будут соответствовать региональным установкам. Если опустить эту константу, программа будет считать первым днем недели воскресенье, а последним — субботу, независимо от региональных установок.
Функция dhLastDaylnWeek, приведенная в примере 2.2, использует ту же концепцию. Однако на этот раз к значению первого дня недели нужно добавлять 6. Таким образом (предположим, что программа с данной функцией выполняется в США), если исходная дата приходится на среду, нужно вычесть возвращаемое функцией WeekDay значение (4), в результате чего получится суббота. Добавив к результату 1, получим первый день недели, а добавив еще 6 — последний день недели.
Пример 2.2. Определение первого или последнего дня недели
Function dhFirstDayinWeek(Optional dtmDate As Date = 0) As Date ' Определяет первый день недели, содержащей дату, ' заданную параметром dtmDate.
' Использует первый день недели, указанный в региональных установках. If dtmDate = 0 Then
' Задал ли пользователь значение даты?
Если нет, используется текущая дата. dtmDate = Date End If dhFirstDayinWeek = dtmDate - WeekDay(dtmDate, vbUseSystem) + 1 End Function
Function dhLastDaylnWeek(Optional dtmDate As Date = 0) As Date ' Определяет первый день недели, содержащей дату, ' заданную параметром dtmDate.
' Использует первый день недели, указанный в региональных установках. If dtmDate = 0 Then
1 Задал ли пользователь значение даты?
' Если нет, используется текущая дата. dtmDate = Date End If dhLastDaylnWeek = dtmDate - WeekDay(dtmDate, vbUseSystem) + 7 End Function
При вызове функций dhFirstDayinWeek и dhLastDaylnWeek задайте значение даты, либо не передавайте никакого значения для этого параметра, чтобы использовать текущую дату. Например, следующая программа находит первый и последний день двух недель:
Debug.Print "Первый день текущей недели: " & dhFirstDayinWeek()
Debug.Print "Последний день текущей недели: " & dhLastDaylnWeek()
Debug.Print "Первый день недели, включающей 1/1/99: " _
& dhFirstDaylnWeek(#1/1/99#) Debug.Print "Последний день недели, включающей 1/1/99: " _ & dhLastDaylnWeek(#1/1/99#)
Определение начала или конца года
Процедура определения первого или последнего дня года проста по сравнению с другими функциями, описанными в этом разделе. При наличии функции DateSerial задача сводится к тому, чтобы сформировать значение даты, задающее 1 января или 31 декабря определенного года. Поскольку эти даты фиксированы в качестве первого и последнего дня года, больше никакие вычисления не нужны. Функции dhFirstDaylnYear и dhLastDaylnYear, приведенные в примере 2.3, выполняют все необходимые операции.
Пример 2.3. Определение первого или последнего дня года
Function dhFirstDaylnYear(Optional dtmDate As Date = 0) As Date
Определяет первый день указанного года. If dtmDate = 0 Then
' Задал ли пользователь значение даты?
Если нет, используется текущая дата. dtmDate = Date End If dhFirstDaylnYear = DateSerial(Year(dtmDate) , 1, 1) End Function
Function dhLastDaylnYear(Optional dtmDate As Date = 0) As Date ' Определяет последний день указанного года. If dtmDate = 0 Then ' Задал ли пользователь значение даты? ' Если нет, используется текущая дата. dtmDate = Date End If dhLastDaylnYear = DateSerial(Year(dtmDate), 12, 31) End Function
При вызове этих функций можно либо не задавать никакого значения (для работы с текущим годом), либо задать значение даты, указывающее определенный год. Каждая функция возвращает соответствующую дату. Например, следующий фрагмент программы определяет первый и последний дни года двумя способами: Debug.Print "Первый день текущего года: " & dhFirstDaylnYear() Debug.Print "Последний день текущего года: " & dhLastDaylnYear() Debug.Print "Первый день следующего года: " & _ dhFirstDaylnYear(DateAdd("уууу" , 1, Date)) Debug.Print "Последний день предыдущего года: " & _ dhLastDaylnYear(DateAdd("уууу", -1, Date))
Определение начала или конца квартала
Определить начало или конец квартала немного сложнее, поскольку работа с кварталами (периоды с января по март, с апреля по июнь, с июля по сентябрь и с октября по декабрь) практически не поддерживается в библиотеке функций VBA. В примере 2.4 представлены функции dhFirstDaylnQuarter и dhLastDaylnQuarter, которые позволяют решить эту проблему.
Пример 2,4, Определение первого или последнего дня квартала
Function dhFirstDaylnQuarter(Optional dtmDate As Date = 0) As Date 1 Определяет первый день квартала, содержащего дату, 1 заданную параметром dtmDate. Const dhcMonthsInQuarter As Integer = 3 If dtmDate = 0 Then ’ Задал ли пользователь значение даты?
Если нет, используется текущая дата. dtmDate = Date End If dhFirstDaylnQuarter = DateSerial(Year(dtmDate), _ Int((Month(dtmDate) - 1) / dhcMonthsInQuarter) * _ dhcMonthsInQuarter + 1, 1) End Function
Function dhLastDaylnQuarter(Optional dtmDate As Date = 0) As Date 1 Определяет последний день квартала, содержащего дату, ’ заданную параметром dtmDate. Const dhcMonthsInQuarter As Integer = 3 If dtmDate = 0 Then ' Задал ли пользователь значение даты? 1 Если нет, используется текущая дата. dtmDate = Date End If dhLastDaylnQuarter = DateSerial(Year(dtmDate), _ Int((Month(dtmDate) - 1) / dhcMonthsInQuarter) * _ dhcMonthsInQuarter + (dhcMonthsInQuarter + 1), 0) End Function
Зная, как найти первый день квартала, не составляет труда определить и последний его день — достаточно прибавить три месяца и вычесть один день. Но как определить первый день квартала, содержащего указанную дату? Точно известно значение года (оно соответствует году указанной даты) и значение дня (оно должно быть равно 1), но как определить месяц? Конечно, можно решить проблему "в лоб”, воспользовавшись оператором Select Case: Select Case Month(dtmDate) Case 1, 2, 3 intMonth = 1 Case 4, 5, 6 intMonth = 4
и T . Д .
End Select
Однако давайте попытаемся найти более красивое решение! Это как раз тот случай, когда стоит достать лист бумаги и подумать, что в действительности происходит. Полезно составить сводную таблицу исходных значений и ожидаемых результатов преобразования значения произвольного месяца в первый месяц соответствующего квартала:
МесяцПервый месяц квартала
11
21
31
44
54
64
77
87
97
1010
1110
1210
Давайте определим математическую зависимость между этими двумя столбцами. (Напоминает школьный курс алгебры, не так ли?) Результат представляет собой последовательность чисел, каждое из которых кратно 3, плюс 1. Если поразмыслить еще немного, можно получить следующее алгебраическое соотношение между двумя столбцами, которое является решением поставленной задачи, используемым функцией dhFirstDaylnQuarter:
Первый месяц квартала = Int((Month -1) / 3) *3+1
Это выражение находит для каждого значения месяца наибольшее кратное трем число, меньшее номера месяца, и прибавляет к нему 1. В результате такого вычисления, основанного на значении из первого столбца, всегда получается значение из второго столбца. Таким образом, представленное выражение, вместо того, чтобы заставлять VBA выполнять поиск при каждом вызове функции, производит достаточно простые вычисления.
При известном значении первого месяца квартала для функции dhFirstDaylnQuarter не составляет труда определить первый его день: программа вызывает функцию DateSerial, формируя дату из заданного года, вычисленного месяца и единицы в качестве значения дня. Для определения последнего дня квартала функция dhLastDaylnQuarter повторяет те же вычисления, что и dhFirstDaylnQuarter, прибавляет единицу к найденному значению месяца для определения следующего месяца, а затем использует 0 в качестве значения дня. Как говорилось в разделе "Определение начала или конца месяца", значение 0 в качестве значения дня, задаваемое для функции DateSerial, приводит к получению последнего дня предыдущего месяца, т.е. именно того, что и требовалось для решения поставленной задачи.
Определение следующего или предыдущего дня недели
Для многих финансовых расчетов необходимо знать следующий день недели после указанной даты. Например, вам может понадобиться дата первой пятницы после 29 апреля 1999 года или дата предшествовавшего этой дате понедельника. Как и при определении первого и последнего дня недели, соответствующие расчеты основываются на том, что из значения даты можно вычесть целое число и рассматривать затем другое значение даты.
В данном случае простейшим решением является вычисление начала недели, на которую приходится указанная дата, и последующее прибавление необходимого количества дней для получения требуемой даты. Соответствующий фрагмент из программ, приведенных в примере 2.5, выглядит следующим образом:
intTemp = WeekDay(dtmDate)
dhPreviousDOW = dtmDate - intTemp + intDOW
Допустим, вы хотите определить дату четверга перед вторником 10/5/99. В этом случае переменной intTemp будет присвоено значение 3 (соответствует вторнику), а переменной intDOW — значение 5 (соответствует четвергу). Выражение dtmDate - intTemp + intDOW ’ то же, что и: ' #10/5/99# -3+5
возвратит дату 10/7/99. Понятно, что это четверг не перед датой 10/5/99, а четверг после нее. Поэтому, последним этапом вычислений является вычитание одной недели, если это нужно. Полностью выражение выглядит следующим образом:
dhPreviousDOW = dtmDate - intTemp + intDOW - _ IIF(intTemp > intDOW, 0, 7)
Когда нет необходимости вычитать 7 для перехода к предыдущей неделе? Давайте поменяем даты местами. Если вы ищете вторник перед 10/5/99, соответствующее выражение должно иметь такой вид:
dtmDate - intTemp + intDOW
' то же, что и: ' #10/5/99# -5+3
что дает в результате #10/5/99#, т.е. правильный ответ. Нет необходимости вычитать 7, чтобы перейти к предыдущей неделе. Подобным образом можно вычислить день недели после указанной даты. Если в текущей неделе искомый день уже прошел, следует прибавить 7 для перехода к следующей неделе.
Пример 2.5. Определение предыдущего или следующего указанного дня недели
Function dhPreviousDOW(intDOW As Integer, _
Optional dtmDate As Date = 0) As Date
* Определение предыдущего указанного
' дня недели перед заданной датой.
Dim intTemp As Integer
If dtmDate = 0 Then
' Задал ли пользователь значение даты? ' Если нет, используется текущая дата. dtmDate = Date
End If intTemp = WeekDay(dtmDate) dhPreviousDOW = dtmDate - intTemp + intDOW - Ilf(intTemp > intDOW, 0, 7)
End Function
Function dhNextDOW(intDOW As Integer, _ Optional dtmDate As Date = 0) As Date ' Определение следующего указанного 1 дня недели после заданной даты. Dim intTemp As Integer If dtmDate = 0 Then
' Задал ли пользователь значение даты?
Если нет, используется текущая дата. dtmDate = Date
End If intTemp = WeekDay(dtmDate) dhNextDOW = dtmDate - intTemp + intDOW + _ Ilf(intTemp < intDOW, 0, 7)
End Function
Следующие примеры иллюстрируют применение этих двух функций:
Debug.Print "Понедельник перед 12/25/99 — это " & _ dhPreviousDOW(vbMonday, #12/25/99#)
Debug.Print "Пятница после 12/25/99 — это " & _ dhNextDOW(vbFriday, #12/25/99#)
Debug.Print "Сегодня " & Date & _ ". Следующий понедельник — " & dhNextDOW(vbMonday)
Определение следующей годовщины события
Часто при работе с датами приходится сталкиваться с необходимостью вычисления следующей годовщины какого-либо события, например дня рождения или свадьбы. Для выполнения подобных вычислений предназначена функция dhNextAnniversary (пример 2.6), которая по заданному значению даты находит следующую годовщину этого события, принимая во внимание дату текущую.
Пример 2.6. Следующая годовщина события
Function dhNextAnniversary(dtmDate As Date) As Date ' Находит следующую годовщину по заданной дате. Dim dtmThisYear As Date 1 Определение соответствующей даты в текущем году. dtmThisYear = DateSerial(Year(Now), Month(dtmDate), _ Day(dtmDate))
Если годовщина события в этом году уже прошла, ' к номеру года прибавляется 1.
If dtmThisYear < Date Then dtmThisYear = DateAdd("yyyy", 1, dtmThisYear) End If dhNextAnniversary = dtmThisYear End Function
Здесь все очень просто. Программа выполняет следующие действия:
Определяет дату, соответствующую годовщине в текущем году.
Если в текущем году эта дата уже прошла, к дате прибавляется единица.
Для определения даты годовщины события в текущем году программа использует следующее выражение:
dtmThisYear = DateSerial (Year(tyow), Month(dtmDate), Day(dtmDate))
Для того чтобы откорректировать результат, если в текущем году эта дата уже прошла, функция использует следующий фрагмент программы:
If dtmThisYear < Date Then dtmThisYear = DateAdd("yyyy", 1, dtmThisYear) End If
В любом случае переменная dtmThisYear будет содержать дату следующей годовщины события.
Для проверки этой процедуры можно воспользоваться следующим фрагментом программы. Пусть текущая дата — 11/12/99, тогда
dhNextAnniversary(#5/16/2000#)
возвращает 5/16/2000, поскольку в 1999 году эта дата уже прошла.
Операции со значениями даты и времени
В этом разделе рассмотрены пять часто встречающихся задач, связанных с обработкой значений даты и времени:
определение числа дней в указанном месяце;
подсчет количества определенных дней недели в месяце;
проверка, является ли указанный год високосным;
округление значения времени до ближайшего значения с заданной точностью; ■ преобразование чисел или строк в значения даты и времени заданного формата.
Для каждой задачи приведены одна или две функции VBA, а также несколько примеров, иллюстрирующих применение этих функций.
Сколько дней в указанном месяце?
Хотя не существует встроенной функции, которая позволяла бы определять количество дней в указанном месяце, эта задача несложная и имеется много способов ее решения. Зная месяц и год, можно выяснить продолжительность месяца с помощью оператора Select Case. Конечно, при этом обязательно знать и год, поскольку в високосные годы февраль длиннее.
Альтернативой (и, мы полагаем, правильной) является наиболее полное использование возможностей VBA. Поскольку VBA позволяет вычитать одну дату из другой, чтобы определять количество дней между датами, можно с помощью функции DateSerial определить первый день указанного и первый день следующего месяца, а затем вычесть первую дату из второй.
Функция dhDaysInMonth, которая представлена в примере 2.7, выполняет все необходимые вычисления. В качестве исходного значения задается определенная дата, а функция вычисляет количество дней соответствующего месяца. Если в этой функции, как и во многих других, вы не зададите дату, она посчитает, что речь идет о текущей дате и вычислит количество дней в текущем месяце.
Пример 2.7. Вычисление количества дней в заданном месяце
Function dhDaysInMonth(Optional dtmDate As Date = 0) As Integer
Определяет количество дней в указанном месяце.
If dtmDate = 0 Then
' Задал ли пользователь значение даты?
Если нет, используется текущая дата. dtmDate = Date
End If
dhDaysInMonth = DateSerial(Year(dtmDate), _
Month(dtmDate) + 1, 1) - _
DateSerial(Year(dtmDate), Month(dtmDate), 1)
End Function
|\Jt1 Совет: Хотя этот совет касается многих функций настоящей главы, для I I У данной функции он является ключевым. Как вы убедились, VBA допускает задание даты во многих форматах. Одним из них является формат #mm/yy#, т.е. в качестве даты можно задавать только месяц и год, a VBA посчитает, что вы имели в виду первое число месяца. При использовании функции dhDaysInMonth полезно иметь подобную возможность, поскольку это избавляет от необходимости следить и за значением дня.
Сколько понедельников в июне?
Если вашему приложению нужна информация о том, сколько определенных дней недели приходится на заданный месяц, решить эту задачу поможет функция dhCountDOWinMonth. Данная функция приведена в примере 2.8 и позволяет задать дату и (в качестве необязательного параметра) определенный день недели. Функция возвращает количество указанных дней недели в месяце, содержащем заданную дату. Если вы не указали значение, определяющее день недели, функция использует вместо него день недели, соответствующий заданной дате.
Пример 2.8. Подсчет количества определенных дней недели в месяце
Function dhCountDOWInMonth(ByVai dtmDate As Date, _
Optional intDOW As Integer = 0) As Integer
Вычисляет количество указанных дней недели в заданном месяце. Dim dtmFirst As Date Dim intCount As Integer Dim intMonth As Integer
If (intDOW < vbSunday Or intDOW > vbSaturday) Then ' При вызове не указан день недели
или этот параметр имеет недопустимое значение. intDOW = WeekDay(dtmDate) End If intMonth = Month(dtmDate)
Определение первого дня месяца. dtmFirst = DateSerial(Year(dtmDate), intMonth, 1) ' Перемещение значения переменной dtmFirst вперед до тех пор, 1 пока она не попадет на указанный день недели. Do While WeekDay(dtmFirst) <> intDOW dtmFirst = dtmFirst + 1
Loop
В данный момент переменная dtmFirst содержит первый
требуемый день недели в месяце. Нужно просто подсчитать, ' сколько этих дней недели встречается в месяце. intCount = 0
Do While Month(dtmFirst) = intMonth intCount = intCount + 1 dtmFirst = dtmFirst + 7
Loop
dhCountDOWInMonth = intCount
End Function
Функция dhCountDOWInMonth выполняет такие действия:
Проверяет допустимость параметров.
Находит первый день заданного месяца.
Изменяет дату в пределах заданного месяца до первого указанного дня недели.
Подсчитывает количество дней, добавляя приращение, равное 7 дням, до тех пор, пока дата "не выйдет за пределы" заданного месяца.
Функция начинает проверку допустимости параметров с параметра intDOW и убеждается в том, что его значение находится в диапазоне от vbSunday до vbSaturday. Если это не так, функция игнорирует значение данного параметра и использует вместо него день недели, соответствующий параметру dtmDate:
If (intDOW < vbSunday Or intDOW > vbSaturday) Then
При вызове не указан день недели
или этот параметр имеет недопустимое значение.
intDOW = WeekDay(dtmDate)
End If
Для определения первого дня месяца программа обращается к функции DateSerial:
intMonth = Month(dtmDate) dtmFirst = DateSerial(Year(dtmDate), intMonth, 1)
Затем программа изменяет значение даты до совпадения текущего дня недели с указанным значением. Это занимает всего несколько строк программы:
' Перемещение значения переменной dtmFirst вперед до тех пор, ' пока она не попадет на указанный день недели.
Do While WeekDay(dtmFirst) <> intDOW
dtmFirst = dtmFirst + 1
Loop
Наконец, для подсчета количества интересующих дней выполняется цикл с добавлением семи дней за один раз, пока значение даты не "выйдет за пределы" заданного месяца:
' В данный момент переменная dtmFirst содержит первый ' требуемый день недели в месяце. Нужно просто подсчитать, ' сколько этих дней недели встречается в пределах месяца. intCount = О Do While Month(dtmFirst) = intMonth
intCount = intCount + 1
dtmFirst = dtmFirst + 7 Loop
Для того чтобы проверить работу этой функции, можно воспользоваться следующим фрагментом программы:
If dhCountDOWInMonth(#12/99#, vbFriday) > 4 Then
MsgBox "В декабре 1999 года оказалось больше четырех пятниц!" End If
Округление значения времени до ближайшего значения с указанной точностью
Если вы занимаетесь созданием программного обеспечения, связанного с формированием расписаний, вам может потребоваться процедура округления значения времени до указанного числа минут. Например, нужно найти ближайшее значение с точностью до 5, 10, 15 или 30 минут. Решение этой задачи не совсем тривиально и осуществляется программой, представленной в примере 2.10.
Для вызова функции dhRoundTime необходимо задать ей значение даты и времени и точность округления. (Можно использовать любой делитель числа 60, однако чаще всего применяются значения 5, 10, 15, 20, 30 или 60.) Например, в результате такого вызова:
? dhRoundTime(#12:32:15#, 5)
получается значение
12:30:00
а в результате следующего вызова:
? dhRoundTime(#12:32:35#, 5)
значение
12:35:00
Если задать функции dhRoundTime полное значение даты и времени, она сохраняет дату, изменяя только составляющую времени.
Пример 2.10. Округление времени до ближайшего значения с указанной точностью
Function dhRoundTime(dtmTime As Date, intlnterval As Integer) As Date
1 Округляет значение времени, заданное переменной dtmTime, 1 до ближайшего значения с заданной переменной intlnterval точностью. Dim sglTime As Single Dim intHour As Integer Dim intMinute As Integer Dim Ingdate As Long
' Выделяет дату из полного значения даты и времени.
Ingdate = DateValue(dtmTime)
1 Выделяет временную часть полного значения даты и времени
1 в виде числа, например 11.5 для 11:30. sglTime = TimeValue(dtmTime) * 24
1 Отделяет значение часов и сохраняет его отдельно.
1 Int обрезает действительное число, а
' CInt округляет, поэтому используется Int. intHour = Int(sglTime)
1 Выделяет значение минут и округляет его до 1 ближайшего значения с указанной точностью. intMinute = CInt((sglTime - intHour) * 60) intMinute = CInt(intMinute / intlnterval) * intlnterval ' Восстанавливает исходное значение даты и времени, 1 округленное до ближайшего с указанной точностью. dhRoundTime = CDate(Ingdate + _ ((intHour + intMinute / 60) / 24)) End Function
По-видимому, эта процедура является самой сложной в данной главе, по крайней мере, с точки зрения производимых ею вычислений. На первом шаге она сохраняет отдельно дату из исходного значения даты и времени. Таким образом, эта составляющая не может быть изменена функцией:
' Выделяет дату из полного значения даты и времени. Ingdate = DateValue(dtmTime)
Затем процедура отделяет временную часть параметра и преобразует ее в десятичное число, умножая соответствующее значение на 24:
' Выделяет временную часть полного значения даты и времени ' в виде числа, например 11.5 для 11:30.
sglTime = TimeValue(dtmTime) * 24
Поскольку временная часть значения даты и времени является долей полного дня, взяв значение, представляющее 12:32:15 Р.М. (0.522395833333333), и, умножив его на 24, получим в результате 12.5375. Время в таком формате уже можно округлять.
Получив время в таком виде, функция отделяет значение часов, поскольку оно изменяться не будет.
Отделяет значение часов и сохраняет его отдельно.
Int обрезает действительное число, а
CInt округляет, поэтому используется Int.
IntHour = Int(sglTime)
На следующем шаге отделяется составляющая времени, которая представляет минуты, и умножается на 60, чтобы определить количество минут. Используя в качестве примера значение 12.5375, умножим дробную часть на 60 и преобразуем результат к целому числу. В итоге получим значение 32, соответствующее числу минут:
intMinute = CInt((sglTime - intHour) * 60)
Решающим шагом всей процедуры является округление количества минут с заданной точностью:
intMinute = CInt(intMinute / intlnterval) * intlnterval
Получив результат округления, на последнем шаге нужно восстановить полное значение даты и времени. Следующая строка программы прибавляет значение часов к значению минут, разделенному на 60, делит сумму на 24, чтобы получить соответствующую дробную часть значения даты и времени, прибавляет к результату сохраненное значение даты и возвращает полное значение даты и времени:
dhRoundTime = CDate(Ingdate + ((intHour + intMinute /60) / 24))
Эту процедуру полезно выполнить в пошаговом режиме, отслеживая текущие значения переменных на различных этапах работы программы. Попробуйте вызвать процедуру из окна отладки, задавая различные значения времени и делители числа 60 в качестве значений точности округления. Усвоив принцип работы функции dhRoundTime, вы сможете эффективно использовать ее во многих приложениях.
Преобразование чисел или строк в значения даты и времени заданного формата
Мир данных пока несовершенен, и они могут поступать в ваше приложение в различных форматах. Особенно много проблем возникает с датами, поскольку существует множество способов их отображения и форматирования. Если вашему приложению необходимо получать информацию из внешних источников, вы оцените две функции, представленные в этом разделе. Первая из них — dhCNumDate (пример 2.11) — предназначена для преобразования дат, записанных в виде числовых значений, в формат Date. Вторая функция — dhCStrDate (пример 2.12) — выполняет ту же задачу, но для форматированных строк.
Некоторые компьютерные системы хранят даты в виде целых чисел, например 19971231 (соответствует #12/31/1997#) или 52259 (соответствует #5/22/1959#). Функция dhCNumDate может преобразовывать эти значения в формат даты и времени VBA, если вы укажете формат заданного числа. Например, для выполнения преобразования напишите:
dtmBirthday = dhCNumDate(19971231, "YYYYMMDD")
Зная формат представляющего дату числа, функция может извлекать отдельные ее части.
Функция dhCStrDate выполняет практически то же самое, но в качестве исходных используются строковые значения. Например, если все даты поступают от центрального компьютера в формате ’’mmddyyyy”, для преобразования такой строки в значение типа Date можно выполнить следующее:
' Строка strOldDate содержит ”05221959".
dtmNewDate = dhCStrDate(strOldDate, "MMDDYYYY")
Пример 2.11. Преобразование
форматированных чисел в значения даты и времени
Function dhCNumDate(ByVai Ingdate As Long, _
ByVai strFormat As String) As Variant
' Преобразует числа в даты в зависимости от
' заданного формата и исходного числа.
В данном случае число и формат должны соответствовать
' друг другу, иначе получится бессмысленный результат.
Dim intYear As Integer
Dim intMonth As Integer
Dim intDay As Integer
Dim fOk As Boolean
fOk = True
Select Case strFormat
Case "MMDDYY"
intYear = Ingdate Mod 100
intMonth = Ingdate \ 10000
intDay = (Ingdate \ 100) Mod 100
Case "MMDDYYYY"
intYear = Ingdate Mod 10000
intMonth = Ingdate \ 1000000
intDay = (Ingdate \ 10000) Mod 100
Case "DDMMYY"
intYear = Ingdate Mod 100
intMonth = (Ingdate \ 100) Mod 100
intDay = Ingdate \ 10000
Case "DDMMYYYY"
intYear = Ingdate Mod 10000
intMonth = (Ingdate \ 10000) Mod 100
intDay = Ingdate \ 1*000000
Case "YYMMDD", "YYYYMMDD"
intYear = Ingdate \ 10000
intMonth = (Ingdate \ 100) Mod 100
intDay = Ingdate Mod 100
Case Else fOk = False End Select
If fOk Then
dhCNumDate = DateSerial(intYear, intMonth, intDay) Else
dhCNumDate = Null
End If
End Function
Совет: Функция dhCNumDate имеет интересное программное решение. При исходном значении типа 220459 (#4/22/59# в формате даты) выделение значения месяца требует определенных усилий. Программа выполняет это, используя сначала целочисленное деление (оператор \), в результате чего получается 2204. Затем для выделения только значения месяца программа применяет оператор Mod, чтобы определить остаток от деления 2204 на 100. Таким образом, применение оператора целочисленного деления и оператора Mod весьма полезно для выделения определенных разрядов числа, как это сделано в dhCNumDate.
Пример 2.12. Преобразование форматированных строк в значения даты и времени
Function dhCStrDate(strDate As String, Optional _ strFormat As String = "") As Date
Преобразует строку, содержащую значение даты, в
1 значение типа Date в соответствии со строкой формата, 1 описывающей информацию в строке даты.
Dim strYear As String
Dim strMonth As String
Dim strDay As String
Dim fDone As Boolean
Select Case strFormat
Case "MMDDYY", "MMDDYYYY" strYear = Mid(strDate, 5) strMonth = Left(strDate, 2) strDay = Mid(strDate, 3, 2)
Case "DDMMYY", "DDMMYYYY" strYear = Mid(strDate, 5) strMonth = Mid(strDate, 3, 2) strDay = Left(strDate, 2)
Case "YYMMDD" strYear = Left(strDate, 2) strMonth = Mid(strDate, 3, 2) strDay = Right(strDate, 2)
Case "YYYYMMDD" strYear = Left(strDate, 4) strMonth = Mid(strDate, 5, 2) strDay = Right(strDate, 2)
Case "DD/MM/YY", "DD/MM/YYYY" strYear = Mid(strDate, 7) strMonth = Mid(strDate, 4, 2) strDay = Left (strDate, 2)
Case "YY/MM/DD" strYear = Left(strDate, 2) strMonth = Mid(strDate, 4, 2) strDay = Right(strDate, 2)
Case "YYYY/MM/DD" strYear = Left(strDate, 4) strMonth = Mid(strDate, 6, 2) strDay = Right(strDate, 2)
Case Else
* Если ни один из форматов не подошел, преобразование ' выполняется с помощью CDate. Это может не сработать, 1 однако ничего лучше предложить здесь нельзя. dhCStrDate = CDate(strDate) fDone = True End Select If Not fDone Then
dhCStrDate = DateSerial(Vai(strYear), Vai(strMonth), Vai(strDay)) End If
End Function
Нет сомнений, обе приведенные выше программы действуют "в лоб". Указывая возможные варианты форматов в самих функциях, вы наверняка согласитесь, что, если возникнет необходимость, легко добавлять собственные новые форматы. В любом случае вопрос заключается лишь в использовании соответствующих математических и строковых функций для выполнения требуемых преобразований.
Работа с промежутками времени
Независимо от того, в какой мере вы хотели бы работать с промежутками времени, значения даты и времени VBA не предназначены для этой цели. В соответствии со своей структурой, значения даты и времени VBA хранят определенный момент времени, а не его промежуток, причем не существует возможности хранить в переменной даты и времени значение, большее 24 часов. Если вы хотите работать с промежутками времени, необходимо выполнять некоторые преобразования, сохраняя промежутки времени в виде данных числовых типов и преобразуя их обратно в форматированные значения для отображения. В других случаях при работе с промежутками времени просто возвращается целое число, определяющее количество единиц измерения времени (лет, дней, месяцев), которое прошло между двумя датами.
В этом разделе рассмотрено несколько стандартных аспектов работы с промежутками времени, а именно:
определение возраста человека, в годах, по заданному дню рождения;
форматирование значения промежутка времени с использованием строки, задающей формат;
форматирование суммарного времени.
Вычисление возраста
Вычисление возраста человека по заданной дате рождения — это обычная задача, возникающая при обработке данных. К сожалению, встроенные средства VBA не предоставляют совершенного и правильного метода определения возраста.
Первое, что приходит в голову, — воспользоваться для вычисления возраста такой формулой:
Age = DateDiff("уууу", Birthdate, Date)
Однако она не всегда дает правильный результат. Если день рождения в этом году еще не наступил, значение переменной Аде окажется большим на 1. Например, представьте, что вы родились 31 декабря 1950 года, а сегодня 1 октября 1999 года. Тогда при вычитании значений лет двух дат (1999 - 1950) получается 49, тогда как в действительности вам пока только 48.
Для того чтобы устранить такое противоречие, функция dhAge, которая приведена в примере 2.14, не только вычитает первое значение года из второго, но и проверяет, наступил ли день рождения в этом году. Если нет, то функция вычитает из результата 1, что дает правильное значение возраста.
Кроме того, функция dhAge позволяет задавать второй необязательный параметр: дату, на которую следует вычислять возраст. Если в качестве второго параметра ничего не задать, программа предположит, что в качестве конечного значения вы хотите использовать текущую дату. Таким образом, в результате следующего вызова функции:
intAge = dhAge(#5/22/59#)
вы получите текущий возраст человека, родившегося 22 мая 1959 года. Если вызвать функцию dhAge так:
intAge = dhAge(#5/22/59#, #1/1/2000#)
вы определите возраст этого человека на первый день 2000 года.
Пример 2.14. Определение возраста (метод 1)
Function dhAge(dtmBD As Date, Optional dtmDate As Date = 0) _
As Integer
Dim intAge As Integer
If dtmDate = 0 Then
' Задал ли пользователь значение даты?
* Если нет, используется текущая дата. dtmDate = Date
End If
intAge = DateDiff("yyyy" , dtmBD, dtmDate)
If dtmDate < DateSerial(Year(dtmDate), Month(dtmBD), _
Day(dtmBD)) Then intAge = intAge - 1 End If dhAge = intAge
Eno Function
Совет: Соблазнительно попытаться решить эту задачу путем целочисленного деления разницы двух дат в днях на количество дней в году, т.е. на 365,25. Такой алгоритм пригоден для многих сочетаний дат, однако не для всех. Поэтому его не стоит реализовывать. Представленные в этом разделе функции достаточно просты и являются приемлемой заменой кажущемуся интуитивно-понятным простому делению.
Если вы ищете наиболее простое решение, воспользуйтесь вместо приведенного другим вариантом функции dhAge, который представлен в примере 2.15. Он основан на том, что логическое значение True соответствует арифметическому значению -1, а логическое значение False — 0. Функция прибавляет к разности лет -1 или 0 в зависимости от заданной даты рождения.
Пример 2.15. Определение возраста (метод 2)
Function dhAge(dtmBD As Date, Optional dtmDate As Date = 0) _
As Integer
* Вычисляет возраст по заданной дате рождения и
* необязательному значению текущей даты.
If dtmDate = 0 Then
* Задал ли пользователь значение даты?
* Если нет, используется текущая дата. dtmDate = Date
End If
dhAge = DateDiff(”yyyy”, dtmBD, dtmDate) + _ (dtmDate < DateSerial(Year(dtmDate), Month(dtmBD), Day(dtmBD)))
End Function
Форматирование значения промежутка времени
VBA не поддерживает работу с промежутками времени и отображение отформатированных значений промежутков времени. Если вы хотите найти разницу между двумя значениями дат и отобразить ее в определенном формате, вы должны сделать это самостоятельно. Функция dhFormatInterval, представленная в примере 2.16, позволяет задать две даты и (что необязательно) спецификатор формата и возвращает строку, представляющую эту разницу. Поскольку у вас имеется готовая функция, вы можете использовать любой спецификатор формата, представленный в табл. 2.8. Конечно, существует возможность добавить к этому списку собственные спецификаторы, модифицировав исходную программу. (Информация о получении символа разделителя времени программным путем приведена ниже в разделе "Форматирование суммарного времени".)
Таблица 2.8. Предлагаемые спецификации формата для функции dhFormatlnterval
Формат
Пример
D Н
3 Days 3 Hours
D Н М
3 Days 2 Hours 46 Minutes
Формат
D Н М S
Пример
3 Days 2 Hours 45 Minutes 45 Seconds
D Н:ММ
3 Days 2:46
D НН:ММ
3 Days 02:46
D HH:MM:SS
3 Days 02:45:45
H M
74 Hours 46 Minutes
Н:ММ
74:46 (с первым нулем в значении минут при необходимости)
H:MM:SS
74:45:45
М S
4485 Minutes 45 Seconds
M:SS
4485:45 (с первым нулем в значении секунд при необходимости)
Пример 2.16. Форматирование интервала между двумя датами
Function dhFormatlnterval(dtmStart As Date, dtmEnd As Date, _
Optional strFormat As String = ”H:MM:SS") As String
* Возвращает разность между двумя моментами времени,
* отформатированную в соответствии со спецификатором,
* заданным переменной strFormat.
Dim IngSeconds As Long
Dim sngMinutes As Single
Dim sngHours As Single
Dim sngDays As Single
Dim intSeconds As Integer
Dim intMinutes As Integer
Dim intHours As Integer
Dim intRoundedHours As Integer
Dim intRoundedMinutes As Integer
Dim strDay As String
Dim strHour As String
Dim strMinute As String
Dim strSecond As String
Dim strOut As String
Dim IngFullDays As Long
Dim IngFullHours As Long
Dim IngFullMinutes As Long
Dim strDelim As String
* Если вы желаете использовать не локальный разделитель,
1 а специальный, замените следующую строку
* программы таким выражением:
* strDelim = ”:”
strDelim = GetTimeDelimiter()
* Вычисление количества секунд в интервале.
* Это ограничивает вычисления двумя миллиардами секунд * (приблизительно 68 лет), однако это не так уж плохо. 1 Затем вычисляется разница в минутах, часах и днях. IngSeconds = DateDiff("s", dtmStart, dtmEnd) sngMinutes = IngSeconds / 60 sngHours = sngMinutes / 60 sngDays = sngHours / 24
* Выделение значений полных часов и минут для последующего отображения. IngFullDays = Int(sngDays) IngFullHours = Int(sngHours) IngFullMinutes = Int(sngMinutes)
* Получение значений приращений для каждой единицы измерения. intHours = Int((sngDays - IngFullDays) * 24) intMinutes = Int((sngHours - IngFullHours) * 60) intSeconds = CInt((sngMinutes - IngFullMinutes) * 60)
* В некоторых случаях значения времени необходимо округлять.
* Две следующие строки программы используют тот факт, что логическое * значение True соответствует -1, а логическое значение False — 0. * Если следующее выражение истинно, программа должна * прибавить к значению 1, а если нет — 0. intRoundedHours = intHours - (intMinutes > 30) intRoundedMinutes = intMinutes - (intSeconds > 30)
strDay = ’’Days” strHour = "Hours” strMinute = ’’Minutes" strSecond = "Seconds"
If IngFullDays = 1 Then strDay = "Day" Select Case strFormat
Case ”D H"
If intRoundedHours = 1 Then strHour = "Hour” strOut = IngFullDays & ” ” & strDay &’’”&_ intRoundedHours & ” ” & strHour
Case ”D H M”
If intHours = 1 Then strHour = ’’Hour”
If intRoundedMinutes = 1 Then strMinute = ’’Minute" strOut = IngFullDays & ” ” & strDay & ” ” & _ intHours & " " & strHour & " ” & _ intRoundedMinutes &’’’’& strMinute
Case "D H M S"
If intHours = 1 Then strHour = "Hour"
If intMinutes = 1 Then strMinute = "Minute"
If intSeconds = 1 Then strSecond = "Second"
strOut = IngFullDays & " ” & strDay &’’’’& intHours & " " & strHour &"’’&_ intMinutes & ’’ " & strMinute &’’’’&_ intSeconds & " " & strSecond
Case ”D H:MM"Case ”D H:MM"3 Days 2:463 Days 2:46
Case ”D H:MM"
Case ”D H:MM"
3 Days 2:46
3 Days 2:46
strOut = IngFullDays & " ” & strDay & ” ” & _ intHours & strDelim & Format(intRoundedMinutes, ”00”)
Case ”D HH:MM" * 3 Days 02:46
strOut = IngFullDays & ” ” & strDay & " ” & _
Format (intHours, ”00”) & strDelim & _
Format(intRoundedMinutes, ”00”)
Case ”D HH:MM:SS” * 3 Days 02:45:45
strOut = IngFullDays & ” ” & strDay & ” ” & _
Format(intHours, ”00”) & strDelim & _
Format(intMinutes, ”00”) & strDelim & _
Format(intSeconds, ”00”)
Case ”H M”* 74 Hours 46 Minutes
If IngFullHours = 1 Then strHour = ’’Hour”
If intRoundedMinutes = 1 Then strMinute = ’’Minute” strOut = IngFullHours & ” ” & strHour & ” ” & _ intRoundedMinutes & ” ” & strMinute
Case ”H:MM”* 74:46
strOut = IngFullHours & strDelim & _
Format(intRoundedMinutes, ”00”)
Case ”H:MM:SS” * 74:45:45
strOut = IngFullHours & strDelim & _
Format(intMinutes, "00”) & strDelim & _
Format(intSeconds, ”00”)
Case ”M S”’ 4485 Minutes 45 Seconds
If IngFullMinutes = 1 Then strMinute = "Minute”
If intSeconds = 1 Then strSecond = ’’Second”
strOut = IngFullMinutes & ” ” & strMinute &’’”&__ intSeconds & ” ” & strSecond
Case ”M:SS”’ 4485:45
strOut = IngFullMinutes & strDelim & _
Format(intSeconds, "00”)
Case Else strOut = ”” End Select dhFormatlnterval = strOut End Function
Для проверки работы функции dhFormatlnterval можно написать тестовую процедуру, подобную представленной в примере 2.17. Процедура проверяет все предопределенные спецификаторы формата.
Пример 2.17. Тестовая процедура для функции dhFormatlnterval
Sub Testinterval() Dim dtmStart As Date Dim dtmEnd As Date
dtmStart = #1/1/99 12:00:00 PM# dtmEnd = #1/4/99 2:45:45 PM#
Debug.Print dhFormatlnterval(dtmStart, dtmEnd, "D H") Debug.Print dhFormatlnterval(dtmStart, dtmEnd, "D H M") Debug.Print dhFormatlnterval(dtmStart, dtmEnd, "D H M S”) Debug.Print dhFormatlnterval(dtmStart, dtmEnd, "D H:MM”) Debug.Print dhFormatlnterval(dtmStart, dtmEnd, "D HH:MM") Debug.Print dhFormatlnterval(dtmStart, dtmEnd, "D HH:MM:SS")
Debug.Print dhFormatlnterval(dtmStart, dtmEnd, ”H M") Debug.Print dhFormatlnterval(dtmStart, dtmEnd, ”H:MM”) Debug.Print dhFormatlnterval(dtmStart, dtmEnd, "H:MM:SS")
Debug.Print dhFormatlnterval(dtmStart, dtmEnd, ”M S") Debug.Print dhFormatlnterval(dtmStart, dtmEnd, ”M:SS”) End Sub
Нужно признать, что функция dhFormatlnterval работает "в лоб". Хотя мы пытались сделать программу проще, для обеспечения гибкости она требует выполнения нескольких шагов. Как она работает? Сначала функция вычисляет разность между двумя значениями даты в секундах, а затем определяет полное количество дней, часов и минут. Кроме того, она вычисляет остаток часов, минут и секунд, который при необходимости также может отображаться. Наконец, определяются округленные значения часов и минут. Таким образом, если вы решите не отображать секунды, значение минут будет соответственно округлено. То же самое происходит и с часами: если вы не хотите отображать минуты, значение часов округляется до ближайшего полного часа. При наличии всех этих значений, функция использует длинный оператор Select Case для определения типа отображаемой строки и формирует правильный результат.
VVВнимание: Поскольку функция dhFormatlnterval вычисляет разность между двумя датами в секундах и присваивает это значение переменной типа Long (длинное целое), вы ограничены промежутком времени приблизительно в 68 лет. Скорее всего, это ограничение не станет камнем преткновения, однако вам следует знать о нем, прежде чем использовать данную функцию в приложениях.
V
V
Форматирование суммарного времени
Как отмечалось, VBA не имеет средств хранения или измерения промежутков времени с помощью значений даты и времени. При вводе значения 8:30 вы можете иметь в виду, например, количество отработанных кем-то часов, но на самом деле вы вводите конкретное время: 8:30 А.М. текущего дня. У VBA не возникает сомнений относительно выполнения вычислений со значениями даты и времени с плавающей точкой, так что для выполнения вычислений препятствий нет — однако в результате может получиться совсем не то, что вы ожидали. Задача, таким образом, состоит в том, чтобы получить возможность вводить значения времени в привычном для вас виде. Вам необходимо преобразовывать их в некоторые обычные величины для вычислений, а затем форматировать результат как стандартную временную величину для отображения. Для выполнения этих операций вам понадобятся две представленные здесь функции — dhCMinutes и dhCTimeStr. Функция dhCMinutes получает в
качестве параметра значение даты и времени и возвращает его временную составляющую, преобразованную в соответствующее число минут. В таком виде легко суммировать последовательности значений времени. Затем для отображения суммарного результата вам понадобится функция dhCTimeStr, которая по заданному числу минут возвращает строку, представляющую результат в формате hh:mm. Например, нужно найти сумму следующих значений: 8:30, 12:30 и 13:25 (каждое из которых является промежутком времени). Для суммирования трех значений времени и преобразования суммы обратно в формат времени можно воспользоваться таким выражением:
dhCTimeStr(dhCMinutes(#8:30#) + dhCMinutes(#12:30#) + _ dhCMinutes(#13:25#))
Его результатом будет строка ”34:25”. Каждая функция состоит всего из нескольких строк. Функция dhCMinutes приведена в примере 2.18 и выделяет из даты составляющую времени при помощи функции Timevalue, а также умножает результат на 24*60, что соответствует количеству минут, представленному дробной частью значения даты и времени.
Пример 2.18. Преобразование
значения даты и времени в интервал минут
Function dhCMinutes(dtmTime As Date) As Long
1 Преобразует значение даты и времени в количество минут, прошедших с
1 полуночи (т.е. удаляет составляющую даты и работает только с 1 составляющей времени).
1 Возвращаемое значение может использоваться для ’ суммирования промежутков времени.
' Преобразует временную часть из доли суток в минуты. dhCMinutes = TimeValue(dtmTime) * 24 * 60
End Function
Функция dhCTimeStr, которая преобразует число минут в строку, отформатированную как значение времени (пример 2.19), также проста. Количество часов определяется из значения минут путем выполнения целочисленного деления (при помощи оператора \). Затем посредством оператора Mod определяется количество оставшихся минут (остаток от деления на 60). Функция форматирует каждое из этих значений и объединяет их в результирующую строку.
Пример 2.19. Преобразование
интервала минут в форматированную строку
Function dhCTimeStr(IngMinutes As Long) As String
Преобразует число минут в строку, 1 которая выглядит как значение времени. dhCTimeStr = Format(IngMinutes \ 60, ”0") & _ GetTimeDelimiter() & Format(IngMinutes Mod 60, ”00”) End Function
Здесь имеется только одна небольшая проблема: не все используют один и тот же символ в качестве разделителя времени. Встроенные спецификаторы формата VBA принимают это во внимание, однако в данном случае вы задаете собственный
формат. Конечно, можно запросить у Windows разделитель времени. Но загвоздка заключается в том, что получить такую информацию непросто, даже в 32-разрядных версиях Windows. Простейшим методом является выделение этой информации из файла WIN.INI, как вы, возможно, делали в Windows 3.1! Хотя ее можно получить прямо из реестра, данный путь требует намного больше усилий. Запросить данную информацию также можно, используя Windows API, однако и такой вариант нс самый простой. Поэтому функция GetTimeDelimiter (пример 2.20) обращается к файлу WIN.INI посредством вызова API-функции GetProf ileString, которая возвращает значение, установленное для параметра sTime в разделе [inti] этого файла. Таким образом, любая функция, выполняющая форматирование значений времени, может использовать ’’родной” разделитель.
Пример 2.20. Выделение локального разделителя времени
Private Function GetTimeDelimiter() As String
* Возвращает разделитель времени из файла WIN.INI. Как ни странно,
* но даже на сегодняшний день это единственный приемлемый путь
* решения данной задачи. Const conMaxSize = 10 Dim strBuffer As String Dim intLen As Integer
strBuffer = Space(conMaxSize)
intLen = GetProfileString("inti", "sTime", "", strBuffer, conMaxSize) GetTimeDelimiter = Left (strBuffer, intLen)
End Function
ffПримечание: Что? Использовать файл WIN.INI для получения значений, которые должны быть в реестре? Да, это так. Проблема состоит в том, что извлечь данную информацию из реестра посредством одной операции невозможно. Сначала необходимо определить идентификатор национальной настройки, а затем — найти требуемую информацию. Поскольку Windows гарантирует, что данная информация содержится в файле WIN.INI (или в соответствующих ключах реестра Windows NT), нет смысла пренебрегать возможностью получить ее простейшим способом посредством функции GetProfileString.
f
f
Резюме
Почти всем приложениям, написанным на VBA, рано или поздно приходится работать со значениями даты и времени. В этой главе подробно рассмотрены встроенные функции VBA для обработки значений даты и времени, а также описаны многие процедуры, использующие эти функции и обеспечивающие более широкие возможности при выполнении операций с датой и временем. В частности, затронуты такие вопросы:
представление значений даты и времени в VBA;
использование встроенных функций VBA для работы с датами и временем:
Date, Time, Now;
DatePart, WeekDay, Year, Month, Day, Hour, Minute, Second;
DateAdd, DateDiff;
DateValue, TimeValue, CDate;
DatesSerial, TimeSerial;
— Format.
дополнительные функции для:
определения конкретной даты;
операций с датами и временем;
работы с промежутками времени.
Используя функции, представленные в этой главе, а также владея навыками написания собственных дополнительных функций, вы сможете решить любую задачу, связанную с обработкой значений даты и времени. Аналогичные аспекты работы с текстовыми и числовыми данными рассмотрены в главах 1 и 3.
ГЛАВА 3
Операции с числами
Как VBA хранит числовые значения?
Встроенные числовые функции
Пользовательские математические и числовые функции
В процессе разработки собственного приложения рано или поздно вам придется работать с числами. Вы столкнетесь с проблемой, как хранить числовые значения, с которыми вы работаете, а также, возможно, захотите использовать встроенные функции VBA для работы с числовыми данными. Для расширения функциональных возможностей, которые предоставляет VBA, может возникнуть необходимость создавать собственные функции.
В этой главе рассказано, как VBA хранит и обрабатывает числовые данные, а также представлен обзор встроенных числовых функций. Затем описан ряд более сложных функций, использующих математические алгоритмы.
Как VBA хранит числовые значения?
В повседневной жизни люди привыкли пользоваться десятичной системой счисления главным образом потому, что у каждого из нас на руках подсеять пальцев. Ранние математики обнаружили, что пальцы являются удобным вычислительным инструментом, и большое количество предметов легче группировать по десять, а нс по два, не по восемь или любому другому числу. Однако компьютер, не имея десяти пальцев, не использует десятки. Для хранения и обработки информации он применяет систему счисления по основанию 2, или двоичную систему счисления. Поскольку в системе счисления по основанию 2 используются всего две цифры (0 и 1), это удобно, чтобы установить соответствие между числами и электронными схемами, где открытые и закрытые переключатели представляют значения 1 и 0 соответственно.
Как каждый разряд десятичного числа может содержать любую цифру от 0 до 9, каждый разряд двоичного числа, называемый битом, может содержать только 0 или I. Биты обычно группируются по 8. Такие группы называются байтами. Один байт может включать 256 комбинаций нулей и единиц, и, следовательно, представлять 256 различных чисел. Для представления больших значений требуется больше байтов:
При выборе способа хранения чисел в VBA нужно принимать во внимание два фактора. Во-первых, насколько большими должны быть числа? Если вы занимаетесь подсчетом звезд во Вселенной, вам понадобится работать с большими числами, чем при подсчете ног у поросят. Во-вторых, точность вычислений. При подсчете звезд во Вселенной вполне допустима ошибка в несколько миллионов, но при учете поросячьих пог вычисления должны быть совершенно точными. Вопрос о точности вычислений приобретает особое значение при работе с очень большими, а также дробными числами. Из-за того, что при вычислениях вы используете десятичные числа, а компьютер хранит их в двоичном представлении, по неосторожности можно наткнуться на подводные камни.
VBA поддерживает несколько типов данных для хранения числовых значений в переменных. Выбор типа переменной зависит от того, насколько большие значения ей придется содержать и насколько высока точность вычислений.
Основное эмпирическое правило, которым следует руководствоваться при выборе типа переменной, заключается в выборе наименьшего из возможных типов данных, соответствующих поставленной задаче. При этом желательно избегать использования типов данных с плавающей точкой (Single и Double). Например, если вы подсчитываете приплод крупного рогатого скота, который редко превышает четыре единицы на каждое животное, целесообразно применять данные типа Byte. (Максимальное значение переменной этого типа — 255.) Если дроби вам нужны лишь потому, что вы работаете с деньгами, воспользуйтесь типом данных Currency. При использовании типа Double только из предосторожности (поскольку он обеспечивает
наибольший возможный диапазон значений и точность вычислений) вы можете столкнуться с непредвиденными сложностями при выполнении операций с плавающей точкой.
Типы данных перечислены в табл. 3.1.
Таблица 3.1. Числовые типы данных VBA
Тип данных
Объем занимаемой памяти
Диапазон значений
Byte (байт)
I байт
от 0 до 255
Integer (целое)
2 байта
от -32768 до 32767
Long (длинное целое)
4 байта
от -2147483648 до 2147483647
Single
(с плавающей точкой обычной точности)
4 байта
от -3.402823Е38 до -1.401298Е-45 для отрицательных значений
и от 1.401298Е-45 до 3.402823Е38 для положительных
Double
(с плавающей точкой двойной точности)
8 байтов
от -1.79769313486232Е308 до -4.94065645841247Е-324 для отрицательных значений и от 4.94065645841247Е-324 до 1.79769313486232Е308 для положительных
Currency (масштабируемое целое)
8 байтов
от -922337203685477.5808
до 922337203685477.5807
Decimal
(в настоящее время поддерживается только в пределах типа Variant)
14 байтов
+/-79228162514264337593543950335
без дробной части;
+/-7.9228162514264337593543950335
с 28 знаками справа от десятичной точки; наименьшим ненулевым значением является +/-0.0000000000000000000000000001
Variant (для чисел)
16 байтов
Любое числовое значение вплоть до границ диапазона для типа Double
В следующих разделах подробно рассмотрены типы данных, приведенные в табл. 3.1, и соответствующие им диапазоны значений. Все типы данных можно разбить на три группы: типы данных, предназначенные для хранения целых чисел, типы данных, предназначенные для хранения дробных чисел и использующие арифметику с плавающей точкой, и смешанная группа, называемая масштабируемыми целыми, в которой для хранения дробных значений применяются целые числа.
Примечание: Тип данных Variant можно использовать для хранения значений любых других типов. VBA имеет функцию TypeName, которая определяет тип любого значения или переменной. Если функцию TypeName применить к значению типа Variant, то она определит подтип типа Variant. Функция TypeName использована в разделе "Числа с плавающей точкой и ошибки при их использовании".
Целые числа
Тип данных Byte является самым простым и наиболее ограниченным среди числовых типов данных. Переменные этого типа хранятся в виде 8-битовых двоичных чисел. Например, число 10 хранится в виде 00001010, т.е. один раз два в первой степени (2) плюс один раз два в третьей степени (8). Значения типа Byte не могут быть отрицательными. Максимальное значение, которое может принимать переменная этого типа, в двоичном формате равно 11111111, а в десятичном — 255.
Следует отметить еще одно различие между двоичными и десятичными числами. Кроме цифр от 0 до 9, в десятичной системе счисления для представления определенных значений используются два специальных символа: десятичная точка и знак минуса. Поскольку основное достоинство двоичной системы счисления состоит в том, что для представления числовых значений применяются только нули и единицы, были разработаны специальные методы представления дробных и отрицательных чисел без каких-либо специальных символов.
Например, для хранения одного значения типа Integer нужно 16 битов, один из которых служит для указания знака (положительного или отрицательного). Таким образом, для представления абсолютного значения числа остается 15 битов. Наибольшее число, которое можно представить пятнадцатью битами, равно 215-1, или 3T1G1. Почему именно 215-1, а не просто 215? Потому что одна комбинация нужна для представления нуля. Поскольку в отрицательном нуле необходимости нет, вместо него может быть представлено еще одно отрицательное число. Поэтому нижней границей диапазона является число -32768.
Тип данных Long, как и типы Byte и Integer, предназначен для хранения только целых чисел. Поскольку объем памяти для хранения переменной этого типа увеличен до 4 байтов (до 32 битов), наибольшим положительным числом будет 231-1 (приблизительно 2 миллиарда), а наименьшим отрицательным231.
I Ии Совет: Единственным отличием между типами данных для работы с целы- 7 ми числами — Byte, Integer и Long — является охватываемый ими диапа- зон значений. Для наиболее эффективного использования компьютерной памяти всегда выбирайте наименьший диапазон, способный охватить все значения, которые может принимать конкретная переменная. Единственный случай, когда для хранения целых чисел следует применять один из типов данных с плавающей точкой, — это при работе с числами, больше 2 миллиардов.
Числа с плавающей точкой и ошибки при их использовании
Двумя типами данных с плавающей точкой, которые часто служат причиной головной боли у разработчиков программного обеспечения, являются Single и Double. Чтобы понять, чем вызвана эта головная боль, нужно узнать, каким образом типы данных с плавающей точкой используют двоичные разряды для хранения потенциально очень больших чисел и дробей.
Тип данных Single применяет то же количество байтов, что и тип Long (4 байта), однако он использует эти 32 двоичных разряда совершенно по-другому. Метод представления данных типа Single и Double является промышленным стандартом, разработанным Институтом инженеров по электротехнике и электронике (IEEE).
Данный метод основан на так называемой арифметике с плавающей точкой. Всестороннее освещение этой темы выходит за рамки данной книги, но основной принцип, лежащий в основе метода, достаточно прост.
Числа с плавающей точкой подобны научной форме записи, при которой число представляется в виде произведения двух других чисел. Например, десятичное число 1500 может быть записано в виде 1.5*103 или 1.5ЕЗ, а число 0.0015 — в виде 1.5*10"3 или 1.5Е-3. При такой записи число состоит из двух частей: первая — это множитель, называемый мантиссой, а вторая — порядок. Целым числам соответствуют положительные порядки, а дробным — отрицательные. Количество разрядов мантиссы определяет точность вычислений, а максимальная величина порядка — допустимый диапазон значений.
В двоичных числах с плавающей точкой биты подразделяются на представляющие мантиссу и представляющие порядок. Например, тип Double использует 1 бит для знака (положительного или отрицательного), 11 бит для порядка и 52 бита для мантиссы.
Как видно из табл. 3.1, типы данных Single и Double способны хранить как огромные положительные и отрицательные числа, так и крошечные дробные значения. Но в отличие от типов данных Integer и Long, эти типы не могут хранить все возможные значения в пределах своих диапазонов. Определенные числа из допустимого диапазона, включая некоторые очень большие целые числа, нс могут быть представлены точно, поэтому они округляются до ближайшего допустимого значения. Так как в пределах любого заданного диапазона имеется бесконечное число дробных значений, всегда будет существовать бесконечное число очень точных дробей, которые также должны быть округлены.
Числа с плавающей точкой округляются и по другой причине: не все дроби можно точно представить двоичными числами. Конечно, десятичные числа также неспособны точно представить определенные дроби. Например, дробь 1/3 нельзя точно представить ни одной комбинацией степеней 10. Десятичное представление 1/3 — 0.3333333 — нс равно в точности 1/3 и никогда не будет равно, независимо от количества троек, после десятичной точки. Аналогично некоторые дробные числа, например 0.0001, можно точно записать только в десятичном, а нс в двоичном представлении. Просто не существует такой комбинации степеней 2, которая давала бы точное значение такой дроби. Округление чисел с плавающей точкой иногда приводит к ошибкам (пример 3.1).
Пример 3.1. Ошибки при выполнении операций с плавающей точкой
Sub TestFloatingPoints()
Dim inti As Integer
Dim sngSum As Single Dim dblSum As Double
For inti = 1 To 10000
sngSum = sngSum + 0.0001
Next inti
Debug.Print TypeName(sngSum) & ":"; sngSum
Будет напечатано "Single: 1.000054"
For inti = 1 To 10000 dblSum = dblSum + 0.0001 Next inti
Debug.Print TypeName(dblSum) & ":"; dblSum
* Будет напечатано "Double: .999999999999906"
End Sub
В процедуре TestFloatingPoints из примера 3.1 показаны две причины возникновения ошибок. Первая обусловлена тем, что округление может приводить к неправильным результатам выполнения математических операций, а вторая — тем, что одни и тс же операции могут давать различные результаты, зависящие от используемых типов данных с плавающей точкой. Причем применение типов данных Single и Double может не только привести к неправильным результатам, но и эти результаты будут отличаться между собой! Это означает, что если вы сравниваете число типа Single с числом типа Double и проверяете их на равенство, результат может быть отрицательным, даже когда числа одинаковы.
Еще большей неприятностью является то, что некоторые ошибки при округлении чисел с плавающей точкой могут оставаться скрытыми при отображении чисел, но при этом результаты операций сравнения могут противоречить законам логики. Например, в программе, представленной в примере 3.2, dbll равно sngl, sngl равно sng2, sng2 равно dbl2, однако dbll не равно dbl2!
Пример 3.2, Ошибки округления
Sub TestEquality()
Dim sngl As Single
Dim sng2 As Single
Dim dbll As Double
Dim dbl2 As Double
sngl = 69.82 sng2 = 69.2 + 0.62 dbll = 69.82
dbl2 = 69.2 + 0.62
Debug.Print "sngl = " & sngl & ", sng2 = " & sng2 * Будет напечатано "sngl = 69.82, sng2 = 69.82"
Debug.Print "dbll = " & dbll & ", dbl2 = " & dbl2 * Будет напечатано "dbll = 69.82, dbl2 = 69.82"
Debug.Print "dbll = sngl: "; (dbll = sngl)
Будет напечатано "dbll = sngl: True"
Debug.Print "sngl = sng2: ”; (sngl = sng2)
* Будет напечатано "sngl = sng2: True"
Debug.Print "sng2 = dbl2 : "; (sng2 = dbl2)
Будет напечатано "sng2 = dbl2: True"
Debug.Print "dbll = dbl2: "; (dbll = dbl2)
’ Будет напечатано "dbll = dbl2: False" !!!
End Sub
В разделе "Округление чисел" приведены алгоритмы, которые можно использовать для округления чисел с плавающей точкой с заданной точностью. Эти функции позволяют избежать скрытых ошибок округления, рассматриваемых в данном разделе. Другим способом предотвратить такие ошибки является применение по мерс возможности масштабируемых целых типов данных, описанных в следующем разделе.
Масштабируемые целые
Ошибки округления могут возникать при работе с десятичными дробями, нс имеющими точных двоичных эквивалентов. Во избежание подобных ошибок типы данных Currency и Decimal используют метод, который называется масштабированием целого числа и основан на том, что все десятичные целые числа в действительности имеют точные двоичные эквиваленты и любое из них можно точно представить в виде некоторой комбинации степеней 2. Масштабированные целые преобразуют десятичные дроби в целые числа, прежде чем сохранить их в двоичном виде, умножая их на число, достаточно большое для того, чтобы исключить десятичную точку.
Тип данных Currency можно использовать для хранения любых чисел, которые попадают в соответствующий диапазон и имеют не более четырех десятичных знаков. Число умножается на 10000, что исключает потребность в десятичной точке, а затем сохраняется в двоичном формате как целое. Это предотвращает ошибки округления, которые могут возникнуть при сохранении десятичных дробей в виде двоичных чисел с плавающей точкой. Процедура в примере 3.3 иллюстрирует применение типа данных Currency для решения проблем, связанных с использованием типов данных с плавающей точкой.
Пример 3.3. Устранение ошибок округления при помощи типа данных Currency
Sub Testcurrency()
Dim inti As Integer
Dim dblSum As Double
Dim curSum As Currency
For inti = 1 To 10000
dblSum = dblSum + 0.0001
Next inti Debug.Print TypeName(dblSum) & ":"; dblSum 1 Будет напечатано "Double: .999999999999906"
For inti = 1 To 10000
curSum = curSum 4- 0.0001
Next inti
Debug.Print TypeName(curSum) & ":"; curSum
* Будет напечатано "Currency: 1" End Sub
Встроенные числовые функции
VBA предоставляет широкий набор встроенных числовых функций, которые для удобства описания разбиты по категориям. Далее в этой главе рассматриваются методы и алгоритмы для выполнения более сложных вычислений, а также несколько задач, которые невозможно решить с использованием только встроенных функций.
Математические и тригонометрические функции
В табл. 3.2 перечислены встроенные математические и тригонометрические функции VBA. Каждой из этих функций в качестве аргумента, обозначенного в таблице как число, может быть передано любое допустимое числовое выражение.
Таблица 3.2. Математические и тригонометрические функции VBA
Функция
Описание
Синтаксис
Atn
Возвращает значение типа Double, определяющее угол, который является арктангенсом числа, в радианах
Atn (число), где число — это отношение катетов прямоугольного треугольника
COS
Возвращает значение типа Double, являющееся косинусом заданного угла
Cos (число), где число — это угол в радианах
Sin
Возвращает значение типа Double, являющееся синусом заданного угла
Sin (число), где число — это угол в радианах
Tan
Возвращает значение типа Double, являющееся тангенсом заданного угла
Tan (число), где число — это угол в радианах
Exp
Возвращает значение типа Double, являющееся результатом возведения числа е (основание натуральных логарифмов) в указанную степень (иногда эту функцию называют антилогарифмом)
Ехр (число) (если значение числа превышает 709.782712893, возникает ошибка)
Log
Возвращает значение типа Double, являющееся натуральным логарифмом числа
Log (число), где число — это любое допустимое выражение, большее нуля
Sqr
Возвращает значение типа Double, являющееся квадратным корнем числа
Sqr (число), где число — это любое допустимое выражение, большее или равное нулю
Sgn
Возвращает значение типа Variant (целое), указывающее знак числа
Sgn (число), где число — это любое допустимое числовое выражение
Тригонометрия — это математика прямоугольных треугольников. С ее помощью можно вычислять углы по известному отношению длин двух сторон прямоугольного треугольника, а также определять это отношение по известному значению угла. В качестве единиц измерения углов в VBA используются радианы. Поскольку 180 градусов соответствует тс радианам (число тс приблизительно равно 3.14159265358979), можно преобразовывать градусы в радианы, умножая значения в градусах на тс/180, и, наоборот, радианы в градусы, умножая значения в радианах на 180/тс. В примере 3.4 приведены функции, выполняющие эти преобразования.
Пример 3.4. Функции преобразования радиан в градусы и наоборот
Function dhConvertDegreesToRadians(dblDegrees As Double) _ As Double
* Преобразует градусы в радианы.
Const PI = 3.14159265358979
dhConvertDegreesToRadians = (dblDegrees / 180) * PI
End Function
Function dhConvertRadiansToDegrees(dblRadians As Double) _ As Double
' Преобразует радианы в градусы.
Const PI = 3.14159265358979
dhConvertRadiansToDegrees = (dblRadians / PI) * 180
End Function
Логарифмические функции
Логарифмические функции VBA работают с натуральными логарифмами. Натуральный логарифм — это логарифм по основанию е, где константа е приблизительно равна 2.718282. Логарифм произвольного числа х по основанию и можно вычислить путем деления натурального логарифма х на натуральный логарифм и, а именно:
Logn(x) = Log(x) / Log(n)
Ниже приведен пример, иллюстрирующий функцию для вычисления логарифмов по основанию 10:
Function dhConvertLoglO(dblDecimal As Double) As Double
* Преобразует десятичное число в логарифм по основанию 10.
dhConvertLoglO = Log(dblDecimal) / Log(10) End Function
Логарифмические преобразования можно обобщить для любого основания и следующим образом:
Function dhConvertLogN(dblDecimal As Double, _ dblLogBase As Double) As Double
Преобразует десятичное число в логарифм по основанию,
’ заданному переменной dblLogBase.
dhConvertLogN = Log(dblDecimal) / Log(dblLogBase) End Function
Определение знака
Функция Sgn возвращает целое число, которое указывает, чем является аргумент: положительным числом, отрицательным числом или нулем. Функция может возвращать значения 1, -1 или 0. Например, Sgn(3) возвращает 1, Sgn(-3) — -1, а Sgn (3-3) — 0. Как и для любой другой математической функции, которая принимает в качестве аргумента числовые выражения, при передаче функции Sgn значения Null возникает ошибка выполнения (ошибка 94 — Недопустимое использование Null).
Производные тригонометрические функции
Встроенные тригонометрические функции можно комбинировать, создавая более сложные функции. В табл. 3.3 приведены формулы, которые вы можете использовать для получения более сложных функций из функций, предоставляемых VBA.
Таблица 3.3. Производные тригонометрические функции
Функция
Производный эквивалент
Sec (секанс)
Cosec (косеканс) Cotan (котангенс) Arcs in (арксинус) Arccos (арккосинус) Arcsec (арксеканс) Arccosec (арккосеканс) Arccotan (арккотангенс) Hsin (гиперболический синус)
Sec(X) = I / Cos(X)
Cosec(X) = 1 / Sin(X)
Cotan(X) = I / Tan(X)
Arcsin(X) = Atn(X / Sqr(-X * X + I))
Arccos(X) = Atn(-X / Sqr(-X * X + 1)) + 2 * Atn(l)
Arcsec(X) = Atn(X / Sqr(X * X - 1)) + Sgn((X) - 1) * (2 * Atn(l))
Arccosec(X) = Atn(X / Sqr(X * X - 1)) + (Sgn(X) -1) * (2 * Atn( 1))
Arccotan(X) = Atn(X) + 2 * Atn(l)
HSin(X) = (Exp(X) - Exp(-X)) / 2
Heos (гиперболический косинус)
Htan (гиперболический тангенс)
HCos(X) = (Exp(X) + Exp(-X)) / 2
HTan(X) = (Exp(X) - Exp(-X)) / (Exp(X) + Exp(-X))
Hsec (гиперболический секанс)
Hcosec (гиперболический косеканс)
HCotan (гиперболический котангенс)
HArcsin (гиперболический арксинус)
HArccos (гиперболический арккосинус)
HArctan (гиперболический арктангенс)
HArcsec (гиперболический арксеканс)
HSec(X) = 2 / (Exp(X) + Exp(-X))
HCosec(X) = 2 / (Exp(X) - Exp(-X))
HCotan(X) = (Exp(X) + Exp(-X)) / (Exp(X) - Exp(-X))
HArcsin(X) = Log(X + Sqr(X * X + !))
HArccos(X) = Log(X 4- Sqr(X * X - 1))
HArctan(X) = Log((l + X) / (1 - X)) / 2
HArcsec(X) = Log((Sqr(-X * X + 1) + 1) / X)
HArccosec
(гиперболический арккосеканс)
HArccosec(X) = Log((Sgn(X) * Sqr(X * X + 1) + 1) / X)
HArccotan
(гиперболический арккотангенс)
HArccotan(X) = Log((X + 1) / (X - 1)) / 2
Ниже показано, как с помощью формул из табл. 3.3 можно формировать собственные тригонометрические функции:
Function dhHyperbolicSine(ByVai dblNumber As Double) As Double
Вычисляет гиперболический синус, используя функцию Exp.
dhHyperbolicSine = (Exp(dblNumber) - Exp(-dblNumber)) / 2 End Function
Преобразование и округление чисел
Как говорилось выше, различным числовым типам данных соответствует разная точность вычислений. Поэтому при преобразовании чисел из одного типа данных в другой часто автоматически происходит округление. Иногда число специально преобразуют в другой тип данных с целью округления. Однако существуют иные способы округления, которыми можно воспользоваться, не прибегая к преобразованию чисел в другой тип данных. В этом разделе описаны встроенные функции VBA для преобразования чисел, способы их использования при округлении, а также некоторые другие алгоритмы округления, которые могут вам понадобиться для получения требуемых результатов.
Функции преобразования типов
В табл. 3.4 приведен список функций VBA, выполняющих преобразование чисел из одного типа данных в другой. Процесс преобразования десятичных чисел в шестнадцатеричные и десятичных в восьмеричные описан ниже, в разделе "Преобразование основания системы счисления".
Таблица 3.4. Функции преобразования чисел
Функция
Тип возвращаемого значения
Округление
GByte(выражение)
Byte
До целого числа (...,5 округляется до четного числа)
CInt(выражение)
Integer
До целого числа (...,5 округляется до четного числа)
CLng(выражение)
Long
До целого числа (...,5 округляется до четного числа)
ОСиг(выражение)
Currency
До четырех десятичных знаков
CDec(выражение)
Decimal Variant
До переменного количества десятичных разрядов, в зависимости от величины числа
CSng(выражение)
Single
До ближайшего числа с плавающей точкой в диапазоне значений
CDbl(выражение)
Double
До ближайшего числа с плавающей точкой в диапазоне значений
CVar(выражение)
Variant Double для числового выражения; Variant Date, если выражение ограничено символами #; Variant String в остальных случаях
То же, что и Double для числовых значений
В качестве аргумента выражение функций преобразования чисел может выступать любое допустимое числовое или строковое выражение. Если переданное в функцию значение аргумента выражение не попадает в допустимый диапазон значений для соответствующего типа данных, возникает ошибка выполнения (ошибка 13 — Несоответствие типа или ошибка 6 — Переполнение).
Внимание: Для преобразования выражений в числа можно воспользоваться другой функцией VBA — Vai. Однако она обладает одним существенным недостатком. В отличие от функций, приведенных в табл. 3.4, функция val не обеспечивает совместимость с различными национальными настройками при преобразовании данных из одного типа в другой. Разные десятичные разделители, разделители тысяч и параметры денежных единиц правильно распознаются функциями преобразования в соответствии с национальной настройкой компьютера. Однако функция Val не имеет возможности использовать национальную настройку компьютера и поэтому может не распознать числа, записанные в соответствии со стандартами, нс принятыми в США.
Округление чисел
Функции cint и CLng, которые используются для преобразования в типы данных Integer и Long, округляют дробные значения до целых чисел. При задании в качестве исходного значения чисел, оканчивающихся на ...,5, эти функции выполняют округление иногда в большую сторону, а иногда — в меньшую. В результате округления всегда получается четное число. Например, результатом и cint (1.5), и cint (2.5) будет число 2.
Функция CCur, которая преобразует числа в тип данных Currency, округляет числа с точностью до четырех десятичных знаков. К сожалению, фирма Microsoft не документировала правило, используемое при округлении значений типа Currency с пятью десятичными разрядами, где пятой цифрой является 5. Иногда эти числа округляются в сторону увеличения, а иногда — в сторону уменьшения. Из табл. 3.5 ясно, что в данном случае четкой зависимости при округлении не отслеживается.
Таблица 3.5. Непредсказуемость результатов округления для типа Currency
Наберите в окне отладки
Результат
? ССиг(.00005)
0.0001
? CCur(.000l5)
0.000 J
? CCur(.00025)
0.0003
? CCur(.00035)
0.0003
? CCur(.00045)
0.0004
? CCur(.00095)
0.0009
? CCur(.00995)
o.oi
? CCur(.00895)
0.0089
?CCurC0l895)
0.019
Поскольку такое, кажущееся случайным, округление может оказаться причиной неточности в ваших вычислениях, лучше выполнять округление чисел до заданного количества десятичных знаков самостоятельно, а не позволять VBA делать это при помощи функции ccur. Дальше будет приведена пользовательская функция dhRound, которая обеспечивает предсказуемые результаты округления до заданного количества десятичных знаков.
Две функции VBA, Int (число) и Fix (число), удаляют дробную часть числа. Они не округляют число, а просто отбрасывают все цифры справа от десятичной точки. Обе функции возвращают значение типа Integer, если результат попадает в диапазон значений этого типа (от -32768 до 32767), и значение типа Long, если результат выходит за пределы диапазона значений типа Integer, но остается в пределах диапазона значений типа Long (от -2147483648 до 2147483647). При работе с положительными числами не имеет значения, какую из двух функций использовать, но при работе с отрицательными следует помнить, что функция Int возвращает ближайшее отрицательное целое, меньшее или равное числу, тогда как функция Fix возвращает ближайшее отрицательное целое, большее или равное чис . . В табл. 3.6 представлены результаты выполнения функций Int и Fix в окне отладки.
Таблица 3.6. Использование функций Int и Fix
Наберите в окне отладки Результат
-10-10-999-999? Int(-9.4)
-10
-10
-9
9
9
-9
9
9
? Fix(-9.4)
? lnt(9.6)
? Fix(9.6)
Примечание: Функция Fix (число) эквивалентна выражению Sgn (число) * Int(Abs(число)).
Избегайте использования функции int выражениями в качестве аргумента, так как иногда это приводит к неожиданным результатам. Например, значением выражения int ((1.55*10) +. 5) будет, как и ожидалось, число 16, а значением выражения int ((2.55*10)+.5) — число 25, хотя, казалось бы, должно получиться 26. Поэтому лучше сначала присвоить значение выражения переменной, а затем задать эту переменную в качестве аргумента функции Int (пример 3.5).
Пример 3.5. Использование переменной для управления функцией Int
Sub Testlnt () Dim dblNumber As Double
Debug.Print Int((2.55 * 10) + 0.5) ’ Печатает "25".
dblNumber = (2.55 * 10) + 0.5
Debug.Print Int(dblNumber) ’ Печатает "26". End Sub
Как говорилось ранее, округление часто приводит к проблемам при работе с числами с плавающей точкой, поскольку результаты округления некоторых десятичных чисел при преобразовании в двоичные числа с плавающей точкой могут быть непредсказуемыми. Как это ни удивительно, но VBA не имеет встроенной функции для округления Round, поэтому приходится писать собственную. В примере 3.6 представлен весьма распространенный алгоритм округления, позволяющий указывать количество десятичных знаков, с точностью до которого необходимо производить округление. Эта функция всегда округляет числа, содержащие 5 после указанного количества знаков, в сторону увеличения. Например, при округлении до одного десятичного знака число 1.25 округляется до 1.3.
Пример 3.6. Общая функция округления
Function dhRound(dblNumber As Double, _ intDecimals As Integer) As Double
' Округляет число до заданного количества десятичных знаков.
' 0.5 округляется в сторону увеличения.
Dim dblFactor As Double
Dim dblTemp As Double
dblFactor = 10 A intDecimals
dblTemp = dblNumber * dblFactor + 0.5 dhRound = Int(dblTemp) / dblFactor End Function
Совет: Вы можете округлять числа до определенного количества знаков слева от десятичной точки (например, округлить число 1234 до 1200). Для этого значение, соответствующее количеству десятичных знаков, должно быть отрицательным. Таким образом, если задать значение переменной intDecimals равным 0, происходит округление до единиц, при значении -1 — округление до десятков, при значении -2 — до сотен, -3 — до тысяч.
Вычитание чисел с плавающей точкой с сохранением точности
Вы, возможно, думаете, что у VBA не должно быть проблем с выполнением простой операции вычитания, поскольку ее результат не может иметь большего количества десятичных знаков, чем любой из двух операндов. Вы ошибаетесь. В табл. 3.8 приведено несколько удивительных результатов вычитания различных десятичных чисел, каждый из которых, казалось бы, должен равняться 0.1. Во избежание ошибок при вычитании сначала нужно округлить все числа до требуемого количества десятичных знаков.
Таблица 3.7. Ошибки при вычитании чисел с плавающей точкой
Наберите в окне отладки
Результат
? 100.8-Ю0.7
9.99999999999943 Е-02
? Ю.8-Ю.7
0.100000000000001
? 1.8-L7
0.1
Вы можете вычитать числа с плавающей точкой одно из другого, предварительно установив максимальное количество включаемых десятичных знаков, так как разность двух чисел не может иметь большего количества десятичных знаков, чем каждое из двух исходных чисел. Мы написали специальную функцию для вычитания (пример 3.7), предотвращающую возможные ошибки округления.
Пример 3.7. Предотвращение ошибок при вычитании
Function dhSubtract(dblVall As Double, _ dblVal2 As Double) As Double
' Вычисляет разность между dblVall и dblVal2,
предотвращая возможные погрешности.
Dim dblResult As Double
Dim intDecimalsl As Integer
Dim intDecimals2 As Integer
Dim intMaxDecimals As Integer
Dim dblTemp As Double
dblResult = dblVall - dblVal2
' Определение количества десятичных знаков в первом числе.
If InStr(dblVall, ". ") = 0 Then intDecimalsl = О
Else
intDecimalsl = Len(dblVall & "") - InStr(dblVall, ".")
End If
* Определение количества десятичных знаков во втором числе.
If InStr(dblVal2, ".") = 0 Then intDecimals2 = О
Else
intDecimals2 = Len(dblVal2 & "") - InStr(dblVal2, ".") End If
If intDecimalsl + intDecimals2 > 0 Then
* Округление чисел до максимального количества десятичных знаков. intMaxDecimals = Ilf(intDecimalsl > intDecimals2, _ intDecimalsl, intDecimals2)
dblTemp = dblResult * 10 A intMaxDecimals + 0.5
dblResult = Int(dblTemp) / (10 л intMaxDecimals)
End If dhSubtract = dblResult End Function
Кроме того, можно воспользоваться новым типом данных Decimal, который поддерживает широкий диапазон значений и обеспечивает высокую точность вычислений, чтобы избежать ошибок при выполнении операций вычитания с плавающей точкой. Функция, представленная в примере 3.8, использует подтип Decimal типа Variant для выполнения точного вычитания значений с плавающей точкой.
Пример 3.8. Использование подтипа Decimal типа Variant для вычитания
Function dhSubtractDec(dblVall As Double, __
dblVal2 As Double) As Double
* Использует подтип Decimal типа Variant для вычисления
' разности между dblVall и dblVal2,
' предотвращая возможные погрешности.
Dim varDecVall As Variant
Dim varDecVal2 As Variant
varDecVall = CDec(dblVall)
varDecVal2 = CDec(dblVal2)
dhSubtractDec = varDecVall - varDecVal2
End Function
Случайные числа
При генерации случайных чисел часто возникают недоразумения, поскольку оператор Randomize:
Randomize[число]
и функция Rnd:
Rnd[ (число) ]
имеют похожий синтаксис.
VBA генерирует случайные числа, начиная с некоторого опорного значения и прогоняя его затем через определенный алгоритм, формирующий число, большее или равное 0 и меньшее 1. Если начинать с одного опорного числа, в результате всегда будет получаться одно и то же "случайное" число. Оператор VBA Randomize инициализирует функцию Rnd, создавая для нее новое опорное число. Если не задать необязательный аргумент оператора Randomize, то в качестве нового опорного числа используется значение, возвращаемое системным таймером.
Если вы решили вообще не применять оператор Randomize, а просто вызывать функцию Rnd без аргумента, то Rnd всегда будет использовать одно и то же число в качестве опорного при первом вызове. При каждом последующем вызове Rnd в качестве нового опорного значения берется последнее сгенерированное число. Таким образом, если вы не используете оператор Randomize и не задаете аргумент функции Rnd, вы всегда будете получать одинаковую последовательность чисел. Зависимость значения, возвращаемого функцией Rnd, от аргумента число представлена в табл. 3.8.
Таблица 3.8. Аргументы функции Rnd
Аргумент функции Возвращаемое значение
< 0Возвращает случайное число в зависимости от заданного значения
аргумента
> оСледующее случайное число в последовательности, независимо от
заданного положительного значения аргумента
= оПовторяется последнее сгенерированное число
Не заданСледующее случайное число в последовательности (как и при зада
нии положительного аргумента)
Число, которое возвращает функция Rnd, имеет тип Single, и его значение больше или равно нулю и меньше единицы. Если вы хотите получать случайные целые числа в определенном диапазоне значений, можно воспользоваться следующей формулой:
i = Int((<верхняя граница> - <нижняя граница> + 1) * Rnd + <нижняя граница^
Допустим, вам нужно сформировать последовательность случайных чисел в диапазоне от 1 до 10. Тогда это выражение будет иметь такой вид:
i = Int ((10 - 1 + 1) * Rnd + 1)
При использовании в качестве верхней границы числа 10 очень широкого диапазона случайных чисел не получится, и после выполнения этой процедуры несколько раз они начнут повторяться. Распространенным заблуждением является мнение, что применение оператора Randomize перед функцией Rnd предотвратит дублирование, однако это не так. Оператор Randomize всего лишь сбрасывает датчик случайных чисел, и отсчет начинается с другого случайного числа из набора генерируемых чисел. И ничто не может помешать ему дублировать числа в заданной последовательности. Приведенная ниже процедура вырабатывает набор случайных чисел:
Dim i As Integer
Randomize
For i = 1 To 10
Debug.Print Int(Rnd * 10) + 1;
Next i
При пятикратном выполнении этой процедуры в окне отладки могут получиться такие результаты:
271210 4110 57
6281852287
6972310 610 42
596854410 16
9593372956
Значит, чтобы избежать дублирования в списке целых чисел, вы должны следить за ними самостоятельно. Ниже представлена процедура (пример 3.9), которая "перетасовывает" числа от 1 до 10, вырабатывая в результате случайный набор из десяти целых чисел без повторений.
Пример 3.9. Генерация случайных чисел без дублирования
Sub dhRandomShuffle() ' Перетасовка десяти случайных чисел. Dim aintValues(l То 10) As Integer Dim inti As Integer Dim intPos As Integer * Заполнение исходных значений. For inti = 1 To 10 aintValues(inti) = inti Next inti ' Цикл по всем элементам за исключением последнего.
По достижении последнего элемента использовать Rnd нет смысла.
For
inti = 10 To 2 Step -1
' Получение случайного числа от 1 до inti.
intPos = Int(Rnd * inti) + 1
Debug.Print aintValues(intPos)
aintValues(intPos) = aintValues(inti)
Next inti
' Получение последнего значения.
Debug.Print aintValues(1)
End Sub
Финансовые функции
VBA предоставляет ряд встроенных функций, которые можно использовать для выполнения финансовых расчетов. Они разбиты на три основные группы: функции учета амортизации, функции учета отчислений и функции учета денежных потоков. Каждая из этих групп описана в последующих разделах.
Функции учета амортизации
Функции учета амортизации используются в бухгалтерском деле для представления в денежном выражении обесценивания основных средств за определенный период времени. Например, фирме, являющейся владельцем грузового автомобиля, необходимо рассчитать сумму ежегодной амортизации грузовика для определения его текущей стоимости в любой момент времени. Поскольку амортизация влияет на размеры налогов, правительства часто устанавливают обязательные формулы, которые следует применять для расчета амортизации. Например, при начислении амортизации методом двукратного учета используется следующая формула:
Амортизация / период времени = ((начальная стоимость - остаточная стоимость) *2) / период эксплуатации
В табл. 3.9 приведена сводка функций VBA для учета амортизации и их аргументов, а в табл. 3.10 — описание этих аргументов.
Таблица 3.9. Функции учета амортизации
Функция
Описание
DDB(cost, salvage, period[, factor])
life,Возвращает значение типа Double, указывающее
степень амортизации имущества за заданный период времени с использованием метода двукратного учета амортизации
SLN(cost, salvage,
life)Возвращает значение типа Double, указывающее
степень амортизации за единицу времени по методу с равномерной амортизацией
SYD(cost, salvage,
life, period) Возвращает значение типа Double, указывающее степень амортизации за заданный период времени по методу с линейной амортизацией
АргументCost Salvage LifePeriod [factor]АргументCost Salvage LifePeriod [factor]Описание
Аргумент
Cost Salvage Life
Period [factor]
Аргумент
Cost Salvage Life
Period [factor]
Начальная стоимость фондов
Стоимость фондов к концу периода эксплуатации
Длительность периода эксплуатации; задается в тех же единицах измерения, что и period
Период времени, для которого рассчитывается степень амортизации Необязательный параметр, учитывающий фактор расчета амортизации (если данный аргумент опущен, по умолчанию используется значение 2 — двойное убывание)
Функции учета отчислений
Отчисления — это выплаты, которые представляют собой либо доход от инвестиции, либо погашение займа. Отрицательные значения свидетельствуют о необходимости произвести выплату, например по кредиту, а положительные — о получении денег, например дивидендов. В табл. 3.11 и 3.12 приведены функции VBA для учета отчислений и их аргументы.
Таблица 3.11. Функции учета отчислений
ФункцияФункцияОписание
Функция
Функция
FV(rate, nper, pmt[, pv[, type]])
Возвращает значение типа Double, указывающее сумму отчислений на основе регулярных фиксированных платежей при фиксированной процентной ставке
Rate (nper, pmt, pv[, fv[, type[, guess]]])
Возвращает значение типа Double, указывающее процентную ставку, необходимую для получения указанной суммы за определенный срок путем регулярных взносов
NPer(rate, pmt, pv[, fvf, type]])
Возвращает значение типа Double, указывающее количество платежей, необходимых для накопления заданной суммы при фиксированной процентной ставке
IPmt(rate, per, nper, pv[, fv[, type]])
Возвращает значение типа Double, указывающее часть регулярного платежа, приходящуюся на выплату процентов для данного периода отчислений
Pmt(rate, nper, pv[, fv[, type]])
Возвращает значение типа Double, указывающее размер регулярных платежей, необходимых для получения нужной суммы
PPmt(rate, per, nper, pv[, fv[, type]])
Возвращает значение типа Double, указывающее часть регулярного платежа, приходящуюся на выплату собственно займа для данного периода отчислений
PV(rate, nper, pmt[, fv[, type]])
Возвращает значение типа Double, указывающее в данный момент сумму будущих отчислений на основе регулярных фиксированных платежей при фиксированной процентной ставке
Аргумент Описание
RateПроцентная ставка за период времени (следует использовать ту же единицу
измерения времензи, что и для аргумента Nper)
NperОбщее количество периодов выплаты отчислений
PmtРазмер выплаты за период
PerНомер периода (в диапазоне от 1 до Nper)
PvСумма выплат на текущий момент, которые должны быть произведены в
будущем
[Fv]Необязательное значение, указывающее результат, который должен быть
достигнут по завершении выплат (если этот параметр опущен, по умолчанию используется 0, что является обычным результатом выплаты займа)
[Туре|Необязательный параметр, указывающий режим взимания платежей: 0 —
платежи вносятся в конце каждого периода и 1 — платежи вносятся в начале каждого периода (если этот параметр опущен, по умолчанию используется 0)
[Guess]Необязательный параметр, содержащий оценку значения, возвращенного
функцией Rate (если аргумент опущен, по умолчанию используется значение 0,1 — 10%)
Мы написали процедуру, которая представлена в примере 3.10, для расчета ежемесячных выплат по займу с использованием функции Pmt.
Пример 3.10. Расчет выплат по займу
Function dhCalcPayment (ByVai dblRate As Double, ByVai intNoPmts _
As Integer, ByVai curPresentValue As Currency, Optional _ varFutureVal As Variant = 0, Optional varWhenDue As _ Variant = 0) As Double
' Расчет выплат с использованием функции Pmt.
If varWhenDue о 0 Then
' Принимает единственное альтернативное значение 1,
указывающее, что выплата происходит в начале периода. varWhenDue = 1
End If
dhCalcPayment = Pmt((dblRate / 12), intNoPmts, _
-CDbl(curPresentValue), varFutureVal, varWhenDue)
End Function
Функции учета денежных потоков
Функции учета денежных потоков выполняют финансовые расчеты на основе последовательности периодических выплат и поступлений. Как и для функций учета отчислений, отрицательные числа представляют расход, а положительные — приход. Однако в отличие от функций отчислений, функции учета денежных потоков позволяют задавать различные значения выплат или поступлений в ходе работы с займами или инвестициями. Выплаты и поступления можно даже смешивать в рамках последовательности операций с денежными потоками.
Функция
Описание
IRR(values() [, guess])
Возвращает значение типа Double, указывающее норму прибыли для последовательности периодических финансовых операций
MIRR(values (),
Возвращает значение типа Double, указывающее модифициро
finance_rate,
ванную норму прибыли для последовательности периодических
reinvest_rate)
финансовых операций
NPV(rate, values ())
Возвращает значение типа Double, указывающее текущую вели-
чину инвестиции на основе ряда периодических финансовых операций и дисконтной ставки
Таблица 3.14. Аргументы функций учета денежных потоков
Аргумент
Описание
values ()
Массив значений денежных потоков; этот массив должен содержать по крайней мере одно отрицательное значение (расход) и одно положительное (приход)
Rate
Дисконтная ставка за период времени, выраженная десятичным числом
finance_rate
Процентная ставка выплат за предоставление финансирования
reinvest_rate
Процентная ставка от реинвестиций полученных денег
[guess]
Необязательный параметр, содержащий оценку значения, возвращенного функцией irr (если аргумент опущен, по умолчанию используется значение 0.1 — 10%)
Массив хранит последовательности значений денежных потоков. Подробная информация о работе с массивами содержится в главе 4.
Функция irr получает окончательный результат путем многократных итераций. Этот процесс начинается с исходного значения guess и продолжается до тех пор, пока не будет достигнут результат с точностью 0.00001%. Если после 20 итераций удовлетворительный результат не получен, функция выдает ошибку.
Пример 3.11 иллюстрирует использование функции NPV для расчета текущей чистой стоимости инвестиций.
Пример 3.11. Расчет текущей чистой стоимости инвестиции
Function dhNetPresentValue(ByVai dblRate As Double, _ ParamArray varCashFlows()) As Double
* Вычисляет текущее чистое значение.
Dim varElement As Variant
Dim i As Integer
Dim IngUBound As Long
Static dblValues() As Double
' Определяет верхнюю границу varCashFlows.
IngUBound = UBound(varCashFlows)
' Устанавливает размер dblValues равным размеру varCashFlows. ReDim dblValues(IngUBound) i = 0
* Переносит элементы varCashFlows в dblValues.
For Each varElement In varCashFlows dblValues(i) = varElement i = i + 1
Next
dhNetPresentValue = NPV(dblRate, dblValuesO)
End Function
Преобразование основания системы счисления
Для преобразования чисел в шестнадцатеричное, восьмеричное и десятичное представление проще всего воспользоваться встроенными функциями Hex, Oct и Vai, приведенными в табл. 3.15.
Таблица 3.15. Функции преобразования в другую систему счисления
Функция Описание
Нех (число)Возвращает значение аргумента число в шестнадцатеричном представлении
(тип String)
Oct (число) Возвращает значение аргумента число в восьмеричном представлении (тип
Variant) длиной до 11 восьмеричных символов; возвращает Null, если исходное значение Null, и 0, если исходным значением является Empty (значение Empty может принимать только переменная типа variant, которой не было присвоено никакого значения)
Vai (строка) Возвращает числа, содержащиеся в строковом аргументе, как числовые зна-
чения соответствующего типа; пробелы, символы табуляции и символы перевода строк из аргумента удаляются (функция прекращает выполнение при чтении первого символа аргумента, который она не распознает как число)
Шестнадцатеричные и восьмеричные преобразования
Функции Нех и Oct возвращают строку с шестнадцатеричным или восьмеричным значением исходного числа. Однако эта строка не содержит префиксов основания системы счисления &Н или &О. Например, Нех (255) возвращает строку "FF”, а не " &HFF", которая соответствует внутреннему представлению этого числа в программе. Если вы преобразовали число в шестнадцатеричное или восьмеричное представление, а теперь хотите снова вернуться к десятичной системе счисления, не забудьте добавить соответствующий префикс, иначе функции преобразования VBA, например функция CLng, не смогут правильно распознать заданное число, что видно из табл. 3.16.
Таблица 3.16. Преобразование в шестнадцатеричное и обратно в десятичное представление
Наберите в окне отладкиРезультат
? Нех(255)FF
? CLng(Hex(255))Ошибка 13 — Несоответствие типа
? CLng(”&H" & Нех(255))255
Примечание: Функция Нех перед преобразованием округляет дробные значения до ближайшего целого числа. Например, результатом выполнения Нех (256) будет 100, и Нех (256.4) также возвращает 100. Несмотря на то, что дробные числа можно представлять в шестнадцатеричном формате, на практике в этом нет необходимости. Функции преобразования VBA, например CLng, распознают только целые шестнадцатеричные числа.
Двоичные преобразования
VBA не имеет встроенных функций двоичного преобразования. Специально написанные программы приведены в примере 3.12 и могут использоваться для преобразования шестнадцатеричных чисел в двоичные, двоичных чисел в шестнадцатеричные, десятичных чисел в двоичные и двоичных в десятичные.
Пример 3.12. Функции двоичного преобразования
Function dhBinaryToDec(ByVai strNumber As String) As Long dhBinaryToDec = Val("&H" & dhBinarytoHex(strNumber))
End Function
Function dhBinaryToHex(ByVai strNumber As String) As String
Dim strTemp As String
Dim inti As Integer
Dim intLen As Integer
Dim strOut As String
Сначала строка strNumber дополняется слева нулями.
Для этого определяется длина строки, округленная
до ближайшего большего значения, кратного 4.
intLen = Len(strNumber)
Определение ближайшего большего значения, кратного 4.
intLen = Int(intLen /4+1) *4
strNumber = Right$(String(intLen, "0") & strNumber, intLen)
Теперь строка рассматривается по группам из 4 цифр с преобразованием * каждой из них в шестнадцатеричное число.
For inti = 1 То intLen Step 4
Select Case Mid(strNumber, inti, 4)
Case "0000"
strTemp = "0"
Case "0001"
strTemp = "1"
Case "0010"
strTemp = "2"
Case "0011"
strTemp = "3"
Case "0100"
strTemp = "4"
Case "0101"
strTemp = "5"
Case "0110"
strTemp = "6"
Case "0111" strTemp = "7" Case "1000" strTemp = "8" Case "1001" strTemp = "9" Case "1010" strTemp = "A" Case "1011" strTemp = "B” Case "1100" strTemp = "C" Case "1101" strTemp = "D" Case "1110" strTemp = "E" Case "1111" strTemp = "F" End Select strOut = strOut & strTemp Next inti dhBinarytoHex = strOut End Function Function dhDecToBinary(ByVai IngNumber As Long) As String Dim strTemp As String Dim inti As Integer strTemp = Hex(IngNumber) strTemp = dhHexToBinary(strTemp) ’ Удаление нулей в начале числа. Do While Left(strTemp, 1) = "0" strTemp = Mid(strTemp, 2) Loop dhDecToBinary = strTemp End Function Function dhHexToBinary (strNumber As String) As String Dim strTemp As String Dim strOut As String Dim i As Integer For i = 1 To Len(strNumber) Select Case Mid(strNumber, i, 1) Case "0" strTemp = "0000" Case "1" strTemp = "0001" Case "2" strTemp = "0010" Case "3" strTemp = "0011" Case "4" strTemp = "0100" Case "5" strTemp = "0101"
Case "6"
strTemp = ”0110” Case "7"
strTemp = "0111" Case "8"
strTemp = "1000" Case "9"
strTemp = "1001" Case "A"
strTemp = "1010" Case "B"
strTemp = "1011" Case "C"
strTemp = "1100" Case "D"
strTemp = "1101" Case "E"
strTemp = "1110" Case "F"
strTemp = "1111" Case Else
1 Такого произойти не может, не так ли? strTemp = ""
End Select
strOut = strOut & strTemp
Next i dhHexToBinary = strOut End Function
Пользовательские математические и числовые функции
В этом разделе рассмотрено несколько полезных пользовательских функций, предназначенных для выполнения основных математических и статистических расчетов. Вы также найдете функцию, преобразующую числа в текст. Приведенные функции сэкономят ваше время при выполнении соответствующих расчетов, однако используемые в них методы программирования довольно просты, поэтому примеры сопровождаются лишь краткими комментариями.
Математические функции
Вы уже знакомы с некоторыми математическими функциями. Это встроенные функции VBA и их комбинации. Ниже описано еще несколько функций, которыми можно воспользоваться при необходимости.
Нахождение наибольшего
общего делителя двух целых чисел
Наибольшим общим делителем (НОД) двух целых чисел называется наибольшее число, на которое оба заданных числа делятся без остатка. Функция в примере 3.13 принимает два аргумента и вычисляет их НОД.
Function dhGreatestCommonFactor(ByVai IngX As Long, ByVai IngY As Long) As Long
' Определяет наибольшее число, на которое ' делится без остатка как IngX, так и IngY.
Dim IngTemp As Long
IngX = Abs(lngX)
IngY = Abs(lngY)
IngTemp = IngX Mod IngY
Do While IngTemp > 0
IngX = IngY
IngY = IngTemp
IngTemp = IngX Mod IngY
Loop
dhGreatestCommonFactor = IngY End Function
Нахождение наименьшего общего кратного двух целых чисел
Похожим числовым соотношением между двумя числами является также наименьшее общее кратное (НОК). НОК двух чисел — это наименьшее число, делителями которого являются оба заданных числа. В примере 3.14 приведена функция, определяющая такое значение.
Пример 3.14. Определение наименьшего общего кратного двух чисел
Function dhLowestCommonMultiple(ByVai intX As Integer, _ ByVai intY As Integer) As Long
Определяет наименьшее число, делителем
которого является как intX, так и intY.
intX = Abs(intX)
intY = Abs(intY)
dhLowestCommonMultiple = intY * (intX \ _ dhGreatestCommonFactor(intX, intY)) End Function
Геометрические расчеты
Используя VBA, можно решить множество задач, связанных с геометрией (например, вычислить площадь поверхности шара). Если вы прилежно изучали геометрию в школе, для вас не составит труда написать соответствующую программу на VBA. Однако если для этого вам потребуется напрягать свою память, мы предлагаем ряд процедур, которые избавят вас от такого неприятного труда (пример 3.15).
Пример 3.15. Разнообразные геометрические функции
Const Р1=3.14159265358979
Function dhAreaofCircle(ByVai dblRadius As Double) As Double ' Вычисляет площадь круга.
dhAreaofCircle = PI * dblRadius A 2
End Function
Function dhAreaOfSphere(ByVai dblRadius As Double) As Double ' Вычисляет площадь поверхности шара.
dhAreaOfSphere = 4 * PI * dblRadius A 2
End Function
Function dhAreaOfRectangle(ByVai dblLength As Double, _ ByVai dblWidth As Double) As Double ' Вычисляет площадь прямоугольника.
dhAreaOfRectangle = dblLength * dblWidth
End Function
Function dhAreaOfTrapezoid(ByVai dblHeight As Double, _
ByVai dblSidel As Double, _
ByVai dblSide2 As Double) As Double
Вычисляет площадь трапеции. dhAreaOfTrapezoid = dblHeight * (dblSidel + dblSide2) / 2 End Function
Function dhVolOfPyramid(ByVai dblHeight As Double, _ ByVai dblBaseArea As Double) As Double
Вычисляет объем пирамиды.
dhVolOfPyramid = dblHeight * dblBaseArea / 3
End Function
Function dhVolOfSphere(ByVai dblRadius As Double) As Double
Вычисляет объем шара.
dhVolOfSphere s PI * (dblRadius A 3) *4/3
End Function
Преобразование денежных значений в текст
-> !-> !При программировании приложений, связанных с выписыванием чеков, вам может понадобиться перевести числа в текстовое представление. Например, значение $149.56 должно быть расшифровано как "сто сорок девять и пятьдесят шесть сотых". В примере 3.16 показано, как решить эту задачу посредством функции dhNumToStr, встроенных числовых функций, а также некоторых строковых функций, описанных в главе 1. В этом примере приведена также функция dhHandleGroup, вызываемая функцией dhNumToStr.
-> !
-> !
Примечание: Функция dhNumToStr использует массивы с нулевым начальным индексом. Для ее правильной работы убедитесь в том, что вы не включили в модуль, в котором она описана, оператор Option Base 1.
Пример 3.16. Преобразование числа в текстовое представление
Function dhNumToStr(ByVai varValue As Variant) As String
On Error GoTo HandleErrors
Dim intTemp As Integer Dim varNames As Variant Dim IngDollars As Long Dim intCents As Integer Dim strOut As String Dim strTemp As String Dim inti As Integer
If Not IsNumeric(varValue) Then Exit Function
’ Максимально возможным значением является 999999999,99. If varValue > 999999999.99 Then Exit Function varNames = Array("", "Thousand", "Million") varValue = Abs(varValue) IngDollars = Int(varValue) intCents = (varValue - IngDollars) * 100
If IngDollars > 0 Then
Сначала цикл по сотням, 1 потом по тысячам, а затем по миллионам. Do intTemp = IngDollars Mod 1000 IngDollars = Int(IngDollars / 1000) 1 Присоединение текстового представления 1 следующей тройки цифр к имеющемуся результату. If intTemp о 0 Then
strOut = dhHandleGroup(intTemp) & " " & _ varNames(inti) & " " & strOut End If inti = inti + 1
Loop While IngDollars > 0
Обработка значения центов.
strOut = RTrim(strOut) & " and " & _ Format$(intCents, "00") & "/100" End If
ExitHere: dhNumToStr - strOut Exit Function
HandleErrors:
* При возникновении любой ошибки возвращается пустая строка. strOut = ""
Resume ExitHere
End Function
Private Function dhHandleGroup(ByVai intValue As Integer) As String Static varOnes As Variant Static varTens As Variant Dim strOut As String Dim intDigit As Integer
If IsEmpty(varOnes) Then varOnes = Array("”, "One", "Two", "Three", "Four", "Five", _ "Six", "Seven", "Eight", "Nine", "Ten", _ "Eleven", "Twelve", "Thirteen", "Fourteen", "Fifteen", _ "Sixteen", "Seventeen", "Eighteen", "Nineteen", "Twenty")
End If
If IsEmpty(varTens) Then
* Элементы 0 и 1 этого массива не используются.
varTens = Array("", "", "Twenty", "Thirty", "Forty", _ "Fifty", "Sixty", "Seventy", "Eighty", "Ninety")
End If
' Получение значения сотен, десятков и единиц.
intDigit = intValue \ 100 intValue = intValue Mod 100
' Если сотни в числе присутствуют, добавляется их текстовое представление. If intDigit > 0 Then strOut = varOnes(intDigit) & " Hundred"
* Обработка значений десятков и единиц. Select Case intValue
Case 1 To 20
strOut = strOut & varOnes(intValue) Case 21 To 99
intDigit = intValue \ 10 intValue = intValue Mod 10 If intDigit > 0 Then
strOut = strOut & " " & varTens(intDigit) End If If intValue > 0 Then
strOut = strOut & "-" & varOnes(intValue) End If End Select
dhHandleGroup = strOut End Function
Статистические функции
В этом разделе представлен ряд полезных статистических функций, в том числе функции для вычисления факториалов, различных типов средних значений и стандартного отклонения, а также функции для определения минимального и максимального значений.
Факториалы
Во многих статистических функциях используются вычисления факториалов. Две функции, которые представлены в примере 3.17, вычисляют факториал рекурсивным и нерекурсивным методом.
Пример 3.17. Вычисление факториала рекурсивным и нерекурсивным способом
Function dhFactorialRecursive(intX As Integer) As Double If intX < 0 Or intX > 170 Then
dhFactorialRecursive = 0#
Elself intX = 0 Then
dhFactorialRecursive = 1# Else
dhFactorialRecursive = intX * _ dhFactorialRecursive(intX - 1) End If
End Function
Function dhFactorial(intX As Integer) As Double
Dim i As Integer Dim dblX As Double If intX < 0 Or intX > 170 Then
dhFactorial = 0#
Exit Function
End If
dblX = 1#
For i = 2 To intX
dblX = dblX * i
Next i
dhFactorial = dblX
End Function
Определение среднего значения, медианы, моды и стандартного отклонения массива
Наиболее часто в статистических расчетах используются функции, определяющие среднее значение, медиану, моду и стандартное отклонение последовательности чисел. Среднее — это просто арифметическое среднее последовательности числовых значений, медиана — это число, которое находится "посредине" последовательности, мода — число, встречающееся в данной последовательности чаще всего. Обычно оно близко к среднему значению, однако, являясь одним из чисел последовательности, оно может несколько отличаться от среднего. Наконец, стандартное отклонение является мерой близости последовательности чисел к среднему значению. В примере 3.18 приведены четыре функции, которые вычисляют эти величины для заданного массива чисел.
Примечание: Функции dhMedianOfArray и dhModeOfArray используют функцию dhQuickSort, рассмотренную в главе 4, для сортировки массива перед определением соответственно медианы и моды. Более подробно вопросы сортировки рассматриваются в главе 4.
Пример 3.18. Функции для вычисления среднего, медианы, моды и стандартного отклонения
Function dhAverageArray(varArray As Variant) As Variant
Dim varltem As Variant
Dim varSum As Variant
Dim IngCount As Long
If IsArray(varArray) Then
For Each varltem In varArray varSum = varltem + varSum IngCount = IngCount + 1 Next dhAverageArray = varSum / IngCount Else
dhAverageArray = Null End If
End Function
Function dhMedianOfArray(varArray As Variant) As Variant
Dim varltem As Variant
Dim varTemp As Variant
Dim varMedian As Variant
Dim inti As Integer Dim IngTemp As Long Dim IngLBound As Long Dim IngElements As Long If IsArray(varArray) Then
Сортировка массива.
Call dhQuickSort(varArray)
Подсчет количества элементов массива. IngLBound = LBound(varArray) IngElements = (UBound(varArray) - IngLBound + 1) ' Определение средней точки массива. Для нечетного 1 числа элементов, это легко (это просто средний элемент)... If (IngElements Mod 2) = 1 Then
dhMedianOfArray = varArray(IngLBound + _ (IngElements \ 2)) Else
* Для четного числа элементов — это средняя точка * между двумя средними элементами...
IngTemp = ((IngElements - 1) \ 2) + IngLBound dhMedianOfArray = ((varArray(IngTemp + 1) - _ varArray(IngTemp)) / 2) + varArray(IngTemp) End If
Else
dhMedianOfArray = Null End If
End Function
Function dhStandardDeviation (varArray) As Double
Вычисляет стандартное отклонение элементов массива. Dim IngN As Long Dim dblSumX As Double Dim dblSumX2 As Double
Dim dblVar As Double Dim intCounter As Integer IngN = 0 dblSumX = 0 dblSumX2 = 0 For intCounter = LBound(varArray) To UBound(varArray) If Not IsNull(varArray(intCounter)) Then IngN = IngN + 1 dblSumX = dblSumX + varArray(intCounter) dblSumX2 = dblSumX2 + varArray(intCounter) л 2 End If Next intCounter dblVar = 0 If IngN > 0 Then dblVar = (IngN * dblSumX2 - dblSumX л 2) / _ (IngN * (IngN - 1)) If dblVar > 0 Then dhStandardDeviation = Sqr(dblVar) End If
End If End Function
Function dhModeOfArray(varArray As Variant) As Variant
Dim varltem As Variant Dim varLast As Variant Dim IngCount As Long Dim IngOccur As Long Dim IngLastOccur As Long Dim IngTotalOccur As Long If IsArray(varArray) Then * Сортировка элементов массива по порядку. Call dhQuickSort(varArray) * Сохранение первого элемента, varltem = varArray(LBound(varArray)) * Цикл по всем элементам. For IngCount = LBound(varArray) To UBound(varArray) * Приращение счетчика появлений. IngOccur = IngOccur + 1 * Если значение не совпадает с предыдущим, 1 нужно проверить, превышает ли частота появлений ' предыдущего значения текущую максимальную частоту. If varArray (IngCount) О varLast Then If IngLastOccur > IngTotalOccur Then
1 Если это так, данное значение становится новым * максимумом, и сохраняется предыдущий 1 элемент массива.
IngTotalOccur = IngLastOccur varltem = varArray(IngCount - 1) End If 1 Этот элемент сохраняется в качестве 1 последнего просмотренного. varLast = varArray(IngCount)
* Сброс счетчика.
IngOccur = О End If
IngLastOccur = IngOccur
Next
Возврат значения, которое встречается чаще всего ’ (не забудьте проверить последнее значение).
If IngOccur > IngTotalOccur Then dhModeOfArray = varArray(IngCount - 1) Else
dhModeOfArray = varltem End If
Else
dhModeOfArray = Null
End If
End Function
Нахождение минимального и максимального значений
Как ни странно, VBA не имеет встроенных функций для определения минимального или максимального значения в последовательности чисел. Однако написать соответствующую функцию, использующую массив значений, довольно легко. В примере 3.19 показаны две функции, предназначенные для нахождения минимального и максимального элемента заданного массива.
Пример 3.19. Функции для определения максимума и минимума
Function dhMaxInArray(varArray As Variant) As Variant
Определяет максимальное значение среди элементов массива.
Dim varltem As Variant
Dim varMax As Variant
Dim inti As Integer
If IsArray(varArray) Then
If UBound(varArray) = -1 Then dhMaxInArray = Null Else
varMax = varArray(UBound(varArray))
For inti = LBound(varArray) To UBound(varArray) varltem = varArray(inti) If varltem > varMax Then
varMax = varltem
End If
Next inti
dhMaxInArray = varMax
End If
Else
dhMaxInArray = Null
End If
End Function
Function dhMinlnArray(varArray As Variant) As Variant * Определяет минимальное значение среди элементов массива.
Dim varltem As Variant Dim varMin As Variant Dim inti As Integer
If IsArray(varArray) Then
If UBound(varArray) = -1 Then dhMinlnArray = Null
Else varMin = varArray(LBound(varArray)) For inti = LBound(varArray) To UBound(varArray) varltem = varArray(inti) If varltem < varMin Then varMin = varltem
End If Next inti dhMinlnArray = varMin End If Else dhMinlnArray = Null End If End Function
В функциях, обрабатывающих значения массивов, вы можете также использовать ключевое слово ParamArray. Переменные, описанные как ParamArray, созданы для того, чтобы вы могли задавать для функции произвольное количество аргументов. К счастью, описанная как ParamArray переменная имеет тип Variant, поэтому очень просто создать ParamArray-версии функций, показанных в примере 3.19. Пример 3.20 представляет эти функции.
Пример 3.20. ParamArray-версии функций для определения максимума и минимума
Function dhMaxInParamArray(ParamArray varValues() As Variant) _ As Variant
Dim varTemp As Variant
varTemp = varValues dhMaxInParamArray = dhMaxInArray(varTemp) End Function
Function dhMinlnParamArray(ParamArray varValues() As Variant) _ As Variant
Dim varTemp As Variant
varTemp = varValues dhMinlnParamArray = dhMinlnArray(varTemp) End Function
Резюме
VBA имеет много функций для работы с числами, однако при их использовании приходится сталкиваться с проблемами, которые не всегда лежат на поверхности. Ниже приведен обзор встроенных функций, а также рассмотрены некоторые проблемы, связанные с данными с плавающей точкой и их округлением. Представлен также ряд полезных пользовательских функций для выполнения числовых расчетов. В частности, в этой главе рассмотрены такие вопросы:
Как VBA хранит и обрабатывает числа:
различные типы данных в VBA;
проблемы при использовании значений с плавающей точкой и способы их решения;
встроенные числовые функции:
— математические;
преобразования типов и округления;
генерации случайных чисел;
финансовые;
преобразования основания системы счисления;
пользовательские математические и числовые функции:
— математические;
геометрические;
преобразования чисел в текст;
— статистические.
Аналогичные аспекты работы со строковыми выражениями и датами рассматриваются в главах 1 и 2.
ГЛАВА 4
Поиск и сортировка в VBA
Время — деньги
Использование массивов
Сортировка массивов
Поиск
Если в своем приложении вы работаете с данными, то рано или поздно вам придется сортировать их или производить поиск определенного элемента в группе. В этой главе представлено несколько полезных методов, которыми вы можете воспользоваться по своему усмотрению.
Конечно же, тема поиска и сортировки данных более полно освещена в других книгах. Мы не ставили перед собой задачи подробно анализировать различные алгоритмы сортировки. Фактически рассмотрен только один — распространенный алгоритм быстрой сортировки Quicksort. Однако мы показали, как применять процедуру сортировки, и рассмотрели принципы ее работы. Из методов поиска мы представили алгоритм бинарного поиска Binary Search, а также продемонстрировали принципы его применения и работы. Поскольку многие читатели будут использовать VBA вместе с ядром баз данных Microsoft Jet или с некоторыми другими источниками данных SQL, мы также попутно даем советы по работе с ядром баз данных.
Время — деньги
Выбирая алгоритм сортировки данных, вы, скорее всего, учитываете его быстродействие. Для принятия решения о том, каким методом воспользоваться, потребуется некоторая помощь. Класс stopwatch поможет сделать оптимальный выбор, исходя из времени, требуемого для выполнения алгоритма. Мы постоянно пользуемся этим простым классом stopwatch в тех ситуациях, когда нужно сравнить быстродействие двух или более процессов.
Знакомство с классом StopWatch
Для измерения промежутков времени не требуется много усилий. VBA имеет встроенную функцию Timer, которая возвращает значение типа Single, определяющее количество секунд, прошедших с полуночи. При попытке использовать эту функцию для измерения промежутков времени вы столкнетесь с тремя трудностями: ■ в полночь функция Timer "сбрасывает” время, поэтому, если выполнение теста совпадет с этим таинственным моментом, результаты тестирования окажутся бессмысленными;
поскольку сброс значений происходит каждые 24 часа, провести более длительные испытания вам не удастся;
эта функция не отличается особой точностью (она фиксирует время с точностью до 1/18 секунды, поскольку использует отдельный системный таймер).
По этим причинам (далее будет приведена еще одна, более значительная) функцией Timer вообще лучше не пользоваться, особенно когда речь идет об измерении коротких промежутков времени. Взамен Windows API предоставляет несколько возможностей измерения промежутков времени, самой простой из которых является функция timeGetTime. Эта функция определяет количество миллисекунд (значение типа Long), прошедших с момента запуска Windows. По сравнению с функцией Timer функция timeGetTime имеет следующие преимущества:
она сбрасывает значения времени приблизительно каждые 48 дней (вряд ли у вас будет задача, которая выполняется столько времени, и вам будет нужно значение с точностью до миллисекунд, однако всегда приятно сознавать, что часы идут все время!);
функция не оперирует сутками, поэтому не возникает проблем при выполнении задач, которые длятся несколько дней;
значения, возвращаемые этой функцией, значительно точнее, чем значения, возвращаемые функцией Timer (время измеряется с точностью не до 1/18 секунды, а до 1/1000 секунды; кроме того, точности измерений способствует и то, что для представления результата не используется арифметика с плавающей точкой (в отличие от функции Timer)).
Очевидно, следует воспользоваться преимуществами этой API-функции для класса stopwatch. Программа для данного класса, представленная в примере 4.1, поразительно проста. Она использует два общих метода: StartTimer и EndTimer. Первый присваивает внутренней переменной plngStart значение времени при запуске секундомера, а второй возвращает разность между текущим значением времени и временем запуска секундомера, т.е. промежуток времени в миллисекундах.
Пример 4.1. Класс StopWatch
Private plngStart As Long
Private Declare Function timeGetTime Lib ’’winmm.dll’’ () As Long
Public Sub StartTimer() plngStart = timeGetTime End Sub
Public Function EndTimer() As Long
EndTimer = (timeGetTime - plngStart) End Function
Применение класса StopWatch
Для использования класса stopwatch обычно пишут такую программу:
Dim sw As New StopWatch sw.StartTimer
* Фрагмент программы, время
* выполнения которого необходимо определить.
Debug.Print "Время выполнения: " sw.EndTimer & "мс."
Последней (и наиболее убедительной) причиной использования функции timeGetTime, а не встроенной функции Timer, является тот факт, что обработка вызова функции Timer требует примерно в пять раз больше времени, чем обработка вызова функции timeGetTime. Чтобы убедиться, что вызов функции Timer требует значительно больше времени, чем вызов функции timeGetTime, запустите процедуру CompareTimers в окне отладки.
Пример 4.2. Сравнение функций Timer и timeGetTime
Private Declare Function timeGetTime Lib "winmm.dll" () As Long
Sub CompareTimers()
Dim intMax As Integer
Dim sw As New StopWatch
Dim inti As Integer
Dim IngResult As Long
intMax = 10000
sw.StartTimer
For inti = 1 To intMax
IngResult = Timer
Next inti
Debug.Print "Timer: " & sw.EndTimer
sw.StartTimer
For inti = 1 To intMax
IngResult = timeGetTime
Next inti
Debug.Print "timeGetTime: " & sw.EndTimer End Sub
Чтобы использовать класс stopwatch в любом приложении, просто импортируйте файл STOPWATCH.CLS в свой проект и вызывайте его так, как показано в примерах. Если где-то в книге говорится о том, что один метод работает быстрее другого, это означает, что оба метода сравнивались при помощи stopwatch.
Использование массивов
В VBA существует множество способов хранения данных, однако при сортировке данные лучше представлять в виде массива. Обработка массивов в VBA имеет множество особенностей, поэтому в следующих разделах рассмотрены понятия, необходимые для того, чтобы использовать методы, описанные в данной главе. Болес подробная информация о создании и применении массивов приведена в справке VBA. Это очень обширная тема, и у нас нет возможности рассмотреть все ее аспекты. Здесь мы попытались объяснить лишь то, что требуется для понимания приведенных примеров.
Все-таки, что такое массив?
Массивом называется индексированная группа данных, которая обрабатывается как единая переменная. Массив можно использовать при работе с группой элементов, для обозначения которых применяются целые числа.
Например, представьте себе, что нужно обработать данные о вашей зарплате за шестилетний период — с 1994 по 1999 год. Поскольку значение года является целым числом, его можно использовать в качестве индекса данных. Вы можете создать массив с именем Salaryinfo (рис. 4.1), описав его следующим образом:
Dim Salaryinfo (1994 То 1999) As Currency
Конечно, вы не ограничены только массивами, содержащими информацию о зарплате, или даже массивами данных определенного типа. При описании массива можно указать любой диапазон значений (для большинства массивов отсчет начинается с нуля или с единицы, однако это не обязательно), причем элементы массива могут относиться практически к любому типу данных. Кроме того, массивы бывают и многомерными. Для доступа к определенному элементу массива воспользуйтесь такими операторами:
Salaryinfo(1999) = 26500
* или
If Salaryinfo(1999) > Salaryinfo(1998) * 1.10 Then
MsgBox("Прирост зарплаты с 1998 по 1999 год был слишком велик. " & "Уменьшите рост зарплаты в этом году!") End If
Salaryinfo Индекс
$20,00Dy*$20,00Dy*1994:1994:$22.500$22.500'995'995
$20,00Dy*
$20,00Dy*
1994:
1994:
$22.500
$22.500
'995
'995
$26,000; Д
$30,000^$30,000^= 1997^= 1997^
$30,000^
$30,000^
= 1997^
= 1997^
B5”j; issa”
Рис. 4.1. В качестве индексов массива можно использовать любой диапазон целых чисел
Представьте себе, что вам нужны поквартальные сведения о зарплате. Таким образом, нужно записать информацию для каждого из четырех кварталов по каждому из шести лет. Для этого можно создать массив, подобный представленному на рис. 4.2, и описать его следующим образом:
Dim Salarylnfо(1994 То 1999, 1 То 4) As Currency
причем года задаются в диапазоне от 1994 по 1999, а кварталы нумеруются с 1 по 4.
маи^
■Я
5625 . 5700. 5800 j 6000.-d 1 site
65006500 j 6500 .’ 7300 j , „п '
7500.$ 7800 .; 78007900 J ’
liiiiiiiiiiiiiii^^
8000 J 8000 7.; 8000. j 8400 :
8650 8800 v 9000 J 9000 d LZd
Puc. 4.2. Многомерный массив
Для чтения или записи элемента массива можно воспользоваться таким выражением:
* Задаем 15% рост зарплаты в четвертом квартале по сравнению с третьим. Salaryinfo(1999, 4) = Salaryinfo(1999, 3) * 1.15
Пойдем дальше: как быть, если нужна информация о поквартальной зарплате нескольких служащих? В этом случае можно сформировать трехмерный массив, представленный на рис. 4.3, и воспользоваться для его описания таким оператором:
Dim Salaryinfo(1994 То 1999, 1 То 4, 1 То 3) As Currency
(т.е. годы с 1994 по 1999, кварталы с 1 по 4 и служащие с 1 по 3). С помощью следующего фрагмента программы вычисляется, на сколько снизилась зарплата третьего служащего в денежном выражении за последний квартал 1999 года, если в процентном отношении уменьшение составило 10%:
Salaryinfo(1999, 4, 3) = Salaryinfo(1999, 4, 3) * 0.90
Далее обсуждаются некоторые подробности применения массивов. Во многих примерах главы массивы используются для решения конкретных задач.
4504505625625005006500 26500 2450'450'650650562562750750Ми8000 $Ми8000 $6506505000 У 5000У i5000 У 5000У i6500 $ 6500 $6500 $ 6500 $780078001|Я1|Я80009000800090006”j|l99Sj6”j|l99Sj730021730021«И«ИДМДМеден;еден;1997 ;1997 ;1998 Л1998 Л750 -|/-|if|^^750 -|/-|if|^^воск ' ;Д ■-л?-^воск ' ;Д ■-л?-^ill,;”^wlhr'^ J Cf,y*a^ Nill,;”^wlhr'^ J Cf,y*a^ N8650 . ...Ji8650 . ...Ji||у.;_„^||у.;_„^
450
450
562
562
500
500
6500 2
6500 2
450'
450'
650
650
562
562
750
750
Ми
8000 $
Ми
8000 $
650
650
5000 У 5000У i
5000 У 5000У i
6500 $ 6500 $
6500 $ 6500 $
7800
7800
1|Я
1|Я
8000
9000
8000
9000
6”j|l99Sj
6”j|l99Sj
730021
730021
«И
«И
ДМ
ДМ
еден;
еден;
1997 ;
1997 ;
1998 Л
1998 Л
750 -|/-|
if|^^
750 -|/-|
if|^^
воск ' ;Д ■-л?-^
воск ' ;Д ■-л?-^
ill,;
”^wlhr'^ J Cf,y*a^ N
ill,;
”^wlhr'^ J Cf,y*a^ N
8650 . ...Ji
8650 . ...Ji
||у.;_„^
||у.;_„^
■ииам
Рис. 4.3. Трехмерные массивы
<х Ни Совет: Хотя применение многомерных массивов является полезным прие- I ill мом для ^°ДелиРования ситуаций реальной жизни, будьте осторожны, так L I VJ как вы усложняете свою программу, используя эти, иногда трудные для понимания, структуры данных. Кроме того, в VBA массивы не являются разреженными — это означает, что пустые элементы занимают столько же памяти, сколько и элементы с данными. Большие массивы могут быть настоящими "пожирателями" памяти, поэтому следует определить, насколько заполнен ваш массив и нельзя ли использовать в данном случае более экономичные структуры данных.
Создание массива
VBA обрабатывает массив точно так же, как и другие переменные. При этом применимы тс же правила наименования, область действия и время жизни. Единственным отличием при работе с массивами является то, что необходимо либо задать нижнюю и верхнюю границы массива, либо указать, что эти значения будут заданы поз.же.
Создать массив, указав его границы, можно следующим образом:
100 целых чисел.
Dim aintitems(1 То 100) As Integer
10 строк данных по 2 строковых значения в каждой.
Dim astrNamesd То 10, 1 То 2) As String
Сообщите VBA о том, что размер будет задан позже.
Dim astrNamesO As String
Более подробная информация об изменении размеров массива после его создания приведена в разделе "Определение размеров массива".
Использование данных в массиве
Создав массив, можно обрабатывать любой его элемент, обращаясь к нему по его индексу. Например, если массив astritems содержит 100 элементов, для просмотра каждого элемента можно воспользоваться следующей программой:
Dim inti As Integer For inti = 1 To 100
Debug.Print astritems(inti) Next inti
Если вы хотите присвоить текущее значение индекса каждому элементу массива aintitems (массив из 50 целых чисел), можно написать такую программу:
Dim aintitems(1 То 50) As Integer
Dim inti As Integer For inti = 1 To 50 aintitems(inti) = inti Next inti
Для работы с элементами массива не всегда требуется использовать циклы. Например, следующая процедура, которая применена в программе сортировки из раздела "Сортировка массивов", выполняет перестановку двух элементов массива. В этом случае массив называется varitems, а переменные intiteml и intltem2 содержат индексы элементов массива, которые нужно переставить:
Dim varTemp As Variant
varTemp = varitems(intltem2)
varitems(intltem2) = varitems(intiteml)
varitems(intiteml) = varTemp
Примечание: Для просмотра элементов массива можно также воспользоваться конструкцией For Each. . .Next, однако делать это не рекомендуется. В данном случае процесс будет протекать медленнее, чем при использовании конструкции For. . .Next, и, кроме того, конструкцию For Each. .. Next нельзя применять для записи значений в массив — она может использоваться только для выборки данных.
Определение размеров массива
Обычно массивы являются статическими структурами данных, т.е. указав VBA его размер, вы впоследствии не сможете автоматически изменить его без явного указания сделать это.
VBA предоставляет несколько способов задать размеры массива:
при описании массива (такие массивы называют массивами фиксированного размера)'.
Dim astrltemsd То 100) As String
перед использованием массива посредством ключевого слова (такие массивы называют динамическими)'.
Dim astrltemsO As String
Размер массива задается следующим оператором * (допустим, переменнаяintiterns имеет значение 100).
ReDim astrltemsd То intltems)
при помощи ключевого слова ReDim:
ReDim astrltemsd То 100) As String
при помощи ключевого слова ReDim можно в любой момент изменить размер динамического массива (если ключевое слово Preserve не указать, VBA удалит все данные, которые содержатся в массиве, если же ключевое слово Preserve указано, VBA сохранит все имеющиеся элементы массива):
Dim astrltemsO As String * Ниже в программе: ReDim astrltemsd То 100) * Заполнение элементов массива данными... * А теперь вы обнаружили, что требуется дополнительно 100 элементов: ReDim Preserve astrltemsd То 200)
f\lh Совет: Хотя задавать нижнюю границу массива не обязательно, настоя- I [О тельно рекомендуем всегда делать это. Если не указать нижнюю границу, отсчет начнется со значения, заданного (или подразумеваемого) оператором Option Base для данного модуля. По умолчанию данное значение равно 0, однако его можно изменить, добавив к модулю оператор Option Base 1. В любом случае, если нижняя граница массива не указана при задании его размеров, VBA использует текущую установку оператора Option Base. При явном задании нижней границы программа становится более наглядной, и вы будете в меньшей степени подвержены ошибкам типа "смещение на единицу".
Альтернативы оператору ReDim Preserve
Оператор ReDim Preserve позволяет сохранить содержимое массива при изменении его размера, однако эта операция довольно Медленная. При изменении размера массива VBA должен выделить область памяти для нового массива, а затем, если указано ключевое слово Preserve, скопировать туда все элементы исходного массива. После этого он освобождает память, занятую исходным массивом. По возможности применения конструкции ReDim Preserve лучше избегать. Каковы же альтернативные варианты? Одним из них является использование семейства для хранения ваших данных. Другим — применение динамической структуры данных типа связного списка. Наконец, следует рассмотреть возможность изменить размер массива путем добавления большого количества элементов за один раз. Другими словами, вместо того чтобы изменять размер массива каждый раз по мере необходимости, добавьте сразу большее количество элементов и изменяйте размер массива, только когда и этих элементов уже будет недостаточно. После добавления элементов можете вызвать оператор ReDim Preserve, чтобы правильно изменить размер массива.
Переменная типа Variant при работе с массивами
Простейшим способом работы с массивами является "присвоение" массива переменной типа Variant и использование этой переменной для обращения к массиву. Таким образом, если astrltems — это массив, состоящий из 100 строк, можно воспользоваться следующей программой для того, чтобы обращаться к astrltems через varlterns:
Dim varltems As Variant
Dim astrltems (1 To 100) As String * Заполнение строк, varltems = astrltems
Предприняв такой шаг, можно использовать varltems как простую переменную, не беспокоясь о замыкающих скобках, но можете также обращаться и к элементам массива, например, следующим образом:
varltems(1) - "Новая строка"
Подобное присвоение происходит автоматически при задании массива в качестве аргумента функции, которая ожидает параметр типа Variant. Например, процедура dhQuickSort (пример 4.3) описана следующим образом:
Sub dhQuickSort(varArray As Variant, _
Optional intLeft As Integer = dhcMissing, _
Optional intRight As Integer = dhcMissing)
При вызове процедуры dhQuickSort можно задавать в качестве аргумента либо массив, либо переменную типа Variant, которая "содержит" массив. Допустим любой из следующих методов: Dim varltems As Variant varltems = ArrayU, 2, 3, 4, 5) Call dhQuickSort(varltems) * или: ========== Dim aintltems(1 To 5) As Integer aintltems(1) = 1 aintltems(2) = 2 aintltems(3) = 3 aintltems(4) =4 aintltems (5) =5 Call dhQuickSort(aintltems) * или: ========== Dim aintltems(1 To 5) As Integer Dim varltems As Variant aintltems(1) = 1 aintltems(2) = 2 aintltems(3) = 3 aintltems(4) =4 aintltems(5) = 5 varltems = aintltems Call dhQuickSort(varltems)
Если вы вызываете процедуру dhQuickSort, задав ей в качестве аргумента массив строк (или любые другие данные определенного типа), VBA присваивает этот массив переменной типа Variant при вызове процедуры. Затем внутри вызванной процедуры можно обращаться к переменной varltems, будто она сама является массивом.
Имеется два способа, с помощью которых можно убедиться в том, что переменная типа Variant действительно содержит массив:
Вызовите функцию IsArray, передав ей переменную типа Variant. Функция возвратит логическое значение True, если переменная указывает на массив, и значение False, когда это не так. В начале процедуры, которая ожидает задания массива, можно вставить такой фрагмент:
If Not IsArray(varltems) Then Exit Sub
Вызовите функцию VarType, передав ей переменную типа Variant. Функция VarType возвратит константу vbArray (8192) плюс значение, соответствующее типу данных в массиве (от vblnteger (2) до vbByte (17)). Например, если в качестве аргумента некоторой функции может выступать только массив строк, напишите такой фрагмент программы:
If VarType(varltems) <> vbArray + vbString Then Exit Sub
Использование функции Arrayi
В некоторых примерах данной главы для записи данных в массив используется ; функция Array. Эта полезная функция позволяет задать список определенных j значений, которые должны быть занесены в массив, и возвращает массив, со- • держащий переданные вами значения. Например, следующий оператор зап и- сывает массив с тремя целыми числами в переменную типа Variant:
IDim varltems As Variant|
•varltems = ArraydOO, 202,315){
।* Почти эквивалентно:|
jDim varltems(0 To 2) AsIntegerj
।varltems(0) = 100'
varltems(1) = 202
varltems(2) = 315;
Обратите внимание на слово "почти" в комментарии. В первом примере ин- i дексы элементов массива varltems будут формироваться с использованием | параметра оператора Option Base, указывающего нижнюю границу индексов , массивов для данного модуля. Если оператор Option Base в этом модуле от- | сутствует или значение его параметра равно 0, элементы будут пронумерованы j 0, 1 и 2. Если же параметр оператора Option Base для модуля равен 1, \ элементы будут пронумерованы 1, 2 и 3. При втором объявлении массива j нумерация его элементов всегда начинается с нуля, независимо от значения | параметра оператора Option Base. При использовании функции Array еле- | | дует не забывать о параметре оператора Option Base. (Это противоречит | | тексту справки VBA, где ошибочно утверждается, что функция Array всегда : | создает массивы с нулевым начальным индексом.)j
Сортировка массивов
После краткого знакомства с массивами можно приступать к рассмотрению процедуры сортировки данных массива. В этом разделе описан распространенный метод сортировки — алгоритм быстрой сортировки Quicksort, который реализует более совершенный способ сортировки. Он несколько сложен, однако работает прекрасно. После написания программы к ней возвращаются обычно только для того, чтобы внести определенные изменения.
Примечание: Почему описан именно этот алгоритм сортировки? Необходимо выбирать такой метод сортировки, при котором количество операций сравнения и перестановки, выполняемых алгоритмом для решения поставленной задачи, сведено к минимуму. Существуют, конечно, и более простые алгоритмы сортировки (с этим утверждением вы не сможете не согласиться, проследив за работой Quicksort по шагам), однако, чем проще алгоритм, тем больше операций сравнения и перемещения данных (перестановки элементов) он выполняет в процессе сортировки массива.
Большее количество операций сравнения и перестановок приводит к увеличению времени выполнения, поэтому Quicksort является хорошим компромиссом: он сложный, но понятный, и гораздо эффективнее других более простых алгоритмов сортировки.
Внимание: Алгоритм Quicksort не является оптимальным при обработке отсортированных данных. Поэтому, если данные поступают в отсортированном виде, время выполнения будет практически таким же, как и для несортированных данных. Некоторые другие (возможно, более простые) алгоритмы "принимают во внимание" тот факт, что данные уже отсортированы, и заканчивают работу быстрее. Если вам приходится часто сталкиваться с сортировкой данных, которые могут быть уже отсортированы, попробуйте использовать другие методы сортировки. (Этот вопрос подробно обсуждается в разделе "Проблемы быстродействия".)
Работа алгоритма Quicksort
Алгоритм Quicksort считается одним из самых быстрых алгоритмов сортировки и использует способ сортировки "разделяй и властвуй". Quicksort делит массив на все меньшие и меньшие части, на две части каждый раз, так, чтобы значения в левой части были меньше, чем в правой. Работа заканчивается по завершении процесса дробления.
Quicksort прибегает к рекурсивному вызову (когда процедура вызывает саму себя, передавая новые параметры), и рекурсивное выполнение сортировки является довольно простым. Алгоритм можно разбить на следующие шаги.
Вызывается функция сортировки с указанием нижней и верхней границ сортируемого сегмента.
Если нижняя граница меньше верхней, то:
текущий массив разбивается на два меньших сегмента таким образом, чтобы все элементы левого сегмента были меньше или равны элементам правого (для этого осуществляется перестановка некоторых элементов из одного сегмента в другой);
та же процедура выполняется для меньшего (левого или правого) из двух сегментов (далее происходит дробление сегментов и их сортировка на более низких уровнях); по завершении сортировки те же действия выполняются с большим сегментом.
Процесс сортировки состоит из двух этапов: разбиение элементов на группы и последующий рекурсивный вызов процедуры сортировки. Здесь можно было бы высказать неудовлетворение по поводу рекурсивных процедур и того, как много памяти они занимают. Вообще говоря, это так, однако алгоритм сортировки данной версии "старается" рационально использовать память. На каждом уровне сначала выполняется сортировка меньшего из двух сегментов. Это обеспечивает минимально возможное число уровней рекурсии: дробление меньшего сегмента до единичного элемента закончится гораздо быстрее, чем дробление большего сегмента. Работая сначала с меньшими сегментами, данный метод предотвращает вызовы самого себя чаще, чем требуется. Процедура dhQuickSort, реализующая алгоритм быстрой сортировки Quicksort, представлена в примере 4.3.
Пример 4.3. Процедура dhQuickSort, реализующая алгоритм быстрой сортировки Quicksort
Const dhcMissing = -2
Sub dhQuickSort(varArray As Variant, _ Optional intLeft As Integer = dhcMissing, _ Optional intRight As Integer = dhcMissing) Dim i As Integer Dim j As Integer Dim varTestVal As Variant Dim intMid As Integer
If intLeft = dhcMissing Then intLeft = LBound(varArray) If intRight = dhcMissing Then intRight = UBound(varArray)
If intLeft < intRight Then intMid = (intLeft + intRight) \ 2 varTestVal = varArray(intMid) i = intLeft j = intRight Do
Do While varArray (i) < varTestVal i = i -I- 1
Loop
Do While varArray(j) > varTestVal j = j " 1
Loop
If i <= j Then SwapElements varArray, i, j i = i + 1 j = j - 1 End If
Loop Until i > j
С целью оптимизации процесса сортировки ’ первым всегда сортируется меньший сегмент. If j <= intMid Then
Call dhQuickSort(varArray, intLeft, j)
Call dhQuickSort(varArray, i, intRight) Else
Call dhQuickSort(varArray, i, intRight) Call dhQuickSort(varArray, intLeft, j) End If
End If End Sub
Private Sub SwapElements(varltems As Variant, _ intlteml As Integer, intltem2 As Integer)
Dim varTemp As Variant
varTemp = varltems(intltem2)
varltems(intltem2) = varltems(intlteml) varltems(intlteml) = varTemp
End Sub
Для процедуры, которая рекурсивно вызывает саму себя, необходимо предусмотреть возможность своевременного прерывания этого процесса. В данном случае программа повторяется, пока значение нижней границы массива меньше, чем значение верхней. По мере того, как процедура dhQuickSort вызывает саму себя, значение верхней границы становится все меньше и меньше, а нижней границы — все больше и больше. Как только эти значения пересекутся, сортировка завершается. Поэтому процедура dhQuickSort начинается с проверки выполнения условия:
If intLeft < intRight Then
* Выполнить сортировку данного сегмента (фрагмент программы опущен). End If
Если при очередном обращении к процедуре dhQuickSort значение верхней границы не превышает значения нижней, сортировка прекращается и процедура возвращается в точку вызова. Определив, что сортировка может выполняться, программа делает следующее.
В качестве величины для сравнения процедура сортировки использует значение из середины сортируемого фрагмента массива. Эта величина будет разделяющим элементом для двух фрагментов данных. Существуют несколько способов выбора разделяющего элемента. Данный вариант сортировки использует элемент, который физически находится посредине выбранного списка элементов:
intMid = (intLeft + intRight) \ 2
varTestVal = varArray(intMid)
Программа начинает поиск слева, двигаясь по массиву до тех пор, пока не найдет элемент, который не меньше разделяющего. Процесс поиска наверняка завершится при выборе разделяющего элемента, который, естественно, не может быть меньше самого себя:
Do While varArray(i) < varTestVal i = i + 1
Loop
Затем поиск продолжается в обратном направлении до тех пор, пока не будет найден элемент, который не больше разделяющего. Этот процесс также наверняка остановится на разделяющем элементе, который, естественно, не может быть больше самого себя:
Do While varArray(j) > varTestVal j = j - 1
Loop
Если положение элемента, найденного на втором шаге, меньше или равно положению элемента, обнаруженного на шаге третьем, процедура переставляет эти элементы, а затем увеличивает указатель для второго шага и уменьшает — для третьего:
If i <= j Then
SwapElements varArray, i, gentlemen
i = i + 1
j = j - 1
End If
Процедура сортировки повторяет шаги со второго по четвертый до тех пор, пока значение указателя для второго шага не превысит значение указателя для третьего (i > j). В этот момент все значения слева от разделяющего элемента будут меньшими или равными ему, а все значения справа — большими или равными ему.
Начиная с меньшей части массива, программа сортировки повторяет эти .шаги для всех подмножеств с обеих сторон от разделяющего элемента, пока проверка на первом шаге не покажет, что процедура завершена:
If j <= intMid Then
Call dhQuickSort(varArray, intLeft, gentlemen)
Call dhQuickSort(varArray, i, intRight) Else
Call dhQuickSort(varArray, i, intRight)
Call dhQuickSort(varArray, intLeft, gentlemen) End If
Чтобы лучше понять описанную методику, представьте себе, что нужно отсортировать массив из десяти целых чисел, позиции которых пронумерованы с 1 до 10 (рис. 4.4). Следующий список соответствует шагам процедуры, представленным на рис. 4.5-4.8. Для нескольких первых шагов приводятся соответствующие фрагменты программы. Так как эти фрагменты небольшие, но шагов довольно много, то в том случае, если соответствующий фрагмент программы уже приводился, он повторно не отображается.
12345678910
793024 j 48.j 26 J 34,3 05 - 482186
g|il||||^^
Рис. 4.4. Образец массива для сортировки
Примечание: Кроме того, будем наблюдать за уровнями рекурсии, т.е. по мере того, как из dhQuickSort вызывается dhQuickSort, необходимо следить за тем, сколько раз процедура вызвала саму себя. В данном примере первым уровнем считается ваш вызов процедуры dhQuickSort, а последующие представляют собой обращения dhQuickSort к самой себе с передачей значений границ массива для сортировки.
Примечание: Для наглядности мы используем символ jА для ссылки на j-й элемент массива. Если переменная j содержит значение 1, элемент, на который она указывает (в исходном массиве), равен 79, а в тексте будет приведено обозначение jA. (Если вы хотите как-нибудь назвать этот символ, назовите его "j с крышкой". Такое название придумал один из авторов, слушая много лет назад лекции по языку Паскаль.)
ШагШаг11ШагШаг22ШагШаг33ШагШаг44ШагШаг5512345678910123456789102121797986862121ввi»li»l2:2:05 ; 2405 ; 246;}6;}34 J 05^ 48^34 J 05^ 48^■■■■■■■■■■34.>1 05 s 4821 J 8634.>1 05 s 4821 J 86304879 1 86304879 1 86■■aif■■aif30 J 2430 J 2430 ч 2430 ч 24JJJJРис. 4.5. Шаги с первого по седьмойРис. 4.5. Шаги с первого по седьмой34 J 05,jj 48. у 79. М 8634 J 05,jj 48. у 79. М 8648 J48 J7979
Шаг
Шаг
1
1
Шаг
Шаг
2
2
Шаг
Шаг
3
3
Шаг
Шаг
4
4
Шаг
Шаг
5
5
12345678910
12345678910
21
21
79
79
86
86
21
21
в
в
i»l
i»l
2:
2:
05 ; 24
05 ; 24
6;}
6;}
34 J 05^ 48^
34 J 05^ 48^
■■■■■
■■■■■
34.>1 05 s 4821 J 86
34.>1 05 s 4821 J 86
304879 1 86
304879 1 86
■■aif
■■aif
30 J 24
30 J 24
30 ч 24
30 ч 24
JJ
JJ
Рис. 4.5. Шаги с первого по седьмой
Рис. 4.5. Шаги с первого по седьмой
34 J 05,jj 48. у 79. М 86
34 J 05,jj 48. у 79. М 86
48 J
48 J
79
79
Вычисляется средняя точка массива (5). Затем указатель i помещается на первый элемент массива, a j — на последний. Пока iA (элемент, на который указывает индекс i) меньше 26, i смещается вправо. (В данном случае он вообще никуда не смещается, поскольку значение 79 не меньше 26.) Пока jЛ больше 26, j смещается влево:
intMid = (intLeft + iritRight) \ 2 varTestVal = varArray(intMid) i = intLeft j = intRight Do
Do While varArray(i) < varTestVal i = i + 1
Loop
Do While varArray(j) > varTestVal j = j - 1
Loop
Так как 1 меньше или равно j, элементы, на которые указывают индексы i и j, меняются местами. Затем i смещается на одну позицию вправо, a j — на одну позицию влево:
If i <= j Then
SwapElements varArray, i, j
i = i 4- 1
j = j - 1
End If
Loop Until i > gentlemen
Поскольку i не превышает j, цикл возвращается в начало. Пока i А меньше 26, i смещается вправо. (Это не так, поэтому смещения не происходит.) Пока jА больше 26, j смещается влево.
Поскольку i меньше или равно j, элементы, на которые указывают индексы i и j, меняются местами. Затем i смещается на одну позицию вправо, a j — на одну позицию влево.
Поскольку индекс i не превышает j, цикл возвращается в начало. Пока i А меньше 26, 1 смещается вправо. Пока jА больше 26, j смещается влево.
Поскольку i меньше или равно j, элементы, на которые указывают индексы 1 и j, меняются местами. Затем 1 смещается на одну позицию вправо, a j — на одну позицию влево.
Поскольку теперь i больше j (i равно 5, a j равно 4), происходит выход из цикла Do. . .Loop. Сейчас j меньше intMid (средней позиции), поэтому выполняется рекурсивный вызов всей процедуры для обработки элементов с 1-го по j-й. (В этот момент вы переходите с первого уровня рекурсии на второй.) Отсортировав элементы в позициях с 1-й по 4-ю, процедура dhQuickSort вызывается снова, чтобы отсортировать элементы в позициях с 5-й по 10-ю (начиная с шага 13).
If j <= intMid Then
Call dhQuickSort(varArray, intLeft, gentlemen)
Call dhQuickSort(varArray, i, intRight)
Все начинается сначала: теперь номер крайнего левого элемента равен 1, а номер крайнего правого — 4, поэтому указатели i и j устанавливаются на эти позиции. Вычисляется средняя точка (2). Пока iA меньше 5, 1 смещается вправо. (В данном случае он вообще не смещается, поскольку значение 21 не меньше 5.) Пока jА больше 5, j смещается влево.
Так как 1 меньше или равно j, элементы, на которые указывают индексы i и j, меняются местами. Затем i смещается на одну позицию вправо, a j — на одну позицию влево.
Поскольку 1 больше j (i равно 2, a j равно 1), происходит выход из цикла Do. . . Loop. Теперь j меньше средней позиции, поэтому выполняется рекурсивный вызов всей процедуры для обработки элементов с 1-го по j -й. (В этот момент вы переходите со второго уровня рекурсии на третий.) Естественно, отсортировав элементы с 1-го по 1-й, процедура снова возвращается на второй уровень и сортирует элементы в позициях со 2-й по 4-ю (начиная с шага 11).
If j <= intMid Then
Call dhQuickSort(varArray, intLeft, j)
В этот момент функция dhQuickSort вызывается со значениями параметров 1 и 1 в качестве граничных точек. Сначала выполняется проверка, меньше intLeft, чем intRight или нет, и если это не так, происходит выход из процедуры. В данном случае мы возвращаемся на второй уровень.
На втором уровне снова вызывается процедура dhQuickSort с конечными точками i и intRight. (Вызвав процедуру, вы снова переходите со второго уровня рекурсии на третий.) Пока iA меньше значения среднего элемента (24), движение происходит вправо. Это помещает i прямо на 24. Пока jА больше 24, j смещается влево. В результате j также помещается на 24.
Поскольку i действительно меньше или равно j, элементы, на которые указывают индексы i и j, меняются местами (это выполняется мгновенно, так как индексы i и j указывают на одно и то же значение). Затем j смещается на одну позицию влево, a i - на одну позицию вправо. (Эта ситуация представлена шагом 12 на рис. 4.6.) Теперь i больше j, поэтому пора выходить из цикла. В данный момент j меньше intMid (j равно 2, a intMid равно 3), и программа вызывает процедуру dhQuickSort, передавая ей значения intLeft (2) и j (2). При этом выполняется переход на четвертый уровень рекурсии. Конечно, как только вы на него попадаете, dhQuickSort определяет, что значение intLeft не меньше значения intRight (при вызове с третьего уровня обе переменные получили значение 2), поэтому программа сразу же возвращается на третий уровень. На третьем уровне следующим шагом снова будет вызов процедуры dhQuickSort с параметрами i (4) и intRight (4). Как вы догадались, пребывание на четвертом уровне снова будет недолгим: поскольку процедура dhQuickSort получила одинаковые значения для параметров intLeft и intRight, что приводит к немедленному возврату на третий уровень. На этом вызов dhQuickSort на третьем уровне заканчивается, и программа возвращается на второй уровень. Но задача второго уровня уже выполнена — и левая, и правая половины первой части массива (элементы с 1-го по 4-й) отсортированы, поэтому пора возвращаться на первый уровень к правой части. Наконец-то мы пришли к ситуации, которая отражена на рис. 4.7 (шаг 13).
Вероятно, вы уже освоились с этой процедурой? Пока iA меньше значения среднего элемента (30), i смещается вправо. Однако он указывает на значение, большее 30 (значение 48 в позиции 5), и смещения не происходит. Пока jА больше значения среднего элемента (30), j смещается влево. Все элементы справа от позиции 7 больше 30, поэтому смещение заканчивается, когда j указывает на средний элемент.
Поскольку i меньше или равно j, два элемента меняются местами.
i смещается на одну позицию вправо, a j — на одну позицию влево. (Сейчас оба указателя находятся на позиции 6.)
Пока iA меньше значения среднего элемента (30), i смещается вправо. Конечно, он указывает на значение 34, поэтому смещения вообще не происходит. Пока jА больше 30, j смещается влево; при этом j смещается на позицию 5, где он указывает на значение 30. Так как i больше, чем j, пора выходить из цикла. В данный момент программа вызывает процедуру dhQuickSort с параметрами intLeft (5) и j (5) (переход с первого уровня на второй). Конечно, данный визит на второй уровень довольно краток, поскольку dhQuickSort сразу определяет, что новые значения параметров intLeft и intRight одинаковы.
Шаг 8Шаг 9Шаг 10Шаг 11Шаг 12Шаг 8Шаг 9Шаг 10Шаг 11Шаг 1255Рис. 4.6. Шаги с восьмого по двенадцатыйРис. 4.6. Шаги с восьмого по двенадцатый6789106789101Шаг 131Шаг 13Шаг 14Шаг 14Шаг 15Шаг 15Шаг 16Шаг 16
Шаг 8
Шаг 9
Шаг 10
Шаг 11
Шаг 12
Шаг 8
Шаг 9
Шаг 10
Шаг 11
Шаг 12
5
5
Рис. 4.6. Шаги с восьмого по двенадцатый
Рис. 4.6. Шаги с восьмого по двенадцатый
678910
678910
1
Шаг 13
1
Шаг 13
Шаг 14
Шаг 14
Шаг 15
Шаг 15
Шаг 16
Шаг 16
Рис. 4.7. Шаги с тринадцатого по шестнадцатый
Теперь вы снова на первом уровне, откуда опять вызываете процедуру dhQuick- Sort (и попадаете на второй уровень) с параметрами 1 (6) и intRight (10). В этот раз процесс на втором уровне продолжается с нужными значениями граничных точек. Пока 1А меньше значения среднего элемента (48), i смещается вправо. Пока jA больше значения среднего элемента (48), j смещается влево.
При значении i равном 7 и j равном 8, элементы, на которые они указывают, меняются местами.
j смещается еще на одну позицию влево, a i — еще на одну позицию вправо. Поскольку i больше j, происходит выход из цикла. Так как j (7) меньше номера средней позиции (8), программа вызывает процедуру dhQuickSort (переход на третий уровень рекурсии) с параметрами intLef t (6) и j (7) в качестве конечных точек.
Здесь уже становится тесно: осталось отсортировать всего два элемента, поэтому средняя позиция равна 6 (т.е. int ((64-7) \ 2)). Пока i А меньше 34, i смещается вправо (на самом деле он никуда не смещается). Пока jА больше 34, j смещается влево. В этот момент оба указателя попадают на позицию 6, которой соответствует значение 34. Поскольку i меньше или равно j, элементы, на которые указывают i и j, меняются местами. (Конечно, бесполезное упражнение, однако его необходимо выполнить!) Затем j смещается на одну позицию влево, а i — на одну позицию вправо.
Поскольку i теперь больше j (i равно 7, a j равно 5), происходит выход из цикла. Выполняется вызов процедуры dhQuickSort (переход на четвертый уровень) с параметрами intLeft (6) и j (5). Конечно, как только вы попадаете на уровень 4, происходит возврат назад, поскольку dhQuickSort сразу определяет, что значение intLeft больше значения intRight. На третьем уровне dhQuickSort вызывается снова, теперь с параметрами i (7) и intRight (7). Визит на четвертый уровень также будет очень коротким, поскольку dhQuickSort и на этот раз немедленно возвращается назад. Теперь работа на третьем уровне завершена, и вы возвращаетесь на второй уровень.
На втором уровне левый фрагмент уже отсортирован (шаги с 20 по 21), и можно вызывать процедуру dhQuickSort (переход на третий уровень) для правого фрагмента. При конечных точках 8 и 10 средним будет девятый элемент, значение которого равно 79. Пока iA меньше 79, i смещается вправо. Пока jA больше 79, j смещается влево. Эти циклы прерываются в уже знакомой нам ситуации: оба индекса i и j указывают на значение 79 в позиции 9. Программа меняет местами 79 с самим собой, а затем i смещается еще на одну позицию вправо, a j — еще на одну позицию влево.
Поскольку i больше j, пора выходить из цикла. Программа вызывает процедуру dhQuickSort (переход на четвертый уровень) с параметрами intLeft (8) и j (8). Конечно, сразу же происходит возврат на третий уровень. Затем вызов процедуры dhQuickSort (четвертый уровень) выполняется для параметров i (10) и intRight (10). Снова происходит немедленный возврат. Третий уровень завершен, поэтому программа возвращается на второй. Здесь отсортированы оба фрагмента массива, и программа возвращается на первый уровень, где обе половины массива также отсортированы, поэтому работа программы завершается. Наконец, весь массив отсортирован!
Прелесть компьютеров заключается в том, что вам не нужно следить за выполнением всех этих шагов при каждой сортировке. Поскольку вы теперь сами убедились в том, что алгоритм будет работать в любом случае, им можно пользоваться без долгих раздумий (тем более, что вы, наверное, решили для себя раз и навсегда никогда больше не копаться в алгоритме Quicksort столь тщательно). В разделе "Использование алгоритма Quicksort" показано, как вызывать процедуру dhQuickSort из вашего приложения.
12345678910
Шаг 17
Шаг 18
Шаг 19
Шаг 20
Шаг 21
Шаг 22
Шаг 23
Отсорти
рованный |||аВ|||Н||ВН|||М|||НЯ|^^
массив ^SSIlSSBSSSSSSsSS&Ssl^S^&SS^I
Рис. 4.8. Шаги с семнадцатого по двадцать третий и окончательный вид массива
Использование алгоритма Quicksort
Поскольку процедура dhQuickSort рекурсивно вызывает саму себя все с меньшими и меньшими сегментами вашего массива, она должна принимать параметры, содержащие массив, а также начальную и конечную точки сортируемого сегмента. В начале сортировки требуется задать весь массив целиком. При таком вызове процедуры граничные параметры необязательны. В момент своего запуска dhQuickSort проверяет значения необязательных параметров intLeft и intRight. Если какое-либо из них равно dhcMissing (константа, равная -2), процедура знает, что границы массива нужно определить с помощью функций LBound и UBound.
Существует две возможности задать исходный массив для процедуры dhQuickSort: либо непосредственно передать реальный массив, либо передать переменную
типа Variant, которая "содержит" массив. Для непосредственного задания массива используйте такой фрагмент программы:
Dim avarltems(l То 10) As Integer
• Теперь заполните массив.
Call dhQuickSort(avarltems())
* Сейчас массив отсортирован.
Чтобы использовать переменную типа Variant, напишите такой код:
Dim varltems as Variant
* Заполнение массива значениями. Сейчас для этого используется
' функция Array с некоторыми данными, взятыми для примера.
varltems = Array(l, 10, 29, 37, 45)
’ Можно присвоить массив переменной типа Variant непосредственно, varltems = avarSomeArray() Call dhQuickSort(varltems)
В любом случае по возвращении из процедуры dhQuickSort массив, который вы задали, будет отсортирован, и вы сможете продолжить работу с упорядоченными данными.
Применение ядра баз данных
Если ваши данные записаны в виде таблицы базы данных Microsoft Jet, то, скорее всего, для получения отсортированных данных вы предпочтете использовать Jet! Вместо того, чтобы задействовать методы обработки массивов, достаточно просто применить запрос или SQL-инструкцию для получения данных в нужном порядке. Предположим, в таблице tblCustomers имеется столбец LastName. Ни один метод выборки данных в отсортированном виде нс сработает быстрее такого выражения SQL:
SELECT [LastName] FROM tblCustomers ORDER BY [LastName];
Конечно, данный способ применим и к любым другим SQL-совместимым источникам данных.
Этот совет относится к любой программной среде, которая предоставляет собственные средства сортировки. Если таблица Excel содержит данные, не имеет смысла копировать их в массив, а затем сортировать.
Поскольку Excel выполняет сортировку очень просто и быстро, целесообразно воспользоваться этим до перемещения данных в массив VBA. Перед тем как использовать VBA для сортировки данных, убедитесь в том, что данную задачу нельзя решить встроенными средствами приложения — такой подход почти всегда предпочтительнее.
Проблемы быстродействия
Почему выбран такой сложный алгоритм сортировки? Да, этот метод выглядит устрашающе, и, исходя только из соображений сложности, можно прийти к выводу, что лучше использовать какой-нибудь другой алгоритм. Однако это не так. Пример 4.4 представляет простую программу сортировки, использующую стандартный алгоритм пузырьковой сортировки Bubblesort. Этот метод прост для понимания, однако может оказаться слишком медленным при выполнении. Алгоритм Bubblesort двигается по массиву, сравнивая каждый элемент со следующим. Если элементы расположены беспорядочно, он упорядочивает их путем перестановки. После прохождения всего массива наибольший элемент "всплывет" наверх (отсюда и название метода). После следующего прохода "всплывут" два наибольших элемента. По завершении каждого прохода несортированная часть массива становится на один элемент меньше. Сортировка продолжается, пока в результате очередного прохода не будет сделано ни одной операции перестановки элементов либо пока не закончатся элементы для сортировки.
Пример 4.4. Простой алгоритм сортировки пузырьковым методом
Sub dhBubbleSort (varltems As Variant)
* Стандартная пузырьковая сортировка.
Dim fSorted As Boolean
Dim inti As Integer Dim intJ As Integer Dim intltems As Integer Dim varTemp As Variant
intltems = UBound(varltems)
* Значение inti устанавливается на единицу
* меньшим значения нижней границы.
inti = LBound(varltems) - 1
Do While (inti < intltems) And Not fSorted fSorted = True inti = inti + 1
For intJ = 1 To intltems - inti
If varltems(intJ) > varltems(intJ + 1) Then varTemp = varltems(intJ) varltems(intJ) = varltems(intJ + 1) varltems(intJ +1) = varTemp fSorted = False
End If
Next intJ
Loop
End Sub
Да, эта программа намного проще, чем использующая алгоритм Quicksort. А как их сравнить? Процедура TestSortTimes () вызывает обе программы и сравнивает результаты их работы.
Пример 4.5. Процедура сравнения алгоритмов сортировки
Sub TestSortTimes(intCount As Integer)
Dim sw As Stopwatch
Dim inti As Integer
Dim varData() As Variant
ReDim varData(l To intCount)
For inti = 1 To intCount
varData(inti) = Format(Int(99 * Rnd + 1), "00")
Next inti
Set sw = New Stopwatch sw.StartTimer
Call dhQuickSort(varData)
Debug.Print "Using simple quicksort: " & sw.EndTimer
For inti = 1 To intCount
varData(inti) = Format(Int(99 * Rnd + 1), "00")
Next inti
sw.StartTimer
Call dhBubbleSort(varData)
Debug.Print "Using bubble sort: " & sw.EndTimer
For inti = 1 To intCount varData(inti) = inti Next inti
sw.StartTimer
Call dhQuickSort(varData)
Debug.Print "Using simple quicksort (ordered): " & sw.EndTimer
sw.StartTimer
Call dhBubbleSort(varData)
Debug.Print "Using bubble sort (Ordered): " & sw.EndTimer End Sub
Тестовая процедура создает массив случайных чисел и вызывает по очереди обе процедуры сортировки. Затем она формирует массив упорядоченных чисел (т.е. массив, который уже отсортирован) и снова вызывает обе программы сортировки. В следующей таблице представлены результаты для массива из 2000 элементов. (Время приведено в миллисекундах для одной итерации сортировки. Эксперимент проводился на компьютере с процессором Pentium Pro с тактовой частотой 200 МГц и 64 Мб оперативной памяти.)
Тип массива
Quicksort
Bubblesort
Случайный
220
21000
Упорядоченный
70
10
Как видно из теста, для случайных чисел алгоритм Quicksort работает значительно быстрее, чем алгоритм Bubblesort. С другой стороны, последний использует преимущества предварительно упорядоченных массивов и в этом случае превосходит по быстродействию алгоритм Quicksort.
Как размер массива влияет на время, необходимое для его сортировки? Результаты сравнения для случайных наборов не отсортированных чисел сведены в таблицу, представленную ниже.
Количество элементов
Quicksort
Bubblesort
100
10
40
1000
100
4000
2000
200
21000
5000
500
140000
Впечатляет, не так ли? При необходимости отсортировать несколько сотен элементов алгоритм Bubblesort просто непригоден. Время сортировки экспоненциально возрастает в зависимости от количества сортируемых элементов. С другой стороны, время, требуемое для выполнения Quicksort, зависит от числа элементов в сортируемом массиве более или менее линейно.
Какой из алгоритмов выбрать? Конечно же, это зависит от ваших данных. Для сортировки десяти чисел не имеет значения, какой алгоритм применять. Если вы сортируете элементы, которые до этого уже были отсортированы, рекомендуем воспользоваться алгоритмом Bubblesort. В противном случае лучше выбрать более сложный, но и более эффективный алгоритм Quicksort.
Сортировка семейств
Использование определенных пользователем семейств придает значительную гибкость приложениям на VBA. Поскольку семейства представляют собой динамические структуры данных, можно по желанию добавлять и удалять из них элементы, не беспокоясь об изменении размерности и сохранности данных. С другой стороны, они не пригодны для сортировки. Это плохо, потому что семейства обеспечивают очень простой способ хранения и обработки массивов с большим количеством элементов. Было бы здорово, если бы их можно было и сортировать.
Какие же сложности возникают при использовании семейств? К сожалению, тут есть несколько существенных проблем, которые необходимо рассмотреть.
Работа с семействами
При добавлении элементов в семейство его необходимо идентифицировать, т.е. снабдить некоторым значением, в качестве которого часто используется уникальное строковое значение (называемое ключом). Например, при открытии формы в Microsoft Access программа автоматически добавляет объект формы к семейству Forms. При этом имя формы используется в качестве ее уникального идентификатора. Таким образом, для получения объекта семейства (ссылки на форму) можно воспользоваться одним из следующих способов:
Forms(1)
* или
Forms ("frmTest")
Для форм Access всегда можно узнать значение уникального идентификатора; для этого нужно просто проверить свойство Name данной формы. Однако для семейств, определенных пользователем, это невозможно. По неизвестным причинам нельзя получить значение ключа для заданного элемента семейства, созданного пользователем. Такой недостаток сильно затрудняет, если не исключает вообще, возможность манипулирования элементами семейства. Странно, не правда ли? Можно использовать ключ для поиска элемента, но, имея элемент, нельзя узнать значение его ключа. Было бы замечательно, если бы можно было добавлять элементы и их уникальные ключи к семейству, а затем работать с семейством в отсортированном по ключу порядке, нс так ли? Да, это было бы здорово, но, к сожалению, нереально — VBA не позволяет сортировать семейства по ключу, и вы не в состоянии узнать значение ключа, чтобы сделать это самостоятельно.
Почему это мешает вам выполнить сортировку семейства самостоятельно? Сортировка требует перемещения элементов из одного места в другое. Перемещение подразумевает копирование элемента во временное местоположение, удаление его из семейства, а затем копирование в новое место. Однако вы не можете поступить подобным образом с элементами семейства; поскольку невозможно узнать значение ключа, любая попытка переместить элемент приведет к потере его уникального идентификатора!
Это означает, что обычные методы сортировки, требующие перестановки элементов, в данном случае не пригодны. В настоящем разделе мы представляем альтернативный вариант. Проявив изобретательность, вы, вероятно, сможете предложить и свои методы. Простейшим способом сортировки семейства является сортировка элементов при их добавлении к семейству (таким образом, семейство все время будет отсортированным).
Сортировка в момент вставки
Один из возможных вариантов сортировки семейства довольно прост: всегда поддерживать его в отсортированном состоянии, т.е. при добавлении к семейству нового элемента нужно убедиться, что он будет вставлен в нужное место. Вставку выполнить несложно, поскольку VBA позволяет задать элемент, перед которым вы хотите вставить новый элемент.
Процедура dhAddToSortedCollection (пример 4.6) позволяет добавлять элементы к семейству, постоянно поддерживая его в отсортированном виде. Ниже объясняется, как работает процедура dhAddToSortedCollection.
Пример 4.6. Благодаря процедуре dhAddToSortedCollection семейство всегда отсортировано
Function dhAddToSortedCollection(col As Collection, _ varNewItem As Variant, Optional strKey As String = "") _ As Boolean
* Добавление к семейству нового значения
* (и при необходимости соответствующего ключа).
On Error GoTo HandleErrors
Dim inti As Integer
Dim fAdded As Boolean
Dim fUseKey As Boolean
fUseKey = (Len(strKey) > 0)
* При первом проходе этот цикл не производит никаких действий. For inti = 1 То col.Count
If varNewItem < col.Item(inti) Then If fUseKey Then col.Add varNewItem, strKey, inti Else col.Add varNewItem, , inti End If fAdded = True Exit For End If Next inti * Если элемент не был добавлен из-за того, что он * должен помещаться после текущего списка элементов * либо из-за того, что в семействе пока нет ни одного элемента, * он добавляется в конец семейства. If Not fAdded Then If fUseKey Then col.Add varNewItem, strKey Else col.Add varNewItem End If End If dhAddToSortedCollection = True
ExitHere:
Exit Function
HandleErrors: dhAddToSortedCollection = False Select Case Err.Number Case dhcErrKeylnUse
* Это единственная возможная ошибка.
Case Else * Ничего не нужно выполнять. Просто * отправьте ошибку назад в точку вызова. End Select Resume ExitHere End Function
Первым шагом процедуры является проверка на наличие в точке вызова уникального значения ключа:
fUseKey = (Len(strKey) > 0)
Если это так, то впоследствии программа передаст данный ключ методу Add объекта Collection.
Следующий фрагмент программы пытается найти правильную позицию для вставки нового элемента. Программа просматривает каждое значение семейства, и если добавляемое значение меньше значения текущего элемента семейства, то правильное место для вставки нового элемента найдено. Когда уникальное значение
ключа задано, программа вызывает метод Add объекта Collection, передавая ему новый элемент, ключ и позицию, перед которой следует выполнить вставку. Если ключ не указан, передаются только новый элемент и позиция. Вставив новое значение, процедура устанавливает флаг (и в дальнейшем программа знает, что вставка произведена), а затем выходит из цикла.
For inti = 1 То col.Count
If varNewItem < col.Item(inti) Then If fUseKey Then col.Add varNewItem, strKey, inti
Else
col.Add varNewItem,col.Add varNewItem,intiinti
col.Add varNewItem,
col.Add varNewItem,
inti
inti
End If fAdded = True Exit For
Exit For
Next inti
Завершение цикла может произойти по двум причинам: либо был вставлен элемент, и программа совершила выход из цикла, либо элемент вставлен не был, и цикл просто закончился. В последнем случае значение флага fAdded будет False, и следующий фрагмент программы может просто добавить новый элемент в семейство. Эта ситуация имеет место только в том случае, если новый элемент нужно добавить в конец семейства, т.е. когда его значение превышает значения всех существующих элементов, либо когда в семействе нет ни одного элемента.
If Not fAdded Then
If fUseKey Then
col.Add varNewItem, strKey
Else col.Add varNewItem
End If
End If
Какие ошибки могут возникнуть и зачем нужна процедура обработки ошибок? Чаще всего ошибка возникает при попытке добавить два элемента с одним и тем же идентификационным ключом. При этом функция выдает ошибку 457 — Запись уже связана с элементом данного семейства. Вызывающая программа должна обработать эту ошибку.
Каковы недостатки описанного метода? Во-первых, он слишком медленный. Конечно, так и должно быть, поскольку новый элемент сопоставляется в среднем с половиной имеющихся в семействе элементов. Значит, чтобы вставить в семейство 20 элементов, вам придется выполнить несколько сотен сравнений. Каждое сравнение требует достаточно много времени.
Во-вторых, поскольку программа сравнивает значения, в семействе можно использовать только простые типы данных (строки, числа и т.д.). Если вы захотите применить описанный метод для более сложных семейств, необходимо переделать программу для выполнения более сложных сравнений. Поскольку вы, скорее всего, будете применять этот метод с простыми семействами, мы предоставили вам возможность модифицировать программу.
Поиск
При наличии отсортированных данных вам, вероятно, потребуется осуществлять поиск определенного элемента в массиве данных. Как и сортировка, поиск является одной из важнейших операций обработки данных, и мы не ставим перед собой цели подробно осветить этот вопрос. С другой стороны, вы не найдете столько алгоритмов для поиска, сколько для сортировки. Поэтому здесь рассмотрен единственный метод поиска, но зато во всех подробностях. В данном разделе речь пойдет об алгоритме бинарного поиска Binary Search и о том, как его использовать для данных, хранящихся в массиве.
Почему применяется алгоритм Binary Search?
Представьте себе, что вы взяли телефонную книгу и намереваетесь отыскать телефон местного ресторана вегетарианской кухни Vegetarian Pizza Kitchen. Можно, конечно, начиная с первого названия в книге, двигаться по списку, рассматривая каждую позицию. Это отнимет у вас уйму времени, учитывая то, что в книге, скорее всего, сотни тысяч телефонов, а вам нужно пройти весь список до буквы V.
Разумный пользователь телефонной книги, конечно, откроет книгу на букве V и начнет поиск с этого места. Представьте на секунду, что телефонная книга не имеет буквенного указателя, и, чтобы определить, в каком месте книги вы находитесь, вам нужно просматривать содержимое страницы. Как в этом случае быстро отыскать Vegetarian Pizza Kitchen? Скорее всего, вы ткнете пальцем наугад где-нибудь посредине книги, увидите, что названия, начинающиеся на V, идут дальше, и отбросите все, что находится до положения вашего пальца. Затем вы проверите среднюю страницу второй половины книги и определите, в какой половине оставшейся части книги находятся слова на букву V. Продолжая этот процесс, вы будете делить книгу на все меньшие и меньшие части, пока, наконец, не найдете нужной записи.
Если телефонная книга содержит 1000 страниц, на первом шаге вы исключите из рассмотрения 500 страниц, на втором — еще 250. Неплохо всего для двух шагов! Поскольку принцип работы алгоритма Binary Search аналогичен, становится понятно, почему он обеспечивает очень быстрый поиск в больших наборах данных: после каждой проверки отсеивается 50% возможных значений, пока вы не наткнетесь на нужное. Строго говоря, алгоритм Binary Search в общем случае выполняет только 1од2п поисков, где п — количество просматриваемых элементов. Таким образом, чтобы найти нужный элемент среди 1000 значений или убедиться, что его там нет, требуется просмотреть не более 10 элементов. (Для читателей, испытывающих сложности при работе с логарифмами, 2 в десятой степени равняется 1024, так что 1од21000 немного меньше 10.) Замечательной особенностью логарифмической зависимости является то, что при удвоении числа элементов количество операций поиска увеличивается всего на одну.
Примечание: Если вы упустили это в рассмотренном примере, учтите сейчас: алгоритм бинарного поиска может работать только в том случае, когда исходные данные предварительно отсортированы. Если вы хотите использовать этот метод, нужно тем или иным способом обеспечить сортировку исходных данных до применения алгоритма бинарного поиска.
Как работает алгоритм Binary Search?
В действительности не существует единственного формального алгоритма Binary Search — их множество. Реализация программы, которую вы получите, зависит оттого, кто вам ее предоставит. Приведенный в этой книге вариант алгоритма работает следующим образом:
Задается отсортированный массив данных и значение, которое нужно найти.
Устанавливаются верхняя и нижняя границы поиска.
Пока нижняя граница меньше верхней:
вычисляется средняя точка оставшейся части массива;
если искомое значение больше среднего, выполняется корректировка нижней границы; в противном случае — корректировка верхней.
Если элемент в нижней позиции совпадает с искомым значением, процедура возвращает соответствующий индекс позиции. В противном случае возвращается значение, указывающее на то, что искомый элемент не обнаружен.
Процедура dhBinarySearch, реализующая алгоритм Binary Search, представлена в примере 4.7.
Пример 4.7. Использование процедуры dhBinarySearch для поиска значения в отсортированном массиве
Function dhBinarySearch(varltems As Variant, _ varSought As Variant) As Integer
Dim intLower As Integer
Dim intMiddle As Integer
Dim intUpper As Integer
intLower = LBound(varltems)
intUpper = UBound(varltems) Do While intLower < intUpper
* Увеличение нижней и уменьшение верхней границы, таким образом,
* чтобы значение varSought не выходило за границы диапазона, 1 если оно вообще там есть.
intMiddle = (intLower + intUpper) \ 2
If varSought > varltems(intMiddle) Then intLower = intMiddle + 1
Else
intUpper = intMiddle End If
Loop
If varltems(intLower) = varSought Then dhBinarySearch = intLower
Else
dhBinarySearch = -1
End If
End Function
Для того чтобы объяснить принцип работы алгоритма бинарного поиска, используем в качестве исходного массива тот же массив из десяти элементов, который применяли при обсуждении алгоритма Quicksort. В данном случае задача состоит в определении положения некоторого числа в массиве. В первом примере (рис. 4.9) поиск завершается успешно, а во втором (рис. 4.10) — искомый элемент обнаружить не удастся.
TestSortDemo
6
33
63
21
Before
: 41
41 71
1-6)
Value: Sorting
41
Items
41 71
71 l->6
33
63
21
: 1-6)
; Swap
41
41 71
Ai
33
63
21 Aj
41
41 21
33
63
71
41
41 21
33
63
71
1-5)
Value: Sorting
41
Items
41 21
21 l->5
33
63
1-5) Swap
2-5)
Value:
Sorting 2-5)
Swap
Swap
4-5)
Value: Sorting 4-5)
41
: Ai
21
21
Items
Items
41 21
Aj
41 41
41 41
41 41
41
2->5
41 41
Ai
33 41
Ai
33 41
4->5
33
33 33
33
33 Aj 41
41
41 41
41
63
63
63
63
63
63
63
63
63
ij After
:21
33 41
63
63
Рис. 4.9. Использование алгоритма Binary Search для определения местоположения значения в массиве
Ниже подробно описывается работа алгоритма Binary Search, используемого процедурой dhBinarySearch. В данном случае программа просматривает массив, приведенный на рис. 4.9 (шаг 1), в поисках значения 34.
Примечание: В этих примерах будет использоваться та же терминология, что и при описании алгоритма Quicksort: для указания значения элемента массива, который находится в позиции х, применяется обозначение хА. Если, например, значение элемента массива varltems, который находится в пятой позиции, равно 12 (т.е. varltems (5) = 12), а переменная х содержит значение 5, то значением хА будет 12.
Пои.гк 34.
Рис. 4.10. Алгоритм Binary Search терпит неудачу при поиске элемента, которого в массиве нет
В качестве границ поиска устанавливаются верхняя и нижняя границы заданного массива:
intLower = LBound(varltems)
intUpper = UBound(varltems)
Если intLower меньше, чем intUpper (а в данном случае это так и есть), переменной intMiddle присваивается среднее этих двух чисел:
intMiddle = (intLower + intUpper) \ 2
Поскольку значение varSought (34) больше, чем intMiddleА (30), значение intLower устанавливается равным intMiddle + 1 (6):
If varSought > varltems(intMiddle) Then intLower = intMiddle + 1
Else
intUpper = intMiddle End If
Так как intLower (6) все еще меньше, чем intUpper (10), переменной intMiddle присваивается среднее этих чисел (8). Поскольку varSought меньше, чем intMiddleA (48), значение intUpper устанавливается равным intMiddle.
Так как intLower (6) все еще меньше, чем intUpper (8), переменной intMiddle присваивается среднее этих чисел (7). Поскольку varSought меньше, чем intMiddleA (48), значение intUpper устанавливается равным intMiddle.
Так как intLower (6) все еще меньше, чем intUpper (7), переменной intMiddle присваивается среднее этих чисел (6). Поскольку varSought (34) нс больше, чем intMiddleA (34), значение intUpper устанавливается равным intMiddle.
Так как теперь значение intLower (6) не меньше значения intUpper (6), поэтому происходит выход из цикла. Поскольку intLowerА равно значению varSought, в результате выполнения функции возвращается значение intLower:
If varltems(intLower) = varSought Then dhBinarySearch = intLower
Else
dhBinarySearch = -1 End If
Примечание: Возможно, вам показалось, что число 34 можно было бы найти в заданном массиве за то же количество шагов (если нс меньшее) путем последовательного просмотра элементов, начиная с первого. Да, вы правы, — для небольших наборов данных почти всегда быстрее просто перебрать все элементы подряд. Однако это всего лишь пример; обычно вы не будете осуществлять бинарный поиск среди десяти чисел. Работая с наборами в 100 и более элементов, вы почувствуете ощутимую разницу в быстродействии между алгоритмами линейного и бинарного поиска.
Что происходит, когда бинарный поиск заканчивается неудачей? Далее представлен процесс поиска значения 65 в массиве, который его не содержит (рис. 4.10).
В качестве границ поиска устанавливаются верхняя и нижняя границы заданного массива:
intLower = LBound(varltems)
intUpper = UBound(varltems)
Если intLower меньше, чем intUpper (а в данном случае это так и есть), переменной intMiddle присваивается среднее этих двух чисел:
intMiddle = (intLower + intUpper) \ 2
Поскольку значение varSought (65) больше, чем intMiddleA (30), значение intLower устанавливается равным intMiddle + 1 (6):
If varSought > varltems(intMiddle) Then
intLower = intMiddle + 1
Else
intUpper = intMiddle
End If
Так как intLower (6) все еще меньше, чем intUpper (10), переменной intMiddle присваивается среднее этих чисел (8). Поскольку varSought больше, чем intMiddleA (48), значение intLower устанавливается равным intMiddle + 1 (9).
Так как intLower (9) все еще меньше, чем intUpper (10), переменной intMiddle присваивается среднее этих чисел (9). Поскольку varSought меньше, чем intMiddleA (79), значение intUpper устанавливается равным intMiddle (9).
Так как теперь значение intLower (9) не меньше значения intUpper (9), происходит выход из цикла. Поскольку intLowerА не является значением, которое искали, возвращается -1. Это говорит о том, что поиск не увенчался успехом.
Вы все еще нс убедились, что бинарный поиск лучше линейного? Что если поместить все числа от 1 до 1000 в массив и пытаться найти одно значение ближе к началу массива, второе — ближе к концу, третье — посредине, а также значение, которого там вообще нет, используя как бинарный, так и линейный поиск, а затем сравнить время выполнения этих операций? Процедура CompareSearch выполняет эти операции. Функция dhLinearSearch, представленная в примере 4.8, начинает поиск с первого элемента массива, сравнивает поочередно каждое значение с искомым и завершает работу при обнаружении совпадения. Конечно, если искомое значение не найдено, поиск завершается по достижении конца массива.
Пример 4.8. Линейный поиск — самый медленный из всех возможных способов поиска
Function dhLinearSearch(varltems As Variant, _ varSought As Variant) As Integer
Альтернатива бинарному поиску. Для нахождения
* значения в массиве используется линейный поиск.
Dim intPos As Integer
Dim fFound As Boolean
fFound = False
For intPos = LBound(varltems) To UBound(varltems) If varSought = varltems(intPos) Then fFound = True Exit For End If Next intPos If fFound Then dhLinearSearch = intPos
Else dhLinearSearch = -1 End If End Function
Для сравнения быстродействия линейного и бинарного поиска процедура CompareSearch заполняет массив требуемым количеством элементов. (Если не указать размер массива, он принимается равным 1000.) Затем с помощью процедуры dhBina- rySearch осуществляется поиск трех элементов в массиве и одного элемента, которого в массиве нет. Потом CompareSearch повторяет этот процесс, используя функцию LinearSearch. В обоих случаях вначале выполняется поиск элементов, находящихся на расстояниях 10, 90 и 50% от начала массива, а затем поиск отсутствующего в массиве элемента. Для каждого случая поиск проводится 1000 раз. Как видно из рис. 4.11, различие в быстродействии впечатляющее. Для массива из 1000 элементов поиск элемента, находящегося в середине массива, занимает 40 миллисекунд при бинарном поиске и около 1100 миллисекунд при линейном (приблизительно в 27 раз больше). Для массива из 10000 элементов различие еще больше. В этом
случае бинарный поиск занимает 50 миллисекунд, а линейный — около 11000! Таким образом, при линейном поиске требуется в 220 раз больше времени. (При этом о разнице во времени поиска элемента, который находится ближе к концу списка речи не идет. Для обнаружения такого значения методом бинарного поиска больше времени не потребуется, а линейный поиск будет продолжаться в среднем вдвое дольше.)
CompareSearch(Той)
Binary search early item: 40
Binary search late iteto: 40
Binary search middle item: (3^)
Binary search not found: 41
Linear search early item: 220
Linear search late item: 1933_
Linear search middle item: ^08y
Linear search not found: 202T^
6060 :(1[o) 50215319218 :(1072^6060 :(1[o) 50215319218 :(1072^CompareSearch (10000) Binary search early item: Binary search late item: Binary search middle item Binary search not found: Linear search early item: Linear search late item: Linear search middle item
60
60 :(1[o) 50
2153
19218 :(1072^
60
60 :(1[o) 50
2153
19218 :(1072^
Linear search not found: 20L5T
Puc. 4.11. Сравнение алгоритмов линейного и бинарного поиска
Следует помнить, что в тех случаях, когда набор данных невелик или данные не отсортированы, лучше использовать линейный поиск. Однако если массив содержит 100 и более элементов, целесообразно применять метод бинарного поиска, а не линейного. Это особенно актуально, когда программа должна часто выполнять поиск данных.
Если данные находятся в таблице
Как и при сортировке, стоит серьезно подумать о целесообразности использования описанных методов поиска в массивах, если ваши данные находятся в таблице базы данных Microsoft Jet или в электронной таблице Excel. Таким образом, необходимо учитывать особенности конкретного приложения. Независимо от того, насколько быстро работает ваша программа сортировки, Jet выполняет сортировку быстрее, используя собственные возможности.
То же самое относится и к процедуре поиска: даже если ваша программа поиска достаточно эффективна, написать SQL-запрос, который возвращает единственную нужную строку, быстрее, чем загрузить данные в массив и провести бинарный поиск. Поэтому дважды подумайте, прежде чем воспользоваться предложенными в этой главе методами поиска и сортировки. Если ваши данные еще не в массиве, копирование их с диска с единственной целью произвести поиск или сортировку не будет сильной стороной вашей программы. Excel, Access и Word предоставляют собственные средства для поиска и сортировки данных, и, прежде чем копировать данные в массив и работать с ними там, стоит в полной мере использовать эти возможности.
С другой стороны, если данные уже находятся в массиве (или любой другой структуре данных в памяти), нецелесообразно копировать их в таблицу базы данных или в электронную таблицу Excel только для того, чтобы выполнить поиск или сортировку. За время, которое вы потратите на запись данных на диск для создания с помощью Jet набора записей, сформированного на основе таблицы с вашими данными, вы могли бы уже давным-давно отсортировать ваш массив и заниматься своими делами. Таким образом, для каждой задачи нужно изучить ситуацию, чтобы правильно выбрать средства ее решения.
Использование алгоритма Binary Search
Вызов процедуры dhBinarySearch довольно прост — достаточно задать ей массив для поиска и искомый элемент:
intPos = dhBinarySearch(varArray, varSought)
Если в результате поиска в массиве varArray искомый элемент varSought найден, процедура возвращает его позицию в массиве, в противном случае возвращается значение -1.
ffВнимание: Не попадитесь в ту же ловушку, что и мы: перед использованием процедуры dhBinarySearch исходный массив должен быть отсортирован. Если этого не сделать, алгоритм бинарного поиска даст неправильный результат. Если по каким-либо причинам вы не можете отсортировать данные, воспользуйтесь процедурой линейного поиска dhLinearSearch (она вызывается точно так же, как и процедура dhBinarySearch).
f
f
Например, следующий фрагмент программы формирует массив случайных чисел и предлагает пользователю угадать как можно больше выбранных чисел. Как только пользователь выбирает неправильное значение, процедура отображает количество правильно угаданных чисел.
Sub SillySearch()
Dim aintltems(l To 100) As Integer
Dim inti As Integer
Dim intPos As Integer
Dim intCount As Integer
For inti = 1 To 100 aintltems(inti) = Int(Rnd * 100) + 1 Next inti Call dhQuickSort(aintltems) Do inti = Vai(InputBox("Выберите число от 1 до 100")) intPos = dhBinarySearch(aintltems, inti) If intPos > 0 Then intCount = intCount + 1 End If
Loop Until intPos < 0
MsgBox "Вы угадали " & intCount & " правильных значений!" End Sub
Внимание: Поскольку процедура dhBinarySearch возвращает значение -1 (искомый элемент не найден), вы не сможете выполнять поиск в массивах, имеющих отрицательные индексы. Поэтому если необходимо провести поиск, например, в массиве с границами от -10 до 10, нужно переделать процедуру dhBinarySearch так, чтобы она использовала другое значение для сообщения о неудачном завершении поиска.
Резюме
Эта глава не является фундаментальным исследованием методов сортировки и поиска. Здесь представлены некоторые решения, связанные с вопросами поиска и сортировки, включая:
определение быстродействия одного метода по сравнению с другим с помощью класса Stopwatch;
создание и использование массивов;
принципы работы алгоритма Quicksort и сортировки массивов с его помощью;
поддержание семейства в отсортированном виде;
работа алгоритма Binary Search и поиск элементов в отсортированном массиве с его помощью;
сравнение быстродействия алгоритмов бинарного и линейного поиска и рекомендации, в каких ситуациях следует использовать тот или иной метод.
Имея на вооружении процедуры dhQuickSort и dhBinarySearch и понимая, как они работают и как их использовать, вы сможете выполнять сортировку и поиск в простых массивах. Кроме того, при необходимости у вас есть возможность видоизменить эти процедуры для обработки данных других типов.
ГЛАВА 5
Профессиональная разработка приложений на VBA
Корректная обработка ошибок
Процесс отладки
Элементы профессионального проектирования
Если вы читаете эту книгу, то, скорее всего, являетесь серьезным разработчиком VBА-программ. Занимаясь созданием пользовательского программного обеспечения, или считая себя опытным пользователем такого пакета как Microsoft Office, вы тратите значительную часть своего времени на программирование. Возможно, вы разрабатываете программные продукты (или, по крайней мере, программные модули), с которыми работают другие. Кроме того, вы хотели бы иметь возможность неоднократно использовать написанный код. Если вам знакомы эти проблемы, то данная глава для вас. В ней рассмотрен ряд аспектов, которые можно отнести к категории профессиональных разработок, освещены такие вопросы, как обработка ошибок, отладка, стандарты присвоения имен и проектирование объектных моделей. Наша цель — предложить методы для создания надежных, мобильных программ на VBA без ошибок. (Да, последнее — задача непростая, однако мы прилагаем для этого все усилия.)
Корректная обработка ошибок
Данная глава начинается с описания процесса обработки ошибок, поскольку он является основной особенностью профессионально спроектированного приложения. При работе любой программы, в конечном счете, возникает ситуация, обработка которой в ней не предусмотрена. То, как вы справитесь с этой ситуацией, влияет на само приложение и на тех, кто его использует. В данном разделе рассмотрены ошибки трех типов, встречающиеся в программах, проанализированы способы их обработки и предложены некоторые рекомендации по включению обработки ошибок в каждую процедуру.
Типы ошибок
При разработке приложения необходимо учитывать ошибки трех типов:
ошибки компиляции;
ошибки выполнения;
логические ошибки.
Ошибки компиляции проявляются в процессе написания программы и обычно являются следствием синтаксических ошибок или неправильного использования функций или свойств. Чаще всего эти ошибки легко идентифицируются и исправляются. VBA проверяет синтаксис каждой строки программы по мере ввода и при обнаружении синтаксической ошибки выдает предупреждающее сообщение, а также выделяет цветом ошибочный текст. О синтаксических ошибках во фрагментах программы, состоящих из нескольких строк, например в цикле For... Next, и других ошибках компиляции VBA сообщает при компиляции приложения. Любое поддерживающие VBA приложение обязательно содержит средства компиляции программы.
I Совет: Квалифицированным программистам на VBA диалоговое окно с со- L I у общениями о синтаксических ошибках может надоесть. Предотвратить появление сообщений о синтаксических ошибках можно, отключив флаг Проверка синтаксиса (Auto Syntax) в меню VBA Параметры (Options). Несмотря на свое название, эта опция не отключает проверку синтаксиса полностью. VBA все же проверяет его и предупреждает вас, выделяя ошибочную строку цветом, но диалоговое окно с сообщением об ошибке не отображается.
Ошибки выполнения более коварны, чем ошибки компиляции, поскольку, как видно из названия, они не проявляются до тех пор, пока приложение не будет запущено. Такие ошибки происходят в том случае, когда синтаксически правильная строка программы не может быть выполнена из-за текущих условий среды. Допустим, вы пытаетесь удалить файл на диске с помощью процедуры VBA Kill, задав ей имя несуществующего файла. Хотя соответствующий оператор не содержит синтаксических ошибок, он не может быть выполнен, поскольку неправильно указан исходный параметр. В этом случае VBA сгенерирует ошибку, которую ваша программа может перехватить и обработать. (Данный процесс называется перехватом ошибки?) Если ваша программа не перехватила ошибку, VBA обрабатывает ее своими средствами. В зависимости от того, какое поддерживающее VBA приложение вы используете, это может либо оставить ваше приложение в работоспособном состоянии, либо аварийно завершить его работу. Поэтому важно, чтобы программа перехватывала все ошибки выполнения, которые могут произойти. В данном разделе показано, как это сделать. Благодаря тщательному тестированию можно избежать большинства проблем, являющихся следствием ошибок выполнения.
Самые коварные ошибки — логические. Они (хорошо известные программистам как жучки (bugs)) проявляются в том, что программа компилируется и выполняется без ошибок, однако выдает неправильные результаты. Эти ошибки обычно возникают из-за оплошности или недосмотра программиста и очень сложно вылавливаются. Рассмотрим программу, представленную в примере 5.1.
Пример 5.1. Программа, которая не содержит синтаксических ошибок, но работает неправильно
Sub Fileinfo(strPath As String)
Dim strDir As String
Вырезает имя файла, оставляя имя каталога. strDir = TrimFileName(strPath)
’ Отображает диалоговое окно.
MsgBox "The file " & strPath & _
" is located in " & strDir, _ vblnformation, "File Info" End Sub
Function TrimFileName(strPath As String) As String
Dim cb As Long
Обрезает символы, начиная с первого символа строки справа ’ и заканчивая первым встретившимся символом обратной косой черты, * или всю строку полностью, если символа
* обратной косой черты нет.
Do
strPath = Left (strPath, Len (strPath) - 1)
Loop Until Right(strPath, 1) = "\" Or _
Len(strPath) = 0
* Возвращает результат.
TrimFileName = strPath
End Function
Процедура Fileinfo принимает в качестве исходного значения путь к файлу и выводит диалоговое окно, отображающее полный путь и имя каталога. Эта процедура вызывает функцию TrimFileName, которая отделяет имя файла от пути, сохранив имена каталогов. При вызове функции TrimFileName из окна отладки она работает правильно. Однако при вызове процедуры File Info оператор MsgBox выводит не полный путь, а имена каталогов два раза подряд. В чем же дело? Ошибка вызвана невнимательностью, чаще всего, неопытного программиста на VBA. Аргумент strPath функции TrimFileName задан ссылкой, а не значением, поэтому при обработке аргумента функция TrimFileName изменяет и значение переменной в функции Fileinfo.
При обнаружении ошибок компиляции или ошибок выполнения VBA сообщает о том, какая ошибка произошла, и выделяет строку программы с ошибкой. Отслеживание логических ошибок полностью возлагается на вас. К счастью, VBA предоставляет средства, которые помогут вам при поиске логических ошибок (см. раздел "Процесс отладки" данной главы).
Перехват ошибок выполнения
Если не указать иное, VBA самостоятельно обрабатывает ошибки выполнения, отображая сообщение об ошибке и прерывая работу программы в той строке, которая вызвала ошибку. Это приемлемо и зачастую полезно на стадии разработки и отладки программы, однако вряд ли пригодится в готовом программном продукте. Вместо этого ваша программа должна самостоятельно отслеживать и обрабатывать собственные ошибки, даже если при их обнаружении она просто выдает такое же сообщение, что и VBA.
Основой данного процесса является перехват ошибки, осуществляемый при помощи оператора On Error. По сути, в процедуре устанавливается обработчик ошибок, который находится в состоянии ожидания до тех пор, пока не произойдет ошибка. Когда это случается, обработчик ошибок запускается и выполняет определенные операторы, которые справляются с ошибкой.
VBA-оператор On Error имеет три формы:
On Error GoTo Label;
On Error Resume Next;
On Error GoTo 0.
!!Примечание: Обработку ошибок в VBA нужно осуществлять отдельно для каждой процедуры. Нет возможности создать универсальную программу обработки ошибок, которая срабатывала бы в ответ на любую ошибку выполнения. Несмотря на то, что вы можете создать отдельную процедуру для обработки ошибок, вам все же необходимо добавлять ее вызов в ответ на возникновение ошибки в текст каждой процедуры.
!
!
Оператор On Error GoTo Label
Оператор On Error GoTo Label является наиболее мощным перехватчиком ошибок, поскольку он обеспечивает наибольшую степень гибкости при обработке ошибок выполнения. Использование данного оператора приводит к тому, что VBA при возникновении ошибки переходит в указанное место программы. Когда в программе
после выполнения оператора On Error GoTo Label возникает ошибка, управление передается заданной метке (Label). В примере 5.2 представлена процедура, в которой показана структура обработчика ошибок.
Пример 5.2, Процедура, содержащая обработчик ошибок
Sub GenericProcWithErrorTrap()
' Заглушка, иллюстрирующая стандартный способ
* организации обработчика ошибок.
' Оператор перехвата ошибок. On Error GoTo HandleError
* Фрагмент программы, в котором могут ' происходить ошибки выполнения.
ExitHere:
' Внимание! Выход из процедуры перед обработчиком ошибок. Exit Sub
HandleError:
* В этом месте происходит обработка ошибок! Resume ExitHere
End Sub
Все начинается с оператора On Error GoTo Label. Обычно это первый исполняемый оператор. Он располагается перед описаниями локальных переменных или сразу же после них. (Его месторасположение — до или после описаний переменных — зависит от стиля программирования и личных предпочтений.) В нашем примере в качестве метки используется HandleError.
Примечание: В Basic предыдущих версий имена меток должны были быть уникальными в пределах всего проекта. В VBA данное ограничение отсутствует. Поэтому при желании вы можете использовать метку HandleError во всех процедурах.
Если при выполнении программы, расположенной за оператором On Error GoTo Label, происходит ошибка, VBA продолжает выполнять программу, начиная с оператора, который следует непосредственно за меткой. Метка должна находиться в той же процедуре, что и оператор On Error GoTo Label.
Обработчик ошибок принято размещать в самом конце процедуры. Так поступают потому, что в случае отсутствия ошибок выйти из процедуры необходимо до обработчика ошибок. В противном случае VBA выполнит код обработки ошибок. Обратите внимание на оператор Exit Sub (и связанную с ним метку ExitHere) в нашем примере.
При возникновении ошибки VBA начинает выполнять код обработки ошибок и переходит в особое состояние, которое характеризуется следующим:
обработчик ошибок, заданный оператором On Error GoTo Label, больше не действует (значит, любая ошибка выполнения, которая может произойти в обработчике ошибок, обрабатывается как неперехватываемая);
для возврата управления основной процедуре и продолжения нормальной обработки ошибок вы можете использовать оператор Resume (описан в разделе "Оператор Resume" данной главы);
для выхода из обработчика ошибок нельзя использовать операторы End Sub или End Function, так как они применяются для выхода из процедуры (общепринято, что процедура должна иметь только одну точку входа и одну точку выхода для передачи управления основной процедуре, выход из которой осуществляется посредством оператора End Sub или End Function, поэтому лучше использовать оператор Resume Label).
Оператор On Error Resume Next
Создание обработчика ошибок с оператором On Error GoTo Label может потребовать достаточно большой программы. Иногда вы захотите игнорировать ошибки. В другом случае вы точно знаете, какая ошибка может произойти, и предпочтете обработать ее без написания полного обработчика ошибок. Оператор On Error Resume Next предназначен именно для этого. Он сообщает VBA о необходимости подавить отображение стандартного сообщения об ошибке и выполнить следующую строку программы. Например, если вы пытаетесь удалить файл, не проверяя, существует он или нет, можно воспользоваться программой из примера 5.3. Если файл имеется, процедура удаляет его, в противном случае происходит ошибка выполнения, но VBA подавляет ее и процедура завершается нормально.
Пример 5.3. Применение оператора On Error Resume Next для игнорирования ошибок выполнения
Sub DeleteFile(ByVai StrFileName As String)
' Пример применения оператора On Error Resume Next.
* Удаляет файл, если он существует.
On Error Resume Next
Kill strFileName
End Sub
Оператор On Error Resume Next позволяет программе проигнорировать ошибку и продолжить работу.
Оператор On Error GoTo О
При использовании операторов On Error GoTo Label или On Error Resume Next они остаются в силе до окончания работы процедуры, объявления другого обработчика ошибок или его отмены. Отмена обработчика ошибок выполняется с помощью оператора On Error GoTo 0. Последующие ошибки будут отслеживаться VBA (или обработчиком ошибок вызывающей процедуры, как описано в разделе "Иерархия обработки ошибок в VBA" данной главы). Данный оператор также сбрасывает значение объекта Err (см. раздел "Определение типа ошибки "), поэтому, если вам нужны значения, которые он содержит, необходимо отдельно сохранить его свойства.
Реакция на перехваченные ошибки
Теперь, когда мы знаем, как перехватывать ошибки, остается выяснить, что делать при их возникновении? Вообще говоря, это зависит от типа ошибки, от того, какие иные обработчики ошибок имеются в распоряжении, а также от того, каким образом вы хотите справиться с ошибкой. В данном разделе рассмотрены возможные варианты.
Определение типа ошибки
В большинстве случаев главное, что следует знать для адекватной реакции на ошибку, — это ее тип. VBA предоставляет эту информацию в объекте Err. Объект Err имеет несколько свойств (их список приведен в табл. 5.1), которые содержат необходимые сведения. Наиболее важными свойствами являются Number (возвращает индивидуальный код ошибки) и Description (содержит информационное сообщение, которое обычно отображается VBA).
Таблица 5.1. Свойства объекта VBA Err
СвойствоОписание
Number
Индивидуальный код ошибки
Description
Информационное сообщение, связанное с ошибкой
Source
Объект или приложение, вызвавшее ошибку
HelpFile
Файл справки Windows с дополнительной информацией об ошибке
HelpContext
Контекстный идентификатор темы справки
LastDLLError
Код ошибки, возвращенный последней выполненной функцией DLL
Где-то внутри обработчика ошибок (фрагмент программы между меткой ошибки и концом процедуры) вы должны сравнить значение Err .Number с одним или несколькими ожидаемыми значениями. В зависимости от результата можно предпринять определенные действия либо для решения проблемы, либо для изящного завершения процедуры.
В примере 5.4 представлена процедура CopyToFloppy, которая пытается скопировать заданный файл на гибкий диск после удаления с диска существующей версии файла. При выполнении этой процедуры может произойти ряд ошибок. Например, иногда отсутствует исходный файл или пользователь забывает вставить гибкий диск в дисковод. Как процедура справляется с ошибкой, зависит от типа ошибки.
Пример 5.4. Проверка свойства Number объекта Err
Sub CopyToFloppy(strFile As String) Dim strDest As String
* Оператор перехвата ошибок. On Error GoTo HandleError
' Формирование пути к файлу назначения.
strDest = ”A:\" & TrimDirectory(strFile)
' Удаление файла с гибкого диска. Kill strDest
* Попытка скопировать файл.
FileCopy strFile, strDest
ExitHere:
Exit Sub
HandleError:
Select Case Err.Number Case Else
MsgBox Err.Description, vbExclamation, _ "Error " & Err.Number & " in CopyToFloppy" End Select
End Sub
Заметьте, что процедура CopyToFloppy использует в обработчике ошибок для определения кода ошибки оператор Select Case. В данном примере обработка ошибок сводится к отображению диалогового окна с информацией об ошибке. В следующих разделах рассматриваются другие способы реакции на ошибки.
Даже такой простой обработчик ошибок предоставляет пользователю полезную информацию. Оператор MsgBox отображает в диалоговом окне стандартный текст сообщения об ошибке (используя свойство Description объекта Err), код ошибки, а также имя процедуры, в которой произошла ошибка. На рис. 5.1 представлено сообщение об ошибке, появляющееся при попытке выполнения этой процедуры, когда гибкий диск в дисководе отсутствует.
Рис. 5.1. Стандартная информация об ошибке, отображаемая обработчиком ошибок
Несмотря на то, что вы можете предпринимать какие-либо действия в ответ на определенные ошибки (как это делается, мы объясним позже), рекомендуется всегда включать оператор MsgBox в оператор Case Else, как в примере 5.4. Данное сообщение информирует пользователя об ошибочной ситуации. При наличии такой информации (код ошибки, ее описание и имя процедуры) вам или пользователю вашей программы будет легче определить источник проблемы.
Оператор Resume
В нашем примере процедура CopyToFloppy завершается сразу после отображения диалогового окна. Несмотря на свою информативность, такой подход далек от оптимального. Было бы намного лучше исправить ошибку, если это возможно.
Должен существовать способ указать VBA повторить операцию, а не просто завершить процедуру. Оператор Resume предназначен как раз для этого. В общем случае, после того как ошибка произошла, данный оператор передает управление в определенную точку процедуры. Оператор Resume имеет три формы:
Resume;
Resume Next;
Resume Label.
Resume
Применение оператора Resume самого по себе возвращает управление оператору, который вызвал ошибку. Используйте оператор Resume в том случае, когда ваш обработчик ошибок устранил проблему, которая привела к ошибке, и вы хотите продолжить выполнение программы с того места, где проблема обнаружена. В нашем примере пользователь может исправить ошибку 71 — Диск не готов, — вставив диск в дисковод. Затем VBA может повторить оператор Filecopy.
В примере 5.5 приведена усовершенствованная версия процедуры CopyToFloppy. В данном случае обработчик ошибок специально отслеживает ошибку 71, используя оператор Case.
Пример 5.5. Использование оператора Case для проверки ошибки с кодом 71
Sub CopyToFloppy(strFile As String)
Dim strDest As String
* Оператор перехвата ошибок.
On Error GoTo HandleError
' Формирование пути файла назначения.
strDest = ”А:\” & TrimDirectory(strFile)
' Удаление файла с гибкого диска.
Kill strDest
’ Попытка скопировать файл.
FileCopy strFile, strDest
ExitHere:
Exit Sub
HandleError:
Select Case Err.Number
Case 71 ' Диск не готов
If MsgBox("Please place a floppy disk in the " & _ "drive.", vbExclamation + vbOKCancel, _ "Load Diskette") = vbOK Then
Resume
Case Else
MsgBox Err.Description, vbExclamation, _ "Error " & Err.Number & " in CopyToFloppy" End Select
Resume ExitHere
End Sub
Если обработчик ошибок обнаружит ошибку с кодом 71, он отобразит диалоговое окно с просьбой поместить диск в дисковод. Диалоговое окно на рис. 5.2 имеет кнопки ОК и Отмена (Cancel). Когда пользователь щелкнет на кнопке ОК, в обработчике ошибок выполнится оператор Resume, в результате чего VBA попытается выполнить оператор Filecopy еще раз. Однако следует заметить, что, если проблема все еще не решена, этот оператор второй раз не выполнится и возникнет бесконечный цикл. Поэтому в диалоговом окне имеется кнопка Отмена. После щелчка на ней мышью выполнение процедуры прекращается.
Рис. 5.2. Пользовательское сообщение об ошибке определенного типа
Resume Next
Если вы не можете исправить ошибочную ситуацию, но ошибка не является критической и не влияет на результат выполнения процедуры, можно воспользоваться оператором Resume Next. Он заставляет VBA выполнить строку программы, которая следует после строки с ошибкой. Однако процедура CopyToFile — очень проста и не содержит других операторов.
Resume Label
Оператор Resume Label используется в тех случаях, когда вы хотите передать управление оператору, находящемуся в некоторой строке, но не в той, где произошла ошибка, и не в следующей за ней. Оператор Resume Label подобен оператору GoTo, однако его можно использовать только внутри обработчика ошибок. В примере 5.5 показано, как применять оператор Resume для перехода на метку ExitHere. Такой выход из процедуры после ошибки выполнения предпочтительнее, потому что в этом случае имеется только единственная точка выхода.
Применение стандартного обработчика ошибок
Все представленные до сих пор версии процедуры CopyToFloppy содержали оператор MsgBox, сообщающий пользователю обо всех ошибках выполнения, которые не обрабатываются процедурой особо. Такой (или подобный) код следует включать в каждую независимую процедуру приложения. (Исключения из этого правила приведены в разделе "Иерархия обработки ошибок в VBA".) Одним из возможных способов реализации данного требования является создание и применение стандартной процедуры обработки ошибок.
Зачем нужна стандартная процедура?
Использование стандартной процедуры для обработки ошибок имеет два преимущества. Во-первых, это обеспечивает единую процедуру для выполнения таких операций, как отображение сообщений об ошибках. При необходимости внести изменения в алгоритм обработки ошибок нужно исправить только одну процедуру. Во-вторых, общая процедура упрощает написание обработчика ошибок. В эту процедуру можно передавать информационные сообщения для пользователя.
Пример процедуры обработки ошибок
В примере 5.6 представлена процедура обработки ошибок, которую мы написали специально для этой книги. Процедура dhError предоставляет информацию об ошибке и отображает ее в диалоговом окне. Она принимает три аргумента: указатель на объект VBA ErrObject, имя программы, вызвавшей процедуру обработки ошибок, и флаг типа Boolean. Флаг определяет, сколько кнопок будет содержать диалоговое окно: две (ОК и Отмена) или только кнопку ОК. Обратите внимание, что все аргументы являются необязательными.
Пример 5.6. Универсальная процедура обработки ошибок dhError
Function dhError( _
Optional strProc As String = "<unknown>", _
Optional fRespond As Boolean = False, _
Optional objErr As ErrObject) _
As Boolean
Dim strMessage As String
Dim strTitle As String
Dim intStyle As Integer
' Если пользователь не задал аргумент ErrObject,
' используется объект Err.
If objErr Is Nothing Then
Set objErr = Err
End If
Если произошла ошибка, она обрабатывается;
в противном случае возвращается значение True.
If objErr.Number = 0 Then
dhError = True
Else
' Формирование заголовка и сообщения.
strTitle = "Ошибка " & objErr.Number & _
" в " & StrProc
strMessage = "Произошла следующая ошибка:" & _ vbCrLf & vbCrLf & objErr.Description
' Задание пиктограммы и кнопок для оператора MsgBox. intStyle = vbExclamation If fRespond Then
intStyle = intStyle Or vbOKCancel
End If
' Отображение сообщения и возврат результата. dhError = (MsgBox(strMessage, _ intStyle, strTitle) = vbOK) End If
End Function
Все аргументы являются необязательными, поэтому данную функцию можно вызывать, указав только ее имя. Однако функции dhError следует задать как минимум имя процедуры, в которой произошла ошибка. Эта информация появляется в заголовке диалогового окна и может помочь пользователю исследовать проблему.
Если в качестве значения аргумента fRespond задать True, функция dhError отображает диалоговое окно с кнопками ОК и Отмена. После щелчка на кнопке ОК функция dhError возвратит значение True, а после щелчка на кнопке Отмена — значение False. Диалоговое окно такого типа используется для того, чтобы пользователь имел возможность отменить текущую операцию.
Функция dhError применена в примере 5.7. Процедура ForceError создает ошибочную ситуацию, пытаясь разделить число на 0. При возникновении ошибки обработчик ошибок процедуры ForceError вызывает функцию dhError в условном операторе. На рис. 5.3 представлено диалоговое окно, сообщающее об ошибке. Если пользователь щелкнет на кнопке Отмена, выполнение программы завершается оператором stop процедуры ForceError. Очевидно, в такой ситуации вам понадобится также выполнить дополнительные операции (выход из процедуры, возврат в основное окно вашего приложения и т.д.). В данном случае процедура ForceError всего лишь показывает, как использовать функцию dhError.
Ошибка 11 в ForceError
Произошла^ледующая ошибка:
Деление на ноль' , ;,
: * 'ок *м Д.
Рис. 5.3. Диалоговое окно с сообщением об ошибке, которое сформировано функцией dhError, вызванной из обработчика ошибок процедуры ForceError
Пример 5.7. Процедура ForceError моделирования ошибки выполнения, которая обрабатывается функцией dhError
Sub ForceError()
On Error GoTo HandleError
Debug.Print 1/0
ExitHere:
Exit Sub
HandleError:
If Not dhError("ForceError", True) Then
Stop
End I f
Resume ExitHere
End Sub
Упрощение обработчика ошибок
В течение многих лет нам приходилось сталкиваться с большим количеством процедур обработки ошибок, многие из которых были довольно сложны и предусматривали различные варианты сообщений, реакции пользователя, а также сопутствующие операции, например запись информации об ошибках в файл. Однако, исходя из нашего опыта, можно сказать, что, чем проще обработчик ошибок, тем лучше. Почему? Просто потому, что крайне редко вам нужно сделать что-то еще, кроме отображения сообщения для пользователя. Возможно, с ситуациями, требующими сложной обработки ошибок, вы столкнетесь в ходе тестирования вашего приложения. Если это необходимо, добавьте нужные операции в текст каждой процедуры. Попытка учесть все возможные ситуации и их последствия в одной процедуре нетолько неэффективна, но и вряд ли реальна.
Иерархия обработки ошибок в VBA
Для того чтобы эффективно справляться с ошибками выполнения, важно понять, как и когда VBA решает их сгенерировать. Обработка ошибок осуществляется иерархически, так что когда ошибка возникает в процедуре, которая не содержит обработчика ошибок, VBA в его поисках просматривает друг за другом все процедуры в цепочке вызовов.
На рис. 5.4 изображено четыре процедуры А, В, С и D. Процедура А вызывает процедуру В, которая, в свою очередь, вызывает процедуру С и т.д. Процедура А, в отличие от других процедур, содержит обработчик ошибок.
Процедура А ^•['ЗГкЧЧИКОПроцедура А ^•['ЗГкЧЧИКО44Ошибка обрабатывается обработчиком ошибок, который содержится в процедуре АОшибка обрабатывается обработчиком ошибок, который содержится в процедуре АПроцедура В■сз обработчикаошибок)Процедура В■сз обработчикаошибок)1 Цепочка вызовов начинается с процедуры А и охватывает процедуры В, С и D1 Цепочка вызовов начинается с процедуры А и охватывает процедуры В, С и D33Процедура С■><без обработчикаошибок)Процедура С■><без обработчикаошибок)VBA просматривает цепочку вызовов, пока не обнаружит обработчик ошибокVBA просматривает цепочку вызовов, пока не обнаружит обработчик ошибокПроцедура D■> (без обработчика :ошибок)Процедура D■> (без обработчика :ошибок)
Процедура А ^•['ЗГкЧЧИКО
Процедура А ^•['ЗГкЧЧИКО
4
4
Ошибка обрабатывается обработчиком ошибок, который содержится в процедуре А
Ошибка обрабатывается обработчиком ошибок, который содержится в процедуре А
Процедура В
■сз обработчикаошибок)
Процедура В
■сз обработчикаошибок)
1 Цепочка вызовов начинается с процедуры А и охватывает процедуры В, С и D
1 Цепочка вызовов начинается с процедуры А и охватывает процедуры В, С и D
3
3
Процедура С
■><без обработчикаошибок)
Процедура С
■><без обработчикаошибок)
VBA просматривает цепочку вызовов, пока не обнаружит обработчик ошибок
VBA просматривает цепочку вызовов, пока не обнаружит обработчик ошибок
Процедура D
■> (без обработчика :ошибок)
Процедура D
■> (без обработчика :ошибок)
2 Ошибка происходит
" z в процедуре D, которая не содержит обработчика ошибок
Рис. 5.4. Обработка ошибки, которая возникла во вложенной процедуре, не содержащей обработчика ошибок
Когда в процедуре D происходит ошибка, VBA осуществляет поиск обработчика ошибок в обратном направлении, вверх по цепочке вызовов, пока не обнаружит его в процедуре А. Затем обработчик ошибок процедуры А обрабатывает ошибку. Если ни одна из процедур в цепочке вызовов не содержит обработчика ошибок, VBA отображает стандартное сообщение об ошибке и останавливает выполнение программы.
Такую иерархическую структуру вы можете выгодно использовать. Она избавляет от необходимости включать обработчик ошибок в каждую процедуру, если он содержится хотя бы в одной из процедур, расположенных выше в цепочке вызовов.
Однако обработка ошибок выполнения в соответствии с иерархией имеет два недостатка. Во-первых, нет возможности указать, в какой из процедур более низкого уровня произошла ошибка. Во-вторых, ошибки некоторых типов бессмысленно обрабатывать в процедуре более высокого уровня. Другими словами, для эффективной обработки ошибки это может быть слишком поздно. Если есть возможность исправить ошибку, перехватив ее в процедуре более низкого уровня, добавьте обработчик ошибок туда.
Совет: Функции-утилиты (т.е. функции, выполняющие операции со строками и математические вычисления) обычно не требуют обработки ошибок, поскольку они практически всегда вызываются из других процедур. В этом случае обработка ошибок будет излишней. Избегайте включения обработчика ошибок в функции-утилиты, за исключением тех случаев, когда вы хотите справиться с определенными ошибками.
Генерация ошибок
Если вы разрабатываете программы на VBA, придет время, когда вам понадобится генерировать ошибки самостоятельно. "Зачем?" — спросите вы. Разве устранение ошибок не является целью? Конечно, является, однако в действительности в определенных ситуациях имеют место ошибки пользователей. Генерация ошибки даст возможность сообщить процедуре, что что-то не в порядке. При этом процедура может справиться с такой ситуацией или передать ошибку процедурам более высоких уровней (в соответствии с иерархией обработки ошибок, описанной в предыдущем разделе).
Предположим, вы пишете функцию, вычисляющую некоторое значение по двум заданным значениям. Вы хотите обеспечить, чтобы одно из исходных значений представляло собой дробь из диапазона от 0 до 1. После логической проверки (например, с помощью оператора if. . .Then) и определения, что исходное значение выходит за пределы допустимого диапазона, у вас есть несколько вариантов:
прервать выполнение функции и возвратить Null или 0;
отобразить сообщение об ошибке и возвратить Null или 0;
возвратить код ошибки;
запросить правильное исходное значение.
Все эти возможности имеют свои недостатки, обусловленные тем, что функция пытается устранить ошибку самостоятельно. Другим, причем лучшим, вариантом является генерация ошибки выполнения и последующая обработка се вызывающей процедурой. Это можно реализовать при помощи метода Raise объекта VBA Err. Процедура в примере 5.8 иллюстрирует данный подход. Обратите внимание, что процедура сама по себе не имеет обработчика ошибок.
Пример 5.8. Применение метода Raise для генерации ошибки выполнения
Function ComputeValue(dblAmount As Double, _ sngRate As Single) As Double
Демонстрация использования метода Raise.
If sngRate < 0 Or sngRate > 1 Then Err.Raise 12345, "ComputeValue", _ "Аргумент должен быть между 0 и 1. "
End If
ComputeValue = dblAmount * sngRate
End Function
Метод Raise принимает три аргумента:
код ошибки, который должен быть уникальным среди всех возможных кодов ошибок (используемых VBA и базовым приложением);
исходную строку, которая позволяет определить источник ошибки (например, имя приложения или процедуры);
сообщение об ошибке, которое должно содержать краткое описание проблемы.
Метод Raise сгенерирует ошибку и прервет выполнение процедуры. Когда расположенный на более высоком уровне обработчик ошибок активизируется, он может извлечь информацию об ошибке из свойств объекта Err.
Поскольку вызов функции производится из различных процедур, нецелесообразно применять единую стратегию обработки ошибок во всех ситуациях. Преимущество метода Raise заключается в том, что он позволяет процедуре более высокого уровня корректно обработать ошибки.
I W Совет: Вообще говоря, для возвращения информации об ошибке пользователя функциям-утилитам следует использовать метод Raise.
Процесс отладки
Обработка ошибок выполнения — это только часть работы программиста. При возникновении ошибки нужно принимать решение о том, что с ней делать. Позволить ли обработчику ошибок отобразить сообщение, а затем вернуть управление пользователю? Или попытаться скорректировать алгоритм программы таким образом, чтобы ошибка больше не происходила? А что делать с программой, которая не генерирует ошибок, но и не работает, как требуется? Решить эти проблемы можно в процессе отладки.
Отладка приложения подобна хирургическому обследованию пациента. В обоих случаях целью является идентифицировать и, в конечном счете, устранить имеющиеся проблемы или аномалии. Для полного исследования проблемы и ее решения приходится пользоваться как подручными средствами, так и собственной интуицией и опытом. Ниже мы рассматриваем инструментарий, предоставляемый VBA для отладки приложений, а также делимся некоторым опытом решения подобных проблем.
Средства VBA для отладки программ
Любую среду разработки можно оценить по степени совершенства ее средств отладки. Среда отладки VBA совершенствовалась от версии к версии и в настоящее время предоставляет пользователю ряд полезных инструментов, которые помогают вылавливать "жучки" и логические ошибки. Эти инструменты включают:
окно отладки Проверка;
точки останова и режим пошагового выполнения;
стек вызова;
контрольные выражения;
контрольные значения;
контекстные подсказки по данным;
окно локальных переменных;
параметры отладки в диалоговом окне Параметры.
Окно Проверка
Окно Проверка (рис. 5.5) предоставляет возможность анализировать работу VBA-кода без использования макросов, форм или других методов запуска программы. Данное окно можно сравнить с командной строкой VBA и применять для запуска процедур и вычисления выражений. Фактически в окне Проверка выполняются почти все операторы, что и в процедурах VBA. Это окно можно открыть, щелкнув на кнопке Окно отладки панели инструментов VBA, выбрав команду Окно отладки в меню Вид или нажав комбинацию клавиш [Ctrl+G].
Рис. 5.5. Вызов процедуры CopyToFloppy в окне Проверка
В окне Проверка отображаются последние 200 введенных или полученных в качестве результата строк. По мере добавления новых строк в конец списка старые строки исчезают из его начала. Воспользовавшись полосой прокрутки, можно поместить курсор в нужную строку и нажать клавишу [Enter]. При этом VBA пересчитает выражение и отобразит результат. Если вы хотите удалить из окна Проверка все строки выше текущего положения курсора, их нужно выделить при помощи комбинации клавиш [Ctrl+Shift+Home], а затем нажать клавишу [Del]. Для выделения строк, расположенных ниже текущего положения курсора, воспользуйтесь комбинацией клавиш [ Ctrl+Shift+End].
Точки останова
Точки останова позволяют установить позиции временной приостановки выполнения программы VBA. На рис. 5.6 показано окно с программой, выполнение которой прервано в точке останова. Обратите внимание на подсвеченную строку программы. Это строка, которую программа собирается выполнить.
ИВЕИВЕ'; Normal - basErrorRoutines (Программа)
ИВЕ
ИВЕ
|(Общая область)|(Общая область)^| jdhError
|(Общая область)
|(Общая область)
Call dhError("ThisProc", True)
Dim strMessage As String
Dim strTitle As String Dim intstyle As Integer
% ’ If the user didn’t pass an ErrObject, use Err
Set objErr = Err
' ' End If
'’ If there is an error, process it
'’ otherwise just return True
If objErr.Number = 0 Then
s l Is I? S ' ' ' 's' S's'j:<' is" SVl^SS Ss^^S,^ Vs.
Pac. 5.6. Программа, приостановленная в точке останова
Стек вызова
В момент приостановки программы в точке останова VBA дает возможность отобразить стек вызова. В стеке вызова приводится список всех активных процедур, причем текущая процедура находится в начале списка, процедура, которая ее вызвала, идет в списке второй и т.д. На рис. 5.7 представлен пример стека вызова в точке останова программы.
Рис. 5.7. Диалоговое окно VBA Стек вызова отображает последовательность вызовов процедурРис. 5.7. Диалоговое окно VBA Стек вызова отображает последовательность вызовов процедур
Рис. 5.7. Диалоговое окно VBA Стек вызова отображает последовательность вызовов процедур
Рис. 5.7. Диалоговое окно VBA Стек вызова отображает последовательность вызовов процедур
Примечание: Иногда в диалоговом окне Стек вызова появляется строка "<Non-Basic Code>” (Программа не на языке Basic). Это сообщение говорит о том, что в цепочку вызовов вовлечен другой процесс, например функция базового приложения VBA или DLL.
Чтобы просмотреть стек вызова, выберите команду Стек вызова (Call Stack) из меню Вид или нажмите комбинацию клавиш [Ctrl+L]. Открыв диалоговое окно Стек вызова, можно перейти к любой процедуре в стеке путем выбора ее из списка и щелчка на кнопке Показать (Show).
Контрольные выражения
Контрольные выражения используются для отслеживания своих значений по мере выполнения программы. В VBA реализован полный набор средств контроля. Вы можете просматривать контрольные выражения интерактивно или добавить их в постоянный список.
Реакция VBA на контрольные выражения может быть различна (она зависит от типа контрольного значения):
отображение значения контрольного выражения;
останов, если значение выражения истинно;
останов при изменении значения выражения.
Контрольные выражения отображаются в отдельном окне (рис. 5.8). Каждый раз, когда VBA находится в режиме останова, он вычисляет значения всех контрольных выражений и отображает их в этом окне. Работать с таким окном намного удобнее, чем выводить значения переменных в окно отладки после выполнения каждой строки программы в пошаговом режиме.
Рис. 5.8. В окне Контрольные значения (Watches) отображаются значения всех заданных контрольных выражений
Типы контрольных значений Останов, если значение выражения истинно (Break When Expression Is True) и Останов при изменении значения выражения (Break When Expression Has Changed) позволяют задать логическое условие или выражение. Когда заданное условие истинно или изменяется, VBA немедленно приостанавливает выполнение программы и переходит в режим останова. Контрольные значения этих типов полезны для определения, когда и как происходят изменения значений переменных. Например, вы знаете, что где-то в вашей программе глобальной переменной
gintvalue ошибочно присваивается значение 0, но не знаете, где и как это происходит. Вы можете установить контрольное значение Останов, если значение выражения истинно с выражением gintvalue = 0. При этом, как только значение переменной gintvalue становится равным 0, VBA переходит в режим останова.
Контрольные значения
Диалоговое окно Контрольное значение (Quick Watch) применяется для быстрого просмотра значения переменной. Чтобы использовать это диалоговое окно, поместите курсор на имя переменной и нажмите комбинацию клавиш [Shift+F9]. Появляется диалоговое окно (рис. 5.9), в котором отображается текущее значение этой переменной. Можно также выбрать выражение, и VBA вычислит его значение. После щелчка на кнопке Добавить (Add) данное выражение добавляется в список постоянных контрольных выражений.
Контрольное значение
||||||il|liiH^^
Project, Т h isDocu men t. Ex a mp Ie
Выражение- --
strMessage:
|l||ll||||||||i■|^^
j|||l|i||||iBi|i||i||S
|t|||i|||||i^^
Puc. 5.9. Окно VBA Контрольное значение (Quick Watch) отображает текущее значение выражения
Контекстные подсказки по данным
Контекстные подсказки по данным (Data Tips) работают аналогично контекстным подсказкам (ToolTips) в окнах модуля. Чтобы воспользоваться подсказкой в тот момент, когда программа находится в режиме останова, наведите указатель мыши на любую переменную или свойство. VBA отобразит маленький прямоугольник с подсказкой, в которой содержится значение выражения. Это даже быстрее, чем использовать контрольное значение!
Окно локальных переменных
Еще одним очень полезным инструментом является окно локальных переменных. Когда программа находится в режиме останова, это окно (рис. 5.10) содержит список всех переменных, находящихся в данный момент в области видимости. Изменяя ширину полей окна, можно просмотреть свойства переменных. Фактически данное окно создает контрольные выражения для всех переменных вашей программы.
И Локальные переменные
„]п| х
рТр^^
I Выражение
||—S] TablesOfFigures
|3й^^
ТаЫезОАчдигезЛ ablesOfFigures
— TrackRevisions
- Type
— UpdateStylesOnOpen
— UserControl
-Щ Variables
-fl VBProject
-Щ Application
-Q Collection
— Description
— HelpContextID
— HelpFile
— Mode
— Name
-Q Parent
Ложь wdTypeDocument Ложь Истина
О
vbext_vm_Break "Project"
Boolean
WdDocurnentType
Boolean
Boolean
Variables/Variables
VB P roj e ct/VB P roj e ct
Application/VBE
VB P roj e ct s/VB P г о j e ct s String
Long
String vbext_VBAMode String Application/VBE
Puc. 5.10. В окне Локальные переменные отображаются значения всех переменных, находящихся в области видимости
Окно Параметры
Два элемента диалогового окна Параметры позволяют настроить средства VBA для обработки ошибок и отладки. Первый представляет собой текстовое поле для констант условной компиляции. Константы, управляющие операторами #if...#Then, можно включить в текст вашей программы или ввести в диалоговом окне Параметры. При вводе нескольких констант их нужно разделять двоеточиями.
Второй параметр управляет процессом перехвата ошибок, когда VBA переходит в режим останова в ответ на ошибку выполнения. Можно выбрать одно из трех значений этого параметра:
Останов при любой ошибке
Заставляет VBA входить в режим останова в ответ на любую ошибку выполнения, даже если в программе предусмотрен обработчик ошибок. Обычно данная установка используется только на этапе отладки программы, а не в готовом программном продукте.
Останов в модулях класса
Заставляет VBA входить в режим останова при всех ошибках в глобальных модулях и модулях класса, для которых не определен перехват ошибок (установка по умолчанию).
Останов на необрабатываемых ошибках
Заставляет VBA входить в режим останова только в глобальных модулях при всех ошибках, для которых не определен перехват ошибок. Если ошибка произошла в модуле класса, VBA входит в режим останова на операторе глобального модуля, который вызвал свойство или метод класса.
а
Примечание: К сожалению, в приложениях Microsoft Office, поддерживающих VBA, параметры обработки ошибок и отладки расположены в различных местах. Например, в Microsoft Access они находятся на вкладке Другие (Advanced) диалогового окна Параметры (Options). В VBA IDE, который поставляется с Microsoft Excel, Power Point и Word, их можно найти в диалоговом окне Параметры VBA и диалоговом окне установки свойств текущего проекта. Чтобы отыскать эти параметры, воспользуйтесь документацией своего базового приложения.
Применение окна отладки
Окно отладки можно использовать для интерактивного тестирования различных частей приложения. Из этого окна выполняется запуск процедур, просмотр и изменение значений переменных и вычисление значений выражений. Можно написать программу на VBA для вывода информации в окне отладки без вашего вмешательства.
Запуск программы из окна отладки
Окно отладки позволяет легко запустить любую функцию или процедуру, которая находится в области видимости. Чтобы запустить процедуру, нужно просто ввести се имя (наряду со значениями параметров) в строке окна отладки и нажать клавишу [Enter]. VBA выполняет процедуру, а затем возвращает управление окну отладки. Этот подход годится как для функций, так и для процедур. С другой стороны, если вы хотите, чтобы при выполнении функции VBA возвращал се результат в окно отладки, необходимо воспользоваться оператором Print. Например, если в окно отладки ввести следующее выражение, VBA запустит функцию MyFunction и выведет возвращаемое значение:
Print MyFunction ()
В VBA, как и в Basic большинства предыдущих версий, разрешается применять сокращенное обозначение метода Print. Вместо слова Print в окне отладки допускается использование символа ? . Это сокращение вы увидите во всех приведеннььх ниже примерах. Итак, предыдущий оператор можно записать следующим образом:
?MyFunction()
Правила области видимости работают, как и в обычной программе. Таким образом, переменные доступны из окна отладки только в том случае, если они в данный момент находятся в области видимости. Открыв окно Локальные переменные, можно определить, что является текущей областью видимости, взглянув на текстовое поле в верхней части окна. В этом поле отображается текущая область видимости переменных.
Работа с выражениями в окне отладки
Окно отладки можно использовать для вычисления выражений, будь то присвоение значений переменным или выполнение сложных расчетов. Введите в окно отладки, например, следующий оператор, и вы сможете просмотреть текущее значение переменной intMyVar:
?intMyVar
Конечно, перед этим переменная должна быть описана в вашей программе и доступна в текущий момент. В окне отладки нельзя использовать оператор Dim (или аналогичные операторы, включая ReDim, Global и Const).
В окне отладки можно не только просматривать результаты вычисления значений выражений, но и изменять значения переменных. Чтобы задать для переменной intMyVar новое значение, воспользуйтесь, например, следующим выражением:
intMyVar = 97
Программа, выполняемая после этого изменения, использует новое значение переменной.
Любой оператор, который вводится для непосредственного выполнения в окне отладки, должен состоять из одной строки. Нельзя ввести оператор, занимающий несколько строк, например If...Then...Else, хотя можно выполнить оператор If...Then, который помещается в одну строку. Чтобы обойти это ограничение, несколько операторов VBA в одной строке следует разделять двоеточием (:).
Например, следующий фрагмент программы выполняет цикл:
For intCount = 0 То 10:Debug.Print intCount:Next intCount
Вывод информации в окне отладки
Окно отладки можно использовать для наблюдения за выполнением процедуры путем вывода в нем сообщений или значений выражений по мере выполнения программы. Для отображения значения любого выражения в процессе работы программы применяется метод Print объекта Debug. В предыдущем примере с помощью метода Print в окно отладки выводится значение переменной intCount.
Вставив операторы Debug. Print в стратегически важных точках своей программы, вы существенно упростите процесс ее отладки. Вот несколько рекомендаций по использованию операторов Debug. Print:
в начале или в конце процедуры;
в процессе итерации по объектам или значениям в семействе или в цикле;
после выполнения сложных расчетов;
после ввода значений пользователем;
перед удалением файлов или других объектов.
Увы, метод Print приносит пользу только при разработке приложения. Вы, скорее всего, не захотите, чтобы пользователи просматривали окно отладки. При желании наблюдать за работой приложения после того, как оно поступит в распоряжение пользователей, рассмотрите возможность использования класса Log, который описан ниже в этой главе в разделе "Протоколирование работы приложения".
Что делать с ненужными операторами Debug.Print?
Вы можете совершенно безболезненно оставить в своей программе операторы Debug. Print. Пока у пользователя окно отладки нс отображено, выполнение этих строк заметно нс будет, оно лишь слегка снизит быстродействие программы. Однако если быстродействие приложения играет существенную роль, можно окружить отладочный код операторами условной компиляции. Например:
* В разделе описаний:
#Const fDebug = True
' В некоторой процедуре:
#If fDebug Then
Debug.Print "Some output"
#End If
Использование точек останова
Точки останова в программе — это своего рода временные контрольно-пропускные пункты. Задав точку останова, вы сообщаете VBA о необходимости приостановить выполнение программы на определенной строке, но сохранить состояние системы в памяти. Это означает, что все переменные, находящиеся в данный момент в области видимости, могут быть просмотрены в окне отладки. Кроме того, можно выполнить программу пошагово: Шаг с заходом (Step Into) и Шаг с обходом (Step Over) (используя элементы меню, кнопки панели инструментов или клавиши [F8] либо [Shift+F8]), что дает возможность наблюдать за выполнением каждого оператора программы.
Чтобы задать точку останова в определенной строке программы, поместите курсор в любое место этой строки и выполните одну из следующих операций: щелкните на кнопке Точка останова (Breakpoint) панели инструментов, выберите команду Точка останова (Toggle Breakpoint) из меню Отладка (Debug) или нажмите клавишу [F9]. Кроме того, точку останова можно создать, щелкнув мышью на области в левой части окна модуля. При этом соответствующая строка выделяется цветом (цвет выделения можно установить в диалоговом окне Параметры). Когда VBA достигает точки останова по ходу выполнения программы, он переключается в окно модуля, в котором отображается точка останова и подсвечивается текущий оператор. Кроме того, на поле с левой стороны окна напротив соответствующей строки появляется маленькая стрелка. VBA приостанавливает работу программы до выполнения данного оператора. Это позволяет проверить или установить значения переменных до того, как VBA выполнит выделенную строку программы.
Совет: При закрытии проекта VBA не сохраняет точки останова вместе с самой программой. Если вы хотите, чтобы точки останова остались, можете воспользоваться оператором stop, который выполняет роль постоянной точки останова. Как и с точками останова, по достижении оператора stop VBA приостанавливает работу программы. Конечно, прежде чем распространять свою программу, следует удалить из нее все операторы stop (или окружить их операторами условной компиляции), так как они будут останавливать программу, независимо от того, в каком базовом приложении она запускается.
Сбросить точку останова вручную можно с помощью команды Точка останова из меню Отладка, нажатием клавиши [F9] или кнопки Точка останова панели инструментов. Сбросить сразу все установленные точки останова можно командой Снять все точки останова (Clear All Breakpoints) из меню Отладка
Пошаговый режим
Когда VBA останавливает выполнение программы в точке останова, вы должны принять решение, что делать дальше. Можно продолжить выполнение программы, щелкнув на кнопке Запуск (Run) панели инструментов, выбрав команду Продолжить (Continue) из меню Запуск или нажав клавишу [F5]. Кроме того, можно воспользоваться пошаговым режимом для выполнения операторов программы по одному. Чтобы выполнить текущий оператор, нажмите клавишу [F8]. После выполнения текущей строки программы VBA переходит в режим останова на следующей строке.
Когда VBA при выполнении программы в пошаговом режиме сталкивается с одной из ваших процедур, он может продолжить работу тремя способами. Это зависит от того, какой из команд вы воспользуетесь:
Шаг с заходом (Step In) ([F8])
VBA входит в процедуру и выполняет ее по одной строке.
Шаг с обходом (Step Over) ([Shift+F8])
Выполняет программу по одной строке, но только в текущей процедуре. Данная команда нужна в том случае, если в тексте отлаживаемой программы содержится вызов уже отлаженной процедуры. Чтобы не терять времени на пошаговое выполнение работающей процедуры, воспользуйтесь при отладке программы командой Шаг с обходом, в результате процедура будет выполнена за один шаг.
Шаг с выходом (Step Out) ([Ctrl+Shift+F8])
VBA выходит из текущей процедуры. Если она была вызвана из другой процедуры, VBA возвращается в строку, следующую за той, из которой вызывалась текущая процедура. Эта команда нужна тогда, когда вы нечаянно зашли в процедуру, не требующую отладки, и хотите вернуться в вызвавшую ее процедуру.
Наконец, вы можете воспользоваться командой Выполнить до текущей позиции (Run to Cursor) меню Отладка или контекстного меню, чтобы продолжить выполнение программы до заданной строки. Для этого выделите любую строку после текущей и нажмите комбинацию клавиш [Ctrl+F8]. Команда Выполнить до текущей позиции заставляет VBA продолжить работу программы, пока не будет выполнена строка, предшествующая выделенной, а затем снова войти в режим останова.
Другие параметры пошагового режима
Находясь в режиме пошагового выполнения, вы имеете возможность перенести текущую точку выполнения в другое место. Для этого в режиме останова нужно поместить курсор на любой из операторов процедуры и вызвать команду Задать следующую инструкцию (Set Next Statement) из меню Отладка или из контекстного меню. Выполнение процедуры будет продолжено, начиная с этой точки. Кроме того, изменить текущую точку выполнения можно, перетаскивая мышью стрелку внутри области в левой части окна модуля. Однако перейти к другой процедуре таким образом вы не сможете.
I Совет: Если вы запутались в нескольких открытых окнах программы, ко- L V манда Показать следующую инструкцию (Show Next Statement) из меню Отладка возвратит вас к текущему оператору.
Может случиться так, что ваша программа содержит столько ошибок, что в ходе ее пошагового выполнения у вас возникнет желание поднять руки вверх и сдаться. В этом случае для останова программы можно воспользоваться командой Прервать (End) из меню Запуск или кнопкой Прервать на панели инструментов. Несмотря па то, что эта команда останавливает выполнение программы и выводит вас из режима останова, она сохраняет содержимое всех переменных уровня модуля и глобальных переменных. Чтобы сбросить эти значения, нужно выбрать команду Сброс (Reset) из меню Запуск или щелкнуть на кнопке Сброс панели инструментов.
Внимание: Находясь в режиме останова, можно запускать другие процедуры. VBA сохраняет текущее положение точки выполнения для первой процедуры, в то время как выполняются другие процедуры. Это может привести к непредсказуемым результатам, особенно при запуске процедур, которые используют одни и те же данные или переменные. Если вы заметили, что ваша программа ведет себя непредсказуемо, убедитесь в том, что у вас нет невыполненного условия режима останова. Для этого можно воспользоваться командой Показать следующую инструкцию из меню Отладка, просмотреть стек вызова или выполнить команду Прервать или Сброс из меню Запуск.
Программы без ошибок
Практически невозможно написать приложение без ошибок, однако с помощью конкретных приемов программирования можно избежать появления типичных ошибок. При написании программ вы должны использовать эти приемы в определенном порядке, даже если вы пишете функцию только для применения в процессе тестирования или только для себя. Хорошие привычки прививаются с трудом, однако потом они нс забываются. В нескольких последующих разделах описано, как не допустить проникновения ошибок в программу (и как очистить программу от ошибок), придерживаясь таких правил:
устраняйте ошибки по мере их появления;
используйте комментарии;
структурируйте свою программу;
применяйте оператор Option Explicit;
по возможности избегайте использования данных типа Variant;
осторожно пользуйтесь простым оператором Dim;
группируйте операторы Dim;
используйте как можно более узкую область видимости переменных;
применяйте совместимые соглашения о наименовании;
используйте утверждения.
Придерживаясь этих рекомендаций, вы сможете выработать хороший стиль программирования, позволяющий избегать "жучков", а также устранять ошибки, которые все-таки закрались в программу.
Единственное эмпирическое правило, состоит в том, чтобы не тратить свое время и нс стараться делать свою программу "умнее", чем требуется. Иногда просто необходимо пользоваться новейшими, наиболее сложными особенностями языка программирования, однако чаще всего простейший путь является одновременно и
наилучшим. Опираясь на это правило, предлагаем несколько тактических приемов, применение которых при программировании на VBA поможет избежать "жучков".
Устраняйте ошибки по мере их появления
Очень важно, чтобы вы ликвидировали ошибки сразу же после того, как они обнаружены, а не оставляли их до тех пор, пока не будут разработаны другие элементы программы. Спешка на завершающем этапе создания проекта может вынудить вас обходить ошибки, а не устранять их. Чтобы устранять ошибки по мере их появления, требуется надежное и систематическое тестирование программы, которого некоторые программисты стараются избегать. Что бы вы предпочли, написать 500 строк программы или провести 50 испытаний уже существующей программы? Большинство из нас выберет первое, поскольку писать программу интересно, а тестировать скучно. Однако если вы понимаете, что будет совсем невесело, когда ваша программа не будет работать уже после передачи ее пользователям, вы серьезно возьметесь за работу и проведете скучное, но необходимое тестирование программы.
Используйте комментарии
Отладить старую программу труднее, чем новую, поскольку она уже не так свежа в памяти. Старой можно назвать программу, которая написана две недели, два дня или два часа назад! Это зависит от интенсивности вашей работы. Единственный способ освежить в памяти давно написанную программу — вставить в нее комментарии. Писать разумные комментарии — это целое искусство, которое состоит в том, чтобы сообщать пользователю ровно столько информации, сколько нужно для описания выполняемых действий, не загромождая программу и не внося в нее беспорядок.
Комментарии должны разъяснять, для чего предназначен тот или иной фрагмент программы, а не рассказывать, как он был реализован. Это очень важный момент. Следует пояснить общую структуру программы и отметить в ней наиболее сложные места, чтобы они были понятны программистам, которые будут работать с программой впоследствии, и вам самим. Помните, что если вы используете в своей программе согласованный стандарт наименования переменных, их имена сами будут служить мини-комментариями.
Комментарий, который не отражает сути дела, хуже, чем отсутствие комментария вообще. Не приходилось ли вам, прочитав комментарий, затем обнаруживать, что следующий за ним фрагмент программы работает совсем не так, как выходит из комментария? Тут уже вам следует выяснить, что именно неправильно — комментарий или программа. Не забывайте при внесении изменений в программу сразу же корректировать соответствующий комментарий, поскольку впоследствии вы уже не вспомните об этом.
Примечание: Одна из причин, которая вынуждает ограничивать количество комментариев разумными пределами, заключается в том, что комментарии занимают место в памяти, пока загружен ваш VBA проект (если только вы нс используете Visual Basic или другую среду, которая позволяет создавать откомпилированные версии вашего проекта). Некоторые программисты удаляют из готовой программы все комментарии. Если вы тоже поступаете подобным образом, нужно быть уверенным в том, что этот вариант программы является окончательным, и вы не будете больше вносить в нее изменения. В противном случае у вас может оказаться две разные версии вашего проекта.
Структурируйте свою программу
Кроме снабжения программы комментариями, необходимо принимать меры для правильной ее организации. При написании текста программы следует пользоваться отступами. Также целесообразно разбивать большие процедуры на более мелкие.
Делайте отступы таким образом, чтобы операторы, которые должны "идти вместе", находились в тексте программы на одном уровне вложенности, а операторы, которые подчинены другим операторам, отстояли от последних на одну позицию табуляции. Большинство программистов на VBA используют отступы как для выделения традиционных управляющих структур (For. . .Next, If. . .Then. . .Else, Do. . .Loop, For Each), так и для указания уровней активности объектов доступа к данным (BeginTrans/CommitTrans, AddNew/Update и т.д.). Подобный подход используется в этой книге во всех примерах программ.
Модуляризация — причудливое название, обозначающее разбиение программы на ряд сравнительно небольших процедур, а не на несколько громадных. Такой стиль написания программ предпочтительнее по (нескольким причинам:
каждая процедура проста для понимания (простую для понимания программу легче реализовать и защитить от ошибок);
ошибки можно локализовать на уровне небольшого фрагмента полной программы (если переменная используется только в пределах функции, которая занимает всего десять строк, любое сообщение об ошибке, относящееся к этой переменной, скорее всего, генерируется именно здесь);
снижается опасность побочных эффектов, обусловленных слишком широкой областью видимости переменных (если вы используете переменную в самом начале функции, которая занимает 500 строк, а затем — в конце для другого цикла, вы можете забыть ее реинициализировать).
Используйте оператор Option Explicit
В диалоговом окне VBA Параметры имеется флаг Явное описание переменных (Require Variable Declaration). Если его установить, VBA в начало каждого создаваемого модуля вставляет строку Option Explicit. Этот оператор вынуждает описывать все переменные до первого обращения к ним в программе, что предотвращает возникновение в вашей программе некоторых трудных для обнаружения ошибок. Без оператора Option Explicit VBA позволяет использовать в программе любую синтаксически корректную переменную, независимо оттого, была она ранее описана или нет. Это означает, что при первом обращении к любой переменной, которую вы забыли предварительно описать, ей автоматически присваивается тип Variant и значение Empty. Флаг Явное описание переменных сократит время, необходимое для отладки.
Использование оператора Option Explicit — это простой способ избежать ошибок, подобных той, которая возникла в программе из примера 5.9. Ошибки такого типа практически невозможно обнаружить на последующих стадиях разработки, поскольку они глубоко скрыты в программе. (Не огорчайтесь, если вам не удастся сразу же найти ошибку в приведенном фрагменте программы; обнаружить ее довольно сложно.)
Пример 5.9. Найдете ли вы ошибку в этой процедуре?
Function UpdateLog(intSeverity As Integer, _
StrProcedure As String, StrTracking As String) As Integer
Dim dbCurrent As Database
Dim rstUsageeLog As Recordset
Dim intFull As Integer
Dim qryArchive As QueryDef
Const dhcMinSeverity = 1
' He стоит регистрировать действия, не являющиеся
* достаточно серьезными, чтобы о них беспокоиться.
If intSeverity < dhcMinSeverity Then Exit Function
End If
* Добавление новой записи в журнал регистраций.
Set dbCurrent = CurrentDbO
Set rstUsageLog = dbCurrent.OpenRecordset _
("zstblUsageLog", dbOpenDynaset) rstUsageLog.AddNew rstUsageLog![Severity] = intSeverty If Err.Number Then
rstUsageLog![ErrorCode] = Err.Number
rstUsageLog1[ErrorText] = Err.Description
End If
rstUsageLog•[User] = Currentuser() rstUsageLog•[Date] = Now
rstUsageLog.Update End Function
Если вы не нашли ошибку в данной программе, она содержится в строке:
rstUsageLog![Severity] = intSeverty
Синтаксическая ошибка, подобная этой, приведет к тому, что в поле Severity будут записаны одни нули. Поиск такой ошибки может стоить вам нескольких часов работы. Использование оператора Option Explicit позволит избежать появления ошибок такого рода.
Избегайте использования данных типа Variant
Тип данных Variant очень удобен, однако его использование не всегда является лучшим решением. Весьма заманчиво описать все переменные как Variant, чтобы не беспокоиться об их содержимом. Однако разработчики VBA ввели явные типы данных не для того, чтобы осложнить вашу жизнь, а для того, чтобы помочь вам. Если какая-либо переменная всегда будет иметь целое значение, объявляйте ее как Integer. Если впоследствии при попытке присвоить этой переменной неправильное значение вы получите сообщение об ошибке, то оно прямо укажет вам источник ошибки и позволит понять, что произошло. Кроме того, при одних и тех же операциях переменные типа Variant обрабатываются медленнее, чем явно описанные переменные, поскольку при использовании типа Variant необходимо дополнительное время на определение типа данных, которые содержатся в переменной в настоящий момент.
К тому же, данные типа Variant требуют больше памяти, чем данные любого другого типа, и поэтому на перемещение переменных типа Variant в памяти затрачивается больше времени. Последних двух причин уже достаточно для того, чтобы вы пересмотрели свое решение об использовании типа Variant там, где этого можно избежать.
Примечание: В некоторых случаях у вас нет выбора в отношении того, какой тип данных использовать. Если вы вводите переменные, которые в определенных точках программы могут принимать значение Null, вы обязаны объявлять их как Variant. Это единственный тип данных, который может содержать значение Null, и попытка присвоить значение Null переменной другого типа вызовет ошибку выполнения. То же самое относится и к возвращаемым функциями значениям. Если функция должна иметь возможность возвращать Null, тип возвращаемого ею значения должен быть Variant.
Используйте ключевое слово ByVai с осторожностью
Будьте осторожны при передаче информации процедурам, параметры которых описаны с использованием ключевого слова ByVai. Хотя применение данного ключевого слова предотвращает изменение процедурой переданных ей переменных (так как ByVai создает копию значения), при вызове процедуры вы можете потерять информацию. Это связано с тем, что VBA приводит информацию, переданную в процедуру, к типу данных ее параметров. Поэтому, если вы передадите переменную типа Single параметру типа Integer, VBA при создании типа Integer отсечет дробную часть значения. Однако вы не получите никакого предупреждения об этом, вследствие чего при использовании ByVai в описании параметров убедитесь в том, что процедуре будет передаваться именно тот тип данных, который она ожидает.
Осторожно пользуйтесь простым оператором Dim
Даже простой оператор Dim может быть источником скрытых ошибок в вашей программе. Рассмотрим такой оператор:
Dim strFirst, strLast As String
Здесь очевидно намерение объявить в одной строке две переменных типа String. В языке С или C++ подобное описание действует именно так. Однако VBA работает по-другому. Оператор As применим только к той переменной, за которой он следует непосредственно, а не ко всем переменным в строке. Результатом рассматриваемого описания будет присвоение переменной strLast типа string, а переменной strFirst типа Variant, причем вести себя они будут по-разному. Например, переменной strFirst присваивается значение Empty, а переменной strLast — строка нулевой длины. В VBA необходимо явно определять тип данных каждой переменной, поэтому выработайте привычку описывать в каждом операторе только одну переменную.
Группируйте операторы Dim
Переменные можно описывать в любом месте процедуры до того, как они фактически используются, и VBA правильно воспримет эти описания. Однако чтобы облегчить последующую отладку программы, приучитесь описывать все переменные в начале процедуры. Это позволяет легко определить, к каким переменным обращается данная процедура, и найти описания в процессе отладки.
Область видимости переменных
Старайтесь использовать как можно более узкую область видимости переменных. Некоторые начинающие программисты узнают, что такое глобальные переменные, и сразу же начинают описывать все свои переменные как глобальные, чтобы полностью исключить проблему области видимости. Порочность такой практики станет понятной, когда вы впервые столкнетесь с двумя процедурами, которые изменяют значение одной и той же глобальной переменной. Если переменная используется только в одной процедуре, опишите ее именно там. Если она используется только процедурами одного модуля, опишите се на уровне этого модуля. Глобальную область видимости следует приберечь только для нескольких переменных, которые действительно присутствуют во многих фрагментах вашей программы.
Применяйте совместимые соглашения о наименовании
В дополнение к приведенным выше соглашениям, которые помогают правильно структурировать программу, рассмотрите возможность принятия в своей программе совместимых соглашений о наименовании объектов и переменных. Мы (и многие другие программисты) стандартизировали свои соглашения о наименовании в соответствии с соглашением о наименовании RVBA.
Совместимый стандарт наименования облегчает поиск ошибок в ваших программах и упрощает их сопровождение для многих программистов. При использовании соглашения о наименованиях RVBA вы получаете информацию о типе переменной и области ее видимости. Эта информация очень помогает в процессе отладки программы.
Ваш друг функция MsgBox
Кроме точек останова для индикации состояния программы можно использовать функцию MsgBox. При таком подходе вы должны принять решение о том, что вы хотели бы контролировать, и вызывать функцию MsgBox для возврата соответствующей информации. Эту методику можно усовершенствовать, написав "оболочку" для функции MsgBox, чтобы сообщения выдавались только при установке определенной константы условной компиляции, свидетельствующей о том, что вы хотите просматривать отладочные сообщения. Соответствующая программа представлена в примере 5.10. Функция MsgBox вызывается только в том случае, если условная константа f Debug имеет значение True.
Пример 5.10. Пример оболочки для функции MsgBox
#Const fDebug = True
Function dhMsgBox(ByVai varMessage As Variant, _
Optional strCaller As String = "<unknown>") As Integer
’ Установка возвращаемого значения в случае,
' если fDebug имеет значение False.
dhMsgBox = True
#If fDebug Then
dhMsgBox = (MsgBox(CStr(varMessage), _ vbOKCancel Or vbQuestion, "Procedure: "& strCaller) _ = vbOK)
#End If
End Function
После щелчка на кнопке ОК функция возвращает значение True, а после щелчка на кнопке Cancel или нажатия клавиши [Esc] — значение False. В последнем случае вы можете предпринять определенные действия, например прервать выполнение программы при помощи оператора stop.
Используйте утверждения
Утверждение — это оператор, который задает определенное логическое условие. Если данное условие нарушается, программа выдает сообщение об ошибке и останавливается. Утверждения полезны в том случае, когда вы хотите проверить правильность данных в процессе выполнения процедуры. Программа в примере 5.11 представляет собой часть функции, которая вычисляет итоговую сумму счета с учетом заданного значения ставки налога (sngTax). Она использует утверждение (вызывая процедуру dhAssert, которая также представлена в примере 5.11) для того, чтобы гарантировать, что заданное значение sngTax находится в диапазоне от 0 до 1.
Пример 5.11. Использование утверждений для проверки данных
Function InvoiceTotal(InglnvoicelD As Long, _
sngTax As Single) As Double
Вызов функции dhAssert для проверки данных.
Call dhAssert((sngTax >= 0) And (sngTax <= 1), _ "Invalid tax rate: ” & sngTax)
* Другие операторы...
End Function
Sub dhAssert(fCondition As Boolean, Optional _ strMessage As String = "Assertion failed.")
' Если условие не выполняется, отображается сообщение об
' ошибке и выполнение процедуры останавливается.
If Not fCondition Then
MsgBox strMessage, vbCritical
Stop
End If
End Sub
Применение утверждения в этом случае целесообразно, поскольку нет иного способа ограничить данные, которые может хранить переменная sngTax. Если условие
истинно, выполнение процедуры продолжается. В противном случае выдается сообщение об ошибке, и VBA останавливает выполнение программы в процедуре dhAssert. В этом случае можно вызвать диалоговое окно Стек вызова, чтобы посмотреть, где произошла ошибка.
Несмотря на то, что функция dhAssert выдает заданный по умолчанию текст сообщения об ошибке, вам следует ввести собственное описание проблемы. Это значительно упрощает ее решение. Широкое использование утверждений в программе помогает находить многие логические ошибки. Однако за это приходится платить незначительным увеличением размера программы и снижением се быстродействия.
Систематическая отладка
Хотя приведенные рекомендации сведут к минимуму количество ошибок в ваших программах, некоторые ошибки неизбежно останутся. Ниже предложены кое-какие методы, позволяющие избавиться от них.
Прежде всего, при отладке нужно иметь возможность создать ситуацию, которая вызвала ошибку. Если вы не можете воспроизвести ошибку, будет очень сложно отследить ’’жучок". Вам повезет, если "жучок” проявится при отладке программы; однако если вы не можете воспроизвести его, ваши шансы исправить ошибку малы. Чтобы воссоздать ошибочную ситуацию, вам нужно иметь как можно больше информации об условиях, которые вызвали ошибку, но не более того. Если пользователь предоставит вам список с сотней операций, выполненных им в то утро, когда произошла ошибка, это затруднит вашу работу. Если же ограничить предоставляемую информацию тремя существенными операциями, которые действительно вызвали ошибку, это может вам помочь. Иногда ошибочная ситуация возникает только в результате выполнения большого числа операций, или, например, после четырехчасовой работы с приложением. Понятно, что найти и исправить такие ошибки гораздо труднее.
Второе правило отладки заключается в том, что вы должны отлаживать не программу, а данные. Это означает, что вместо того чтобы, просматривая программу, размышлять о том, что же она делает, нужно при помощи отладчика определить, какие именно данные приводят к ошибке. Это правило кажется тривиальным, однако оно очень эффективно при выявлении "жучков".
После того как ошибка обнаружена, убедитесь, что вы понимаете суть проблемы, прежде чем вносить исправления в программу. Например, часто ошибка возникает при описании переменных простых типов данных (Integer, Long, string и т.д.). У вас в процессе тестирования программа работает прекрасно, однако у вашего клиента она выдает сообщение "Invalid Use of Null" (Недопустимое использование Null).
Существует два решения этой проблемы, причем каждое из них требует понимания конкретного приложения. Очевидно, что значение Null нельзя присваивать переменным типа Integer, Long или String, поэтому вы можете принять решение об изменении их типа на Variant. С другой стороны, возможно, следует запретить пользователю вводить значение Null. При принятии решения необходимо учитывать конкретную ситуацию. Ясно, что изменять программу нужно только при наличии веских оснований, однако удивительно, что многие программисты игнорируют этот принцип.
Наконец, независимо от того, насколько опытны вы в программировании и насколько уверены в результате, вносите за один раз только одно изменение в вашу программу, а затем тестируйте ее снова. Очень легко попасть в ловушку, сделав одновременно несколько изменений, после чего вам придется выбросить текущую версию программы и вернуться к резервной копии, так как вы не будете знать, какое исправление работает правильно, а какое только усугубляет ситуацию. Словом, тише едешь — дальше будешь. В качестве дополнительной меры предосторожности рекомендуем документировать изменения в программе, используя комментарии либо в
конкретных процедурах, либо в разделе описаний модуля. Ведя учет внесенных изменений с указанием дат, когда они были сделаны, вы сможете отслеживать проблемы, вызванные различными "исправлениями".
I Совет: Другой способ предотвратить неблагоприятное влияние исправ- L I у лений, внесенных в одну процедуру, на другие части вашей программы заключается в использовании программы управления исходным кодом, которая поддерживает различные версии. Это позволяет вернуться к работающей версии программы и применить другую тактику отладки.
Существуют еще два элемента стратегии отладки, которые и предлагаются для рассмотрения. Многие программисты считают одним из наиболее действенных способов отладки такой: вы хватаете все свои распечатки и отправляетесь в соседнюю комнату, прерываете работу программиста, который там находится, и говорите: "Послушай-ка, извини за беспокойство, но у меня тут отказывается работать одна функция. Смотри, я передаю ей здесь имя формы, а затем описываю переменную формы, а потом — ой, подожди, это же не сама форма, а только ее имя. Все равно, спасибо за помощь." Если вам это помогает, вы можете дать себе волю и работать подобным образом, используя в качестве аудитории непрограммистов, и даже (в моменты отчаяния) обсуждать ситуацию со своей собакой или с сочувствующей (хотя и скучающей) любящей половиной.
Конечно, иногда подобной исповеди вашей программе недостаточно. Если вы имеете удовольствие работать с себе подобными, воспользуйтесь этим. Вполне возможно, что другому программисту удастся заметить ошибку там, где вы ее пропустили.
Когда другие средства не помогают, сделайте перерыв. Легко зациклиться при попытке повторить одно и то же снова и снова, хотя это не работало с первого раза. Прогуляйтесь, пообедайте, примите душ. Иногда "жучков" отыскивает ваше подсознание, в то время как вы думаете совсем о другом. Многие программисты рассказывают истории о том, как они просыпались посреди ночи, увидев во сне "жучок", о существовании которого до этого момента даже и не подозревали. Записав ночью информацию, они шли на работу, находили ранее незамеченный "жучок" и тут же устраняли проблему.
Оперативная справка
Оперативная справка — ценный ресурс для современных пользователей компьютеров. Чаще всего она является единственной доступной формой помощи. С целью экономии денег многие крупные компании не поставляют каждому пользователю печатную документацию. Более того, большинство пользователей предпочитают обращаться к оперативной справке даже при наличии печатного руководства. Оперативная справка способствует быстрому признанию вашего приложения, а также сокращает расходы на сопровождение продукта. В этом разделе описано, каким образом с помощью VBA можно подключить оперативную справку к своему приложению. Однако данный раздел нельзя рассматривать как руководство по созданию файлов справки. Для этих целей служит документация, сопровождающая программное обеспечение для создания файлов справки.
Файлы справки
Имейте в виду, что для обращения к определенной теме справки вам нужно знать не только имя файла справки, но и контекстный идентификатор темы. Контекстный идентификатор представляет собой уникальное числовое значение, соответствующее определенной теме в файле справки. При разработке тем справки каждой из них присваивается уникальное имя. Перед компиляцией файла справки этим именам ставятся в соответствие числовые контекстные идентификаторы.
Темы справки в программе
VBA позволяет обратиться к определенной теме справки посредством:
функции inputBox;
функции MsgBox;
метода Err.Raise.
Данные процедуры имеют необязательные аргументы для имени файла справки и контекстного идентификатора. Если эти параметры заданы при вызове функции inpytBox или MsgBox, VBA добавляет в соответствующее диалоговое окно кнопку Справка (Help). Например, на рис. 5.11 представлено диалоговое окно, которое появляется при выполнении следующего оператора:
?InputBox(Prompt:^"Введите нужное значение:", _
Title:="Ввод значения", HelpFile:="MYAPP.HLP", _
Context:=100)
Рис. 5.11. Задание имени файла справки и контекстного идентификатора заставляет VBA отображать в диалоговом окне кнопку Справка
Если пользователь нажимает клавишу [F1] или щелкает мышью на кнопке Справка, VBA открывает файл справки для заданного контекстного идентификатора.
Метод Raise объекта Err также имеет необязательные аргументы для файла справки и контекстного идентификатора. Стандартное диалоговое окно VBA, сообщающее об ошибке, содержит кнопку Справка, после нажатия на которую открывается файл справки VBA на теме Ошибка, определяемая приложением или объектом (Application defined error). Однако если вы задаете собственный файл справки и контекстный идентификатор, VBA в ответ на метод Raise открывает ваш файл справки. Если у вас определен обработчик ошибок, вы имеете доступ к этой информации с помощью свойств HelpFile и HelpContext объекта Err. В примере 5.12 представлена тестовая процедура, использующая метод Err.Raise для этих целей.
Пример 5.12. Использование метода Err.Raise для предоставления информации файла справки
Sub ShowHelpInfо()
On Error GoTo HandleError
Err.Raise 12345, "Sub ShowHelpInfo", _
"This error is by design.", "MAINAPP.HLP", 100
ExitHere:
Exit Sub
HandleError:
Debug.Print Err.HelpFile, Err.HelpContext Resume ExitHere
End Sub
Примечание: Хотя аргументы HelpFile и HelpContext функций InputBox, MsgBox и метода Raise являются необязательными, если вы задали один из них, то обязательно следует задать и второй.
Резюме
В этой главе рассмотрены весьма существенные аспекты разработки профессиональных приложений. Подробно проанализирована обработка ошибок и отладка программы. Теперь вы знаете, как VBA обрабатывает ошибки в ваших приложениях, как предупреждать их появление и бороться с ними. Кроме того, мы рассказали о различных средствах отладки, предоставляемых VBA, и предложили ряд рекомендаций и советов по устранению ошибок из вашей программы.
Мы обсудили не только процесс обработки ошибок, но и способы добавления в приложение элементов профессионального проектирования. Рассмотрены вопросы реализации стеков процедур и протоколирования ошибок, которое предоставит вам адекватные сведения об ошибочных ситуациях.
В этой главе вы познакомились со следующими вопросами:
как VBA обрабатывает ошибки выполнения;
как используются операторы On Error и Resume;
зачем нужны такие средства отладки, как точки останова, контрольные значения и стек вызова;
как реализовать в приложении оперативную подсказку.
ГЛАВА 6
Windows Networking API
Основные сетевые функции
Дополнительные сетевые функции
VBA не поддерживает множество сетевых возможностей Windows 95 и Windows NT, хотя позволяет использовать такие объекты, как подключенные диски и сетевые пути, если они существуют. Но возможности поиска, подключения или отключения сетевых ресурсов, составления списка этих объектов в VBA недоступны. Однако все это можно реализовать через Windows API. В данной главе рассмотрены некоторые наиболее полезные функции WNet и LAN Manager API, которые позволяют обеспечить ваше приложение более полной информацией о сети.
ffПримечание: Почему мы рассматриваем как функции WNet API, так и функции LAN Manager API? Функции WNet API, вообще говоря, использовать проще, и они позволяют решить большинство задач, связанных с работой в сети. Функции LAN Manager API предназначены для более широкого круга задач, они несколько сложнее в использовании и иногда немного пугают, поскольку приходится выбирать более чем из 100 различных функций. В этой главе проведен обзор как функций WNet API, так и функций LAN Manager API, однако особое внимание уделяется функциям WNet, поскольку они нужны большинству программистов. Кроме того, функции LAN Manager API доступны только из Windows NT, поэтому область их применения ограничена.
f
f
Основные сетевые функции
В данном разделе описаны основные сетевые функции, которые могут понадобиться при создании приложения, а именно:
для подключения к сетевым ресурсам с помощью стандартных диалоговых окон;
для отключения от сетевых ресурсов посредством стандартных диалоговых окон;
для программного подключения к сетевым ресурсам;
для программного отключения от сетевых ресурсов;
■ для получения информации о сетевых ресурсах.
Использование диалоговых окон работы с сетью
Наиболее простой и знакомый пользователям способ добавления в приложения функций работы с сетью заключается в использовании диалоговых окон операционной системы, которые дают возможность подключаться к сетевым ресурсам и отключаться от них.
Подключение к сетевому ресурсу с помощью диалогового окна
На рис. 6.1 представлен пример диалогового окна операционной системы Windows NT 4.0 для подключения сетевых ресурсов. Учтите, что диалоговые окна подключения сетевых ресурсов различных операционных систем могут отличаться. Хотя диалоговые окна Windows NT 3.51 и Windows NT 4.0 похожи, диалоговое окно, используемое Windows 95, очень сильно отличается от них. В Windows 95 необходимо вводить UNC-путь (Uniform Naming Convention — единые правила наименования), при этом в диалоговом окне появляется MRU-список (Most Recently Used) произведенных ранее подключений. Однако в окне Windows 95 нет той возможности просмотра ресурсов, которую предоставляет Windows NT.
Map Network Drive
|_
I Drive:| £3 I:
i path:Jv3RA^^
। Connect As: (
P Expand by DefaultP Expand by DefaultI^ Reconnect at Logon
P Expand by Default
P Expand by Default
| Shared Directories:
^ Microsoft Windows Network
|^ CAD-LAB
■® CS-LAB
+®ALIEN2
■®CACI2
■"QDRAG0N2
; iSBOOKS
! I© cd-rom
| ।£) ClientsN et work Client Distribution S h<
Я export
S home
Puc. 6.1. Подключение к сетевым ресурсам с помощью диалогового окна подключения Windows NT 4.0
Для активизации диалогового окна нужно использовать следующее описание функции:
Private Declare Function WNetConnectionDialog Lib "mpr.dll" _
(ByVai hwnd As Long, ByVai dwType As Long) As Long
Единственно, что необходимо задать, — это дескриптор окна, которое будет родительским по отношению к данному диалоговому окну (или 0, если в качестве родительского используется окно Windows), а также тип отображаемых ресурсов. Наиболее распространенные константы, которые используются диалоговыми окнами, представлены в табл. 6.1.
Рис. 6.2. Подключение к сетевым ресурсам с помощью диалогового окна подключения Windows 95
Таблица 6.1. Распространенные константы сетевых диалоговых окон
Константа
Значение
Описание
DhcNoError
0&
Вызов функции выполнен успешно
DhcDlgCancelled
-l&
Пользователь закрыл диалоговое окно кнопкой Отмена (Cancel)
dhcErrorExtendedError
1208&
Произошла расширенная ошибка
dhcErrorlnvalidPassword
86&
Задан неверный пароль
dhcErrorNoNetwork
1222&
Сеть не обнаружена
dhcErrorNotEnoughMemory
8&
Недостаточно памяти для отображения диалогового окна
dhcResourceTypeDisk
1&
Использование ресурсов типа диск
dhcResourceTypePrint
2&
Использование принтерных ресурсов
m
Совет: Полагаем, что не стоит вызывать сетевые функции API непосредственно, поскольку при этом важно задать правильно все параметры. Для того чтобы обеспечить правильный вызов функций, мы реализовали функ- ции-оболочки для всех API-функций. Большинство функций API возвращает код ошибки, который сообщает, успешно ли выполнена функция; иногда функции-оболочки делают то же самое, а в некоторых случаях обрабатывают возвращаемое значение сами.
В примере 6.1 продемонстрирована функция, которую можно использовать для вызова функции API WNetConnectionDialog.
Пример 6.1. Функция-оболочка для вызова диалогового окна подключения сетевого ресурса
Public Function dhConnectDlg(Optional ByVai hwnd As Long = 0) _
As Long
' Отображает диалоговое окно подключения к сетевым ресурсам.
Dim IngReturn As Long
Dim IngExtendedError As Long
' Вызов диалогового окна подключения к сетевым ресурсам.
IngReturn = WNetConnectionDialog(hWnd, dhcResourceTypeDisk)
' Если функция не выполнена, принимает информацию об ошибке.
If IngReturn О dhcNoError Then
IngExtendedError = dhGetLastNetworkError(True)
' Если произошла расширенная ошибка, возвращает
' ее вместо стандартной ошибки.
dhConnectDlg = Ilf(IngExtendedError = dhcNoError, _
IngReturn, IngExtendedError)
End If
End Function
В Windows NT можно задать только флаг dhcResourceTypeDisk, а в Windows 95 — еще и флаг dhcResourceTypePrint, который позволяет подключиться к сетевым принтерам.
Изменение MRU-списка
Один из способов добавить возможность просмотра в диалоговое окно Windows 95 и усовершенствовать диалоговое окно Windows NT — поместить собственные элементы в MRU-список. Эти записи хранятся в реестре под следующими двумя ключами (в зависимости от установленной на вашем компьютере операционной системы):
* Для Windows NT:
HKEY_CURRENT_USER\Software\Microsoft\Windows NT\ _
Current Version\Network\Persistent Connections * Для Windows 95: HKEY CURRENT USER\Network\Recent
' Registry Editor' Registry EditorHHHelp■■'Windows Messaging SubsystemWindows NT_J Currentversion-u Devices- I ExtensionsH Network’ : CD Event ViewerHelp■■'Windows Messaging SubsystemWindows NT_J Currentversion-u Devices- I ExtensionsH Network’ : CD Event ViewerNarasВ (Default)NarasВ (Default)QbQb®d®d; j Remote Access Admin _J Remoteboot Manager• : i Server Manager•1User Manager] User Manager for Domai_J PrinterPorts[+1 .. f Program Manager; j Remote Access Admin _J Remoteboot Manager• : i Server Manager•1User Manager] User Manager for Domai_J PrinterPorts[+1 .. f Program Manager@1 Order§1 SaveConnections@1 Order§1 SaveConnectionsi Date _. .(value not set) "Wcaci2\c" "WSCANERXC" "Wgoblin3\c" "Winten2\c$" "Wmulti3\c" "edcba" "yes”i Date _. .(value not set) "Wcaci2\c" "WSCANERXC" "Wgoblin3\c" "Winten2\c$" "Wmulti3\c" "edcba" "yes”
' Registry Editor
' Registry Editor
H
H
Help■■'
Windows Messaging Subsystem
Windows NT
_J Currentversion
-u Devices
- I Extensions
H Network
’ : CD Event Viewer
Help■■'
Windows Messaging Subsystem
Windows NT
_J Currentversion
-u Devices
- I Extensions
H Network
’ : CD Event Viewer
Naras
В (Default)
Naras
В (Default)
Qb
Qb
®d
®d
; j Remote Access Admin _J Remoteboot Manager
• : i Server Manager
•1User Manager
] User Manager for Domai
_J PrinterPorts
[+1 .. f Program Manager
; j Remote Access Admin _J Remoteboot Manager
• : i Server Manager
•1User Manager
] User Manager for Domai
_J PrinterPorts
[+1 .. f Program Manager
@1 Order
§1 SaveConnections
@1 Order
§1 SaveConnections
i Date _. .
(value not set) "Wcaci2\c" "WSCANERXC" "Wgoblin3\c" "Winten2\c$" "Wmulti3\c" "edcba" "yes”
i Date _. .
(value not set) "Wcaci2\c" "WSCANERXC" "Wgoblin3\c" "Winten2\c$" "Wmulti3\c" "edcba" "yes”
Г*.- Lc^-pnir!1 hr EYj/ijr.^H jL-EcEh^DtiwateV^N1 XCurrenb/ersfurhNe^^u?rn?ck.<:
Рис. 6.3. Использование реестра для изменения MRU-списка соединений
Каждое значение в реестре под заданным ключом представляет собой строку; ее имя — это буква алфавита, а значение — элемент списка. После списка упорядоченных по алфавиту ключей следует значение под именем Порядок (Order), определяющее порядок отображения записей. Чтобы поместить элемент в MRU-список, нужно включить его в список значений, а затем добавить соответствующую букву в строку Порядок. Изменив список, не забудьте по завершении работы удалить все внесенные изменения, если не хотите оставлять их. На рис. 6.3 показано окно, содержащее ключи реестра для некоторого сетевого окружения.
Получение информации о расширенной сетевой ошибке
Обратите внимание на дополнительные вычисления, которые выполняет функция dhConnectDlg для выбора возвращаемого кода ошибки в примере 6.1. Основная причина включения этих дополнительных операций носит исторический характер: функция WNetConnectionDialog, которая была популярна определенное время, возвращает те же самые значения основных ошибок, что и 16-битовая версия: 0 при успешном завершении, -1 при отмене, а также несколько конкретных значений ошибок. Вместо того чтобы изменять возвращаемые значения этой функции, что привело бы к нарушению совместимости с преобразованными приложениями, компания Microsoft решила использовать возможности расширенных ошибок, предоставляемые функцией WNetGetLastError, для представления детальной информации. Информация о расширенных ошибках используется и другими сетевыми функциями API, однако функция WNetDisconnectDialog применяет ее часто, поскольку сама она не возвращает достаточной информации об ошибках.
Функция dhConnectDlg, а также некоторые другие функции, описанные в этой главе, вызывают функцию dhGetLastNetworkError, чтобы получить информацию о расширенной ошибке. Многие из функций WNet API могут возвращать константу dhcErrorExtendedError. В этом случае нужно немедленно вызвать функцию API WNetGetLastError для получения более подробной информации об ошибке. В качестве оболочки функции API WNetGetLastError предлагаем функцию dhGetLastNetworkError, которая возвращает код расширенной ошибки и выводит диалоговое окно с дополнительной информацией (если такая имеется). Функция dhGetLastNetworkError представлена в примере 6.2.
Пример 6.2. Получение информации о последней сетевой ошибке
Public Function dhGetLastNetworkError( _
Optional fDisplayError As Boolean = False) As Long
* Получает информацию об ошибке от последней сетевой операции.
Dim IngReturn As Long
Dim IngError As Long
Dim strError As String
Dim IngErrorLen As Long
Dim strProvider As String
Dim IngProviderLen As Long
* Устанавливает буферы для информации об ошибке * (они должны иметь размер не менее 256 символов, * но не помешает сделать их длиннее).
strError = String$(256, vbNullChar)
strProvider = Strings(256, vbNullChar)
IngErrorLen = Len(strError)
IngProviderLen = Len(strProvider)
* Пытается получить информацию о расширенной ошибке. IngReturn = WNetGetLastError(IngError, strError, _ IngErrorLen, strProvider, IngProviderLen)
If IngReturn = dhcNoError Then dhGetLastNetworkError = IngError
* Если есть информация о расширенной ошибке
* и вызывающая процедура требует ее отображения,
* информация отображается.
If fDisplayError And IngError <> dhcNoError Then strError = dhTrimNull(strError) strProvider = dhTrimNull(strProvider)
MsgBox "Error " & IngError & ": " & strError, vblnformation, strProvider
End If
End If
End Function
!!Внимание: Важно подчеркнуть, что API-функция WNetGetLastError возвращает значение только самой последней ошибки. Функцию dhGet- LastNetworkError необходимо вызывать сразу после выполнения функции, которая могла возвратить ошибку. Если пользователь осуществит какую-то сетевую операцию, либо вручную, либо посредством другого приложения, информация об ошибке вашей программы будет утеряна навсегда.
!
!
Отключение от сетевого ресурса с помощью диалогового окна
Может возникнуть необходимость предоставить в рамках приложения возможность разорвать сетевое соединение. Эта возможность обеспечивается API-функцией WNetDisconnectDialog, описание которой почти идентично описанию функции WNetConnectionDialog:
Private Declare Function WNetDisconnectDialog Lib "mpr.dll" _
(ByVai hwnd As Long, ByVai dwType As Long) As Long
W1 varvna хагт^с ahon^JH:\\T varyna\newway^R:\\DRAGON2\netappl^S:\\DRAGON2\netappl2^Z:\\DRAG0N2\homeW1 varvna хагт^с ahon^JH:\\T varyna\newway^R:\\DRAGON2\netappl^S:\\DRAGON2\netappl2^Z:\\DRAG0N2\homeDisconnect Network DriveDisconnect Network DriveNetwork Drive1Network Drive1OKCancelHelpOKCancelHelp
W1 varvna хагт^с ahon
^JH:
\\T varyna\newway
^R:
\\DRAGON2\netappl
^S:
\\DRAGON2\netappl2
^Z:
\\DRAG0N2\home
W1 varvna хагт^с ahon
^JH:
\\T varyna\newway
^R:
\\DRAGON2\netappl
^S:
\\DRAGON2\netappl2
^Z:
\\DRAG0N2\home
Disconnect Network Drive
Disconnect Network Drive
Network Drive1
Network Drive1
OK
Cancel
Help
OK
Cancel
Help
Puc. 6.4. Отключение от сетевых ресурсов с помощью окна отключения Windows NT
Данная функция открывает диалоговое окно, показанное на рис. 6.4. Для вызова функции можно воспользоваться функцией-оболочкой dhDisconnectDialog, которая представлена в примере 6.3.
Пример 6.3. Отключение от сетевых ресурсов
Public Function dhDisconnectDialog( _
Optional ByVai hwnd As Long = 0) As Long
Отображает диалоговое окно отключения от сетевых ресурсов.
Dim IngReturn As Long
Dim IngExtendedError As Long
* Вызов диалогового окна отключения от сетевых ресурсов.
IngReturn = WNetDisconnectDialog(hwnd, dhcResourceTypeDisk)
1 Если функция не выполнена, получает информацию об ошибке.
If IngReturn о dhcNoError Then
IngExtendedError = dhGetLastNetworkError(True)
* Если произошла расширенная ошибка, возвращает
* ее вместо стандартной ошибки.
dhDisconnectDialog = Ilf ( _
IngExtendedError = dhcNoError, _
IngReturn, IngExtendedError)
End If
End Function
Как и в случае с dhConnectDlg, в результате будет возвращено либо значение dhcNoError, если отключение действительно выполнено, либо dhcDlgCancelled, если пользователь закрыл диалоговое окно, нажав кнопку Отмена (Cancel). Кроме того, может быть возвращена одна из констант, представленных в табл. 6.2.
Другие диалоговые окна, которыми можно воспользоваться
В дополнение к функциям dhConnectDlg и dhDisconnectDialog можно воспользоваться стандартным диалоговым окном Windows Открытие файла (Open File), которое описано в главе 7. Это окно содержит кнопку Сеть (Network), позволяющую пользователю подключиться к сетевому ресурсу. Кроме того, данное диалоговое окно знакомо большинству пользователей и его легко включить в ваше приложение. Если вы хотите подключиться к сетевому ресурсу, часто необходимо выбрать файл, который вы уже когда-то подключали, поэтому это самый простой способ добавить информацию о состоянии сети в ваше приложение.
Имеется несколько преимуществ использования диалоговых окон, которые предоставляет операционная система:
их легко вызывать и включать в ваше приложение;
их внешний вид и поведение знакомы пользователю;
вся работа по реальному подключению к сети или отключению от нее выполняется диалоговыми окнами.
Конечно, имеются и некоторые недостатки:
при успешном выполнении функции не просто узнать, к какому ресурсу подключился пользователь;
интерфейс диалоговых окон не настраивается с помощью вашего приложения;
вы не можете управлять процессом соединения (например, у вас нет возможности указать, сохранять данное соединение при перезагрузке компьютера или нет).
Из-за этих проблем многие приложения, которым необходимо иметь сетевые возможности, должны выполнять такую работу самостоятельно, а не возлагать ее на диалоговые окна системы.
Управление сетевыми ресурсами
Иногда может возникнуть необходимость в рамках вашего приложения управлять подключением к сетевым ресурсам и отключением от них. В этом разделе рассмотрены основные функции, предназначенные для выполнения сетевых операций.
Подключение к сетевому ресурсу
Имеются две функции Win32 API, которые позволяют подключиться к сетевому ресурсу: WNetAddConnection и WNetAddConnection2. Первая служит для совместимости со старыми 16-разрядными API. Вызвать эту функцию проще, однако она не столь гибкая и не возвращает столько информации, как более новая версия. Здесь представлены обе функции, однако обычно в приложениях целесообразнее использовать функцию WNetAddConnection2.
Ниже приведены описания и типы для этих функций:
Private Type NETRESOURCE
dwScope As Long
dwType As Long
dwDisplayType As Long
dwUsage As Long
IpLocalName As String
IpRemoteName As String
IpComment As String
IpProvider As String
End Type
Private Declare Function WNetAddConnection Lib "mpr.dll" Alias _ "WNetAddConnectionA" (ByVai strNetPath As String, _
ByVai strPassword As String, ByVai strLocalName As String) _ As Long
Private Declare Function WNetAddConnection2 Lib "mpr.dll" Alias _ "WNetAddConnection2A" (IpNetResource As NETRESOURCE, _ ByVai strPassword As String, ByVai strUserName As String, _ ByVai IngFlags As Long)As Long
Для того чтобы вызвать функцию WNetAddConnection, достаточно задать следующие параметры:
путь устройства, к которому вы хотите подключиться;
пароль (если таковой имеется) или строку нулевой длины;
локальное имя, которое вы хотите назначить новому ресурсу.
Для вызова функции WNetAddConnection вы можете воспользоваться функцией dhAddConnectionl, представленной в примере 6.4.
Пример 6.4. Подключение ресурсов с помощью функции WNetAddConnection
Function dhAddConnectionl(strNetPath As String, _ strPwd As String, strLocalName As String) As Long * Добавляет сетевое соединение.
dhAddConnectionl = WNetAddConnection(strNetPath, _ strPwd, strLocalName)
End Function
Вызов функции dhAddConnectionl осуществляется следующим образом:
IngReturn = dhAddConnectionl("\\middlemarch\setup", _ "password", "J:")
При этом локальному диску J ставится в соответствие заданный совместно используемый сетевой ресурс с указанием пароля password. Если в вашей сети применяется доменная модель защиты Windows NT, соединение будет установлено с правами доступа данного пользователя.
Константы для функций WNetAddConnection и WNetAddConnection2 приведены в табл. 6.2.
Таблица 6.2 Константы функций WNetAddConnection и WNetAddConnection2
Константа
Значение
Описание
DhcNoError
0&
Вызов функции выполнен успешно
dhcErrorAccessDenied
5&
Недостаточные права доступа к указанному ресурсу
dhcErrorAlreadyAssigned
85&
Заданное локальное имя уже присвоено другому ресурсу
dhcErrorBadDevType
66&
Тип устройства не соответствует типу ресурса
DhcErrorBadDevice
1200&
Ошибочное устройство
dhcErrorBadNetName
67&
Имя удаленного ресурса указано неверно или ресурс не может быть обнаружен
dhcErrorBadProfile
1206&
Профиль пользователя задан в неверном формате
dhcErrorCannotOpenProfile
1205&
Нет доступа к профилю пользователя для обновления постоянной информации
dhcErrorDeviceAlready- Remembered
1202&
В профиле пользователя уже присутствует запись для указанного локального имени
dhcErrorExtendedError
1208&
Произошла расширенная ошибка
dhcErrorlnvalidPassword
86&
Задан неверный пароль
dhcErrorNoNetOrBadPath
1203&
Имя удаленного ресурса указано неверно или ресурс не может быть обнаружен
DhcErrorNoNetwork
1222&
Сеть не обнаружена
Константа
Значение
Описание
dhcResourceTypeAny
0&
Использование ресурсов любого типа
dhcResourceTypeDisk
1&
Использование ресурсов типа диск
dhcResourceTypePrint
2&
Использование принтерных ресурсов
DhcMaxPath
260
Максимально допустимое количество символов в пути к удаленному ресурсу
dhcConnectUpdateProfile
1&
Обновление профиля пользователя для сохранения этого подключения после перезагрузки
dhcConnectDontUpdateProfile
0&
Не обновлять профиль пользователя
Во многих случаях функция WNetAddConnection просто недостаточно гибкая. Вам может понадобиться установить соединение, используя имя и пароль другого пользователя (возможно, с другими правами доступа) либо указать, сохранять установленное соединение после перезагрузки компьютера или нет, либо задать провайдера сети при наличии нескольких сетей. Если вам нужны эти возможности, следует использовать функцию WNetAddConnection2 И функцию-оболочку dhAddCon- nection2, которая представлена в примере 6.5.
Пример 6.5. Подключение ресурсов с помощью функции WNetAddConnection2
Function dhAddConnection2(strNetPath As String, _ strLocalName As String, strUserName As String, _ strPwd As String, Optional fPersistent As Boolean = True, _ Optional fDisk As Boolean = True) As Long
Добавляет сетевое соединение.
Dim usrNetResource As NETRESOURCE
Dim IngFlags As Long
With usrNetResource
.dwType = Ilf (fDisk, dhcResourceTypeDisk, __ dhcResourceTypePrint)
.IpLocalName = strLocalName
.IpRemoteName = strNetPath
.IpProvider = vbNullString
End With
IngFlags = Ilf(fPersistent, dhcConnectUpdateProfile, _ dhcConnectDontUpdateProfile)
dhAddConnection2 = WNetAddConnection2(usrNetResource, _
strPwd, strUserName, IngFlags) End Function
Эта функция принимает параметры, представленные в следующей таблице.
strNetPath
UNC-путь, к которому необходимо подключиться
strLocalName
Локальное имя ресурса (например, К)
strUserName
Пользователь, чьи права доступа будут использоваться при подключении
strPwd
Пароль пользователя, определенного параметром strUserName
fPersistent
Указывает, сохранять ли это соединение после перезагрузки (по умолчанию параметр принимает значение True и использовать его необязательно)
fDisk
Указывает, является ли этот ресурс диском (если значение параметра True, то ресурс является диском, а если False — принтером; по умолчанию параметр принимает значение True и является необязательным)
Описание
Параметр
Функция заполняет элементы структуры netresource, а затем вызывает WNet- AddConnection2. Ниже представлен пример вызова функции dhAddConnection2:
IngReturn = dhAddConnection2("\\middlemarch\setup", "К:", _
"Sam”, "", False)
Этот вызов подключает диск К к заданному ресурсу с правами доступа пользователя по имени Sam (который не имеет пароля). Когда пользователь выполнит перезагрузку, данное соединение не будет переустановлено.
Отключение от сетевого ресурса
Для отключения ресурса, так же как и для его подключения, имеется две функции API: WNetCancelConnection и WNetCancelConnection2. Единственной дополнительной возможностью второй функции является то, что вы можете выбирать, указывать в профиле пользователя, что ресурс отключен, или нет. (Если не обновлять профиль пользователя, то при следующей загрузке компьютера ресурс вновь будет доступен.) Ниже представлены описания этих функций:
Private Declare Function WNetCancelConnection Lib _
"mpr.dll" Alias "WNetCancelConnectionA" _
(ByVai IpszName As String, ByVai fForce As Long) As Long
Private Declare Function WNetCancelConnection2 Lib _
"mpr.dll" Alias "WNetCancelConnection2A" _
(ByVai IpName As String, ByVai dwFlags As Long, _
ByVai fForce As Long) As Long
Часто встречающиеся константы, которые могут использоваться с обеими функциями, приведены в табл. 6.3.
Таблица 6.3. Константы функций WNetCancelConnection и WNetCancelConnection2
Константа
Значение
Описание
DhcNoError
0&
Вызов функции выполнен успешно
DhcErrorBadProfile
1206&
Профиль пользователя задан в неправильном формате
dhcErrorCannotOpenProfile
1205&
Нет доступа к профилю пользователя для обновления постоянной информации
DhcErrorDeviceInUse
2404&
В данный момент устройство используется, а параметру Force не присвоено значение True
DhcErrorExtendedError
1208&
Произошла расширенная ошибка
DhcErrorNotConnected
2205&
Указанный ресурс в данный момент не подключен
DhcErrorOpenFiles
2401&
Устройство используется, его файлы открыты, а параметру Force не присвоено значение True
dhcConnectUpdateProfile
1&
Обновление профиля пользователя для отражения того, что ресурс отключен
dhcConnectDontUpdateProfile
0&
Не обновлять профиль пользователя
Для вызова WNetCancelConnection можно воспользоваться функцией dhCancelConnectionl, представленной в примере 6.6. Задайте этой функции имя ресурса, который необходимо отключить, и необязательный параметр типа Boolean (его значение по умолчанию равно False), указывающий на то, хотите ли вы выполнить отключение в том случае, когда устройство используется или имеются открытые файлы.
Пример 6.6. Отключение от ресурса с помощью функции WNetCancelConnection
Public Function dhCancelConnectionl(strLocalName As String, Optional fForceDisconnect As Boolean = False) As Long
* Отключает сетевое соединение.
dhCancelConnectionl = WNetCancelConnection( _ strLocalName, Abs(fForceDisconnect))
End Function
Совет: Обратите внимание на то, что dhCancelConnectionl использует функцию Abs (абсолютное значение) при передаче параметра fForce- Disconnect типа Boolean. Многие функции API принимают параметры типа Boolean, однако C++ определяет значение True как 1, тогда как VBA определяет True как -1. В большинстве случаев функции API сравнивают все значения с False (т.е. 0), поэтому неважно, чему равно True — 1 или -1. Однако бывают исключения. Например, если DLL проверяет именно
значение True (1), то передача значения True, заданного VBA (-1), приведет к ошибке. Это не относится к тому случаю, когда DLL передает значение вашей программе: если вы определили переменную как Boolean и DLL передает ей значение 1, VBA правильно трактует данное значение как True.
Вызов WNetCancelConnection2 С ПОМОЩЬЮ функции dhCancelConnection2 (пример 6.7) осуществляется аналогичным образом. Эта функция добавляет еще один флаг, который указывает, хотите ли вы обновлять профиль пользователя.
Пример 6.7. Отключение от ресурсов
с помощью функции WNetCancelConnection2
Public Function dhCancelConnection2(strLocalName As String, _
Optional fForceDisconnect As Boolean = False, _
Optional fUpdateProfile As Boolean = True) As Long
Отключает сетевое соединение.
Dim IngFlags As Long
IngFlags = Ilf(fUpdateProfile, dhcConnectUpdateProfile, _ dhcConnectDontUpdateProfile)
dhCancelConnection2 = WNetCancelConnection2(strLocalName, _ IngFlags, Abs(fForceDisconnect))
End Function
ffВнимание: Мало что расстроит пользователей больше, нежели отключение вами сетевых ресурсов, которые в данный момент используются. Поэтому всегда при вызове функций dhCancelConnectionl И dhCancel- Connection2 устанавливайте для параметра f ForceDisconnect значение False. Если функция возвратит значение dhcErrorOpenFiles или dhcErrorDevicelnUse, вы сможете выдать пользователю предупреждающее сообщение о том, что на диске имеются открытые файлы и существует риск потерять несохраненные данные. Когда пользователь подтвердит, что он действительно хочет отключить соединение, можно повторить операцию отключения, установив на этот раз fForceDisconnect равным True.
f
f
Получение информации о сетевых ресурсах
Для работы некоторых сетевых приложений может понадобиться UNC-путь определенного подключенного сетевого диска или имя текущего пользователя, а, возможно, вам нужно получить (или даже изменить) имя компьютера. Эти вопросы рассматриваются в данном разделе.
Определение UNC-пути подключенного сетевого диска
Представьте себе обычную ситуацию: в текущем сеансе работы пользователь вашего приложения изменил файл, который находится на подключенном сетевом диске. В следующий раз, когда пользователь входит в систему, логическое имя (буква) диска изменилось, и ваше приложение не в состоянии найти тот же самый файл. Как
решить такую проблему? Вместо того чтобы хранить информацию о подключенном диске, нужно преобразовать логическое имя диска в его UNC-путь (включая имя сервера и имя диска), используя функцию API WNetGetConnection. В качестве входного параметра эта функция принимает локальное имя, а возвращает соответствующий UNC-путь. (Конечно, выполнение данной функции может и не завершиться успешно, поскольку в случае задания неправильного локального пути может не оказаться соответствующего удаленного пути.)
Описание функции WNetGetConnection выглядит следующим образом:
Private Declare Function WNetGetConnection Lib "mpr.dll" _
Alias "WNetGetConnectionA” (ByVai strLocalName As String, _
ByVai strRemoteName As String, IngRemoteNameLen As Long) As Long
Функция принимает локальное имя, буфер, в который будет записываться UNC-путь, а также значение длины этого буфера. В результате успешного выполнения функции происходит запись в буфер UNC-пути. Функция возвращает значение dhcNoError в случае успешного выполнения или один из стандартных кодов ошибки в случае неудачи. Наиболее часто встречающиеся константы приведены в табл. 6.4.
Таблица 6.4. Константы функции WNetGetConnection
Константа
Значение
Описание
DhcNoError
0&
Вызов функции выполнен успешно
DhcErrorBadDevice
1200&
Неверное локальное имя
DhcErrorNotConnected
2205&
Указанный ресурс в данный момент не подключен
DhcErrorMoreData
234
Размер буфера слишком мал (параметр длины буфера будет содержать необходимое значение, если функция возвращает эту ошибку)
dhcErrorExtendedError
1208&
Произошла расширенная ошибка
dhcErrorConnectionUnavailable
1201&
Устройство является постоянно подключенным, однако в данный момент соединение отсутствует
dhcErrorNoNetwork
1222&
Сеть не обнаружена
dhcErrorNoNetOrBadPath
1203&
Имя удаленного ресурса указано неверно либо ресурс не обнаружен
Для вызова функции WNetGetConnection можно воспользоваться функцией-оболочкой dhGetRemoteName, которая принимает в качестве аргумента локальное имя и возвращает в случае успешного выполнения UNC-путь, а в противном случае строку нулевой длины. Эта функция представлена в примере 6.8.
Пример 6.8. Определение UNC-пути подключенного сетевого диска
Public Function dhGetRemoteName(strLocalName As String) As String
’ По заданному подключенному сетевому ресурсу определяет его UNC-путь.
Dim IngRemoteNameLen As Long
Dim strRemoteName As String
Dim IngReturn As Long
Do
Устанавливает буфер.
strRemoteName = String$(IngRemoteNameLen, vbNullChar)
IngReturn = WNetGetConnection(strLocalName, _ strRemoteName, IngRemoteNameLen)
Цикл продолжается, пока функция не завершится успешно,
или не возникнет любая ошибка, кроме ошибки,
свидетельствующей о том, что буфер слишком мал.
Loop Until IngReturn о dhcErrorMoreData
If IngReturn = dhcNoError Then
dhGetRemoteName = dhTrimNull(strRemoteName)
End If
End Function
В цикле Do. . .Loop функции dhGetRemoteName использована интересная и довольно распространенная методика заполнения выходного буфера. Сначала значение IngRemoteNameLen равно 0, и при первом выполнении цикла строковая переменная strRemoteName устанавливается с помощью функции String таким образом, чтобы она содержала ноль символов:
Do
Определяет буфер строки.
strRemoteName = String$(IngRemoteNameLen, vbNullChar)
Если при вызове функции WNetGetConnection буфер слишком мал для записи возвращаемых данных, функция присваивает параметру IngRemoteNameLen значение необходимой длины буфера и возвращает код ошибки dhcErrorMoreData:
IngReturn = WNetGetConnection(strLocalName, _ strRemoteName, IngRemoteNameLen)
’ Цикл продолжается, пока функция не завершится успешно
или не возникнет любая ошибка, кроме ошибки,
’ свидетельствующей о том, что буфер слишком мал.
Loop Until IngReturn о dhcErrorMoreData
При втором проходе цикла программа устанавливает длину строки strRemoteName достаточной для записи возвращаемого значения. Таким образом, функция WNetGetConnection правильно заполнит буфер и возвратит значение, которое отличается ОТ dhcErrorMoreData.
Вы можете сэкономить время, затрачиваемое на двукратный вызов функции, сразу установив значение IngRemoteNameLen достаточно большим или воспользовавшись методом, который применяется в функции dhGetRemoteName для установки точного размера буфера.
Следующий фрагмент программы является примером использования функции dhGetRemoteName для замены буквы подключенного сетевого диска UNC-путем, если таковой существует:
' Преобразует из "T:\SAMPLE.TXT" в
' ’’\\GATEWAY\JAZ\SAMPLE.TXT”, предполагая, что
' Т:\ соответствует \\GATEWAY\JAZ.
If Mid$(strFilePath, 2, 1) = ":" Then strRemote = dhGetRemoteName(Left$(strFilePath, 2)) If Len(strRemote) > 0 Then
strFilePath = strRemote & Mid$(strFilePath, 3) End I f
End If
Примечание: Вместо WNetGetconnection, которая используется функцией dhGetRemoteName, для определения сетевого пути можно воспользоваться другой функцией Windows API. Она даже автоматически отделяет серверную и дисковую часть UNC-пути (информацию о соединении) от остальной части пути. Эта API функция, WNetGetUniversalName, рассмотрена ниже, в разделе "Функция WNetGetUniversalName".
Определение имени текущего пользователя
У вас могут быть приложения, в которые необходимо интегрировать систему безопасности сети, изменяя интерфейс приложения или функции некоторых элементов в зависимости от текущего пользователя. Неважно, почему это делается: в целях безопасности сети, для поддержки пользовательских установок и настроек, контроля состояния сети или по иным причинам, информация о том, кто в данный момент работает с приложением, имеет большое значение. Эту информацию можно получить посредством функции API WNetGetUser, описание которой выглядит следующим образом:
Private Declare Function WNetGetUser Lib "mpr" Alias _ "WNetGetUserA” (ByVai strName As String, _ ByVai strUserName As String, IngLength As Long) As Long
Предназначение этой функции зависит от того, что задается в качестве ее первого параметра. Если в качестве параметра strName задать пустую строку, буфер strUserName будет содержать имя работающего в данный момент пользователя. Если указать имя ресурса, например подключенный диск, буфер будет содержать имя пользователя (или домен Windows NT и имя пользователя), которое указано при создании соединения. Оба способа вызова этой функции могут быть реализованы посредством функции dhGetUserName, принимающей в качестве необязательного аргумент, который передается как параметр strName в функцию WNetGetUser. Функция dhGetUserName, очень похожая на функцию dhGetRemoteName, приведена в примере 6.9. Константы, чаще всего возвращаемые функцией WNetGetUser, представлены в табл. 6.5.
Пример 6.9. Использование функции WNetGetUser для определения текущего пользователя
Public Function dhGetUserName _
(Optional strLocalName As String = vbNullString) As String 1 Определяет имя текущего пользователя сети.
Dim IngUserNameLen As Long Dim strUserName As String Dim IngReturn As Long
Do
Определяет буфер строки.
strUserName = String? (IngUserNameLen, vbNullChar)
IngReturn = WNetGetUser(strLocalName, strUserName, IngUserNameLen)
Loop Until IngReturn <> dhcErrorMoreData
If IngReturn = dhcNoError Then dhGetUserName = dhTrimNull(strUserName)
End If End Function
Таблица 6.5. Константы функции WNetGetUser
Константа
Значение
Описание
dhcNoError
0&
Вызов функции выполнен успешно
dhcErrorNotConnected
2205&
Указанный ресурс в данный момент не подключен
dhcErrorMoreData
234
Размер буфера слишком мал (параметр длины буфера будет содержать необходимое значение, если функция возвращает эту ошибку)
dhcErrorExtendedError
1208&
Произошла расширенная ошибка
dhcErrorNoNetwork
1222&
Сеть не обнаружена
dhcErrorNoNetOrBadPath
1203&
Имя удаленного ресурса указано неверно либо ресурс не обнаружен
Получение и присвоение имени компьютера
В приложениях, предназначенных для работы в сети, целесообразно иметь воз- можность определять имя рабочей станции, на которой выполняется приложение. Эта информация содержится в реестре и инициализируется при входе в Windows. Хотя эти сведения можно извлечь из реестра, однако, если имя компьютера было изменено (с помощью диалогового окна Параметры сети (Network Properties), функции Windows API или редактора реестра REGEDIT.EXE), компьютер "не узнает" об этом до тех пор, пока не будет перезагружен. Поэтому значение в реестре может не соответствовать действительному имени компьютера.
Win32 API имеет две функции — GetComputerName и SetComputerName — для получения и задания данной информации. Описания этих функций выглядят следующим образом:
Private Declare Function GetComputerName Lib "kernel32" _
Alias "GetComputerNameA" (ByVai strBuffer As String, _
IngSize As Long) As Long
Private Declare Function SetComputerName Lib "kernel32" _
Alias "SetComputerNameA" (ByVai strComputerName As String)_ As Long
Функция GetComputerName принимает в качестве параметра буфер и его размер. Она записывает в буфер имя компьютера и возвращает ненулевое значение в случае успешного завершения. Функция SetComputerName принимает в качестве аргумента новое имя и присваивает его компьютеру. (На самом деле имя не изменится, пока машина не будет перегружена.) Вызвать эти функции API можно с помощью функций-оболочек dhGetComputerName и dhSetComputerName, которые представлены в примере 6.10.
Пример 6.10. Получение и задание имени компьютера
Public Function dhGetComputerName()As String
* Возвращает имя компьютера рабочей станции.
Dim IngReturn As Long
Dim IngBufferSize As Long
Dim strBuffer As String
Делает длину буфера достаточной для имени плюс vbNullChar.
IngBufferSize = dhcMaxComputernameLength + 1
strBuffer = String$(IngBufferSize, vbNullChar)
IngReturn = GetComputerName(strBuffer, IngBufferSize)
If CBool(IngReturn) Then dhGetComputerName = Left(strBuffer, IngBufferSize) End If
End Function
Public Function dhSetComputerName(strComputerName) As Boolean
* Устанавливает имя компьютера рабочей станции.
Dim IngReturn As Long
IngReturn = SetComputerName(strComputerName)
dhSetComputerName = CBool(IngReturn)
End Function
Дополнительные сетевые функции
Основные сетевые функции, рассмотренные в первой части этой главы, в состоянии "удовлетворить” большинство ваших сетевых требований. Однако иногда требуется больше, чем выполнение основных операций. В данном разделе рассмотрены некоторые дополнительные сетевые возможности, которые можно добавить в ваше приложение, а именно:
получение более подробной информации об удаленных ресурсах, чем может предоставить функция WNetGetConnection;
составление списка подключенных сетевых ресурсов;
составление списка доступных сетевых ресурсов совместного использования;
составление списка компьютеров в сети;
использование LAN Manager API.
Функция WNetGetUniversalName
Функция WNetGetUniversalName может возвратить либо UNC-вариант определенного удаленного пути (например, "\\GATEWAY\JAZ\Updates", если задать ей в качестве аргумента "Т: \Updates", при условии, что диск Т подключен к \\GATE- WAY\JAZ), либо структуру данных, в которую записана информация о подключении, включая различные фрагменты строки "\\GATEWAY\JAZ\Updates", что устраняет необходимость самостоятельно анализировать эту строку. Однако вызов данной функции имеет ряд интересных особенностей, которые относятся и ко многим другим функциям API. В этом разделе проанализирована функция WNetGetUniversalName и трудности, связанные с ее вызовом из VBA. Кроме того, данный раздел можно рассматривать как введение в раздел "Составление списков сетевых ресурсов", где рассматриваются вопросы, связанные с созданием перечней сетевых ресурсов при возникновении аналогичных трудностей. К счастью, все эти проблемы разрешимы, и соответствующие функции можно успешно вызывать из VBA.
Проблемы, связанные с буфером
Теоретически все функции WNet должны вызываться так же просто, как и функции, рассмотренные до сих пор. Но практически Microsoft не обеспечила простого вызова некоторых функций WNet из VBA. Проблема заключается в том, что кое-какие функции используют структуры данных, содержащие адреса строковых значений, а не сами значения, что усложняет обмен информацией с этими функциями. Например, ниже представлено описание API-функции WNetGetUniversalName:
Private Declare Function WNetGetUniversalName Lib "mpr" _
Alias "WNetGetUniversalNameA" (ByVai strLocalPath As String, _
ByVai InglnfoLevel As Long, IpBuffer As Any, _ IngBufferSize As Long) As Long
На первый взгляд, параметры IpBuffer и IngBufferSize выглядят точно так же, как и в других функциях, например WNetGetUser. Однако на этот раз буфер будет содержать переменную одного из следующих пользовательских типов:
Private Type UNIVERSAL_NAME_INFO
IpUniversalName As String
End Type
Private Type REMOTE_NAME_INFO
IpUniversalName As String
IpConnectionName As String
IpRemainingPath As String End Type
Проблема состоит в том, что функция API может выделить место для строк, которые используются этими структурами данных, в буфере, следующем непосредственно за самой структурой в памяти, однако строки внутри этих пользовательских структур данных на самом деле являются указателями на буферы, содержащие текст. Поэтому в результате выполнения функции WNetGetUniversalName с использованием структуры remote_name_info будет сформирован буфер, подобный представленному на рис. 6.5.
памяти: 200204;208 >212)/236)254 )
Рис. 6.5. Структура буфера REMOTE_NAME_INFO при выполнении более сложных функций WNet API
Другими словами, если вызов функции выполнен правильно, можно с уверенность сказать, что вся информация находится внутри буфера. Однако, не имея возможности расшифровать, куда указывают указатели структуры remote_name_info, вы не в состоянии выделить эту информацию. Как видно по рис. 6.5, вы не можете даже проанализировать эти три строки самостоятельно, поскольку порядок их следования в буфере неизвестен.
Решение проблем, связанных с буфером
Для решения проблем, связанных с буфером, можно применить несколько "маленьких хитростей". Первая из них заключается в том, чтобы вместо строки или структуры remote_name_info использовать в качестве буфера байтовый массив. Сердцевина функции будет выглядеть следующим образом:
Dim IngReturn As Long
Dim abyt () As Byte
Dim IngBufferSize As Long
Do
ReDim abyt(IngBufferSize)
IngReturn = WNetGetUniversalName(strLocalPath, _
dhcRemoteNamelnfoLevel, abyt(0), IngBufferSize) Loop Until IngReturn <> dhcErrorMoreData
Один из способов задать параметр ingBuf ferSize — просто получить длину структуры remote jsiame_infcl Однако если вы попытаетесь сделать это, вы обнаружите, что ее длина составляет 12 байтов (размер трех длинных целых, которые содержат три указателя типа Long на строки структуры). Поскольку буфер, передаваемый функции WNetGetUniversalName, также должен содержать сами строки, буфер, имеющий длину, равную размеру структуры, никогда не будет достаточно большим! Но программа использует вместо этого байтовый массив, поэтому с помощью оператора ReDim его размер можно сделать таким, как требуется.
При вызове функции WNetGetUniversalName в качестве буфера ей задается первый элемент байтового массива. Поскольку байтовый массив занимает непрерывную область памяти, функция WNetGetUniversalName может писать в него, зная только его первый элемент. В результате вызова функции соответствующие данные будут записаны в байтовый массив, и следующая задача — преобразовать три указателя в три реальные строки. Для этого нужно сделать вот что:
Выполнить обратное преобразование каждой 4-байтовой группы в длинное целое
Существует несколько способов решить данную задачу. Мы выбрали самый простой: возложили эту обязанность на VBA. Посредством оператора VBA LSet вы можете присвоить переменным типа Byte одного пользовательского типа значения типа Byte другого пользовательского типа. Для преобразования группы из 4-х байтов обратно в значение типа Long модуль NETWORK.BAS включает два типа данных и простую функцию, которая выполняет преобразование:
* Временные типы для преобразования данных. Private Type Bytes
Bytel As Byte
Byte2 As Byte
Byte3 As Byte
Byte4 As Byte
End Type
Private Type TempLong
IngValue As Long
End Type
Private Function dhCvtBytesToLong(abyt() As Byte, intlndex As Integer) As Long
* Преобразование группы из 4-х байтов в значение типа Long
посредством оператора LSet.
Dim usrBytes As Bytes
Dim usrLong As TempLong
If UBound(abyt) < intlndex + 3 Then
1 Если в массиве недостаточно байтов, возвращается 0. dhCvtBytesToLong = 0
Else
1 Оператор LSet требует, чтобы с обеих сторон знака
1 равенства находились пользовательские типы данных, поэтому
1 структура заполняется справа.
With usrBytes
.Bytel = abyt(intlndex)
.Byte2 = abyt(intlndex + 1)
.Byte3 = abyt(intlndex + 2)
.Byte4 = abyt(intlndex + 3)
End With
1 Копирование группы из 4-х байтов в структуру, * которая содержит длинное целое.
LSet usrLong = usrBytes
dhCvtBytesToLong = usrLong.IngValue
End If End Function
Функция dhCvtBytesToLong принимает в качестве параметров байтовый массив и индекс в массиве, начиная с которого нужно выполнять копирование. Если, начиная с этого индекса, в массиве имеется 4 байта (т.е. если вы задали приемлемую стартовую позицию), программа копирует данную группу в структуру данных типа Byte, а затем посредством оператора LSet копирует эти байты в структуру TempLong. После того как VBA перенесет все байты и выполнит преобразование, функция возвращает эквивалентное значение типа Long.
Выполнить преобразование адреса в смещение в пределах байтового массива
После выполнения числового преобразования переменная IngPointer содержит адрес определенной строки в памяти, который пока мало чем полезен. Для того чтобы получить доступ к этой строке, необходимо преобразовать данный адрес в смещение в пределах байтового массива. К счастью, это легко сделать: вы можете определить адрес первого элемента массива, а затем вычесть полученное значение из адреса, который содержится в переменной IngPointer. В результате вычитания вы узнаете смещение интересующей нас строки в пределах байтового массива. Например, если IngPointer содержит значение 254, а байтовый массив начинается с адреса 200, то строка начинается с abyt (54). (Те, кто считает, что в VBA нельзя выполнять арифметические действия с указателями, заблуждаются, не так ли?)
Использование функций VarPtr, ObjPtr и StrPtr
VBA 5.0 включает три полезные, однако недокументированные функции: VarPtr, ObjPtr и StrPtr. VarPtr возвращает адрес переменной, которая задана в качестве ее аргумента (т.е. указатель на эту переменную), StrPtr — указатель на строковую переменную, a ObjPtr — указатель на объектную переменную. Эти функции могут быть полезны во многих ситуациях, особенно при обращении к API.
Однако будьте внимательны! Поскольку эти функции не описаны в документации, они являются не поддерживаемой частью библиотеки функций VBA, вследствие чего они могут не войти в состав будущих версий VBA. С другой стороны, некоторые мастера Microsoft используют эти функции, поэтому остаются шансы на то, что они сохранятся и впоследствии.
Имеется несколько способов определения адреса первого элемента байтового массива (включая создание DLL, которая принимает строку или переменную другого типа и возвращает указатель на нее), однако проще всего воспользоваться недокументированной функцией VarPtr. Окончательная формула для определения индекса в массиве, с которого начинается строка, имеет следующий вид:
IngPointer = dhCvtBytesToLong(abyt, intlndex)
Inglndex = IngPointer - VarPtr(abyt(0))
Отыскать конец строки
Наконец, последним шагом является перемещение по массиву, начиная с ingin- dex, до тех пор, пока не попадется нулевой байт (байт со значением 0), указывающий на конец строки, которую вы пытаетесь извлечь. Функция dhConvertByteArrayPartToString принимает в качестве параметров байтовый массив и положение указателя в массиве (в случае с REMOTE_NAME_INFO IpUniversalName равна 0, IpConnectionName равна 4 и IpRemainingPath равна 8), а возвращает указанную строку. В примере 6.11 функция dhConvertByteArrayPartToString приведена полностью.
Пример 6.11. Функция dhConvertByteArrayPartToString
Private Function dhConvertByteArrayPartToString( _ abyt() As Byte, _ intlndex As Integer) As String
Преобразует указанную часть байтового массива в строку.
Dim IngCurrentlndex As Long
Dim IngPointer As Long
Dim IngByteArraySize As Integer
Dim strOut As String
strOut = vbNullString
IngByteArraySize = UBound(abyt)
* Получает значение указателя строки. IngPointer = dhCvtBytesToLong(abyt, intlndex)
Определяет значение индекса в массиве, с которого * начинается искомая строка.
IngCurrentlndex = IngPointer - VarPtr(abyt(0))
* Если индекс имеет допустимое значение, выполнение процедуры продолжается.
If IngCurrentlndex > 0 Then
Цикл до достижения нулевого байта.
Do Until abyt(IngCurrentlndex) = 0 Or _ IngCurrentlndex > IngByteArraySize
strOut = strOut & Chr$(abyt(IngCurrentlndex) )
* Перемещается к следующему символу.
IngCurrentlndex = IngCurrentlndex + 1 Loop End If
dhConvertByteArrayPartToString = strOut End Function
Обе функции dhGetRemotelnfо и dhGetUniversallnfo из примера 6.12 вызывают эту полезную (хотя и несколько необычную) функцию.
Пример 6.12. Два способа вызова функции WNetGetUniversalName
Public Function dhGetRemotelnfо(strLocalPath As String, _ usrRemoteNamelnfо As REMOTE_NAME_INFO) As Long
' Заполняет структуру REMOTE_NAME_INFO
по заданному локальному пути.
Dim IngReturn As Long
Dim abyt() As Byte
Dim IngBufferSize As Long
Do
ReDim abyt(IngBufferSize)
IngReturn = WNetGetUniversalName(strLocalPath, _ dhcRemoteNamelnfoLevel, abyt(O), IngBufferSize) Loop Until IngReturn <> dhcErrorMoreData
If IngReturn = dhcNoError Then
Если вызов выполнен успешно, заполняет ' структуру REMOTE_NAME_INFO.
With usrRemoteNamelnfo
.IpUniversalName = _
dhConvertByteArrayPartToString(abyt, 0)
.IpConnectionName =_
dhConvertByteArrayPartToString(abyt, 4)
.IpRemainingPath = _ dhConvertByteArrayPartToString(abyt, 8) End With
End If
dhGetRemotelnfo = IngReturn
End Function
Public Function dhGetUniversallnfo(strLocalPath As String, _ usrUniversalNamelnfo As UNIVERSAL_NAME_INFO) As Long
Заполняет структуру UNIVERSAL_NAME_INFO
по заданному локальному пути.
Dim IngReturn As Long
Dim abyt() As Byte
Dim IngBufferSize As Long
Do
ReDim abyt(IngBufferSize)
IngReturn = WNetGetUniversalName(strLocalPath, _ dhcUniversalNamelnfoLevel, abyt(0), IngBufferSize) Loop Until IngReturn <> dhcErrorMoreData
If IngReturn = dhcNoError Then
Если вызов выполнен успешно, заполняет ' структуру UNIVERSAL_NAME_INFO.
With usrUniversalNamelnfo
.IpUniversalName = _
dhConvertByteArrayPartToString(abyt, 0)
End With
End If
dhGetUniversallnfo = IngReturn
End Function
Информация, представленная в этом разделе относительно функции dhConvert- ByteArrayPartToString, пригодится вам при обсуждении способов составления списков сетевых ресурсов в следующем разделе, поскольку сетевые функции API, которые применяются при составлении списков, используют буферы абсолютно так же, как функция WNetGetUniversalName.
Например, для вызова этих двух функций можно воспользоваться следующим фрагментом программы:
Sub TestUniversal(strPath As String)
Dim uni As UNIVERSAL_NAME_INFO
Dim rni As REMOTE_NAME_INFO
Call dhGetUniversallnfo(strPath, uni)
Call dhGetRemotelnfo(strPath, rni)
Debug.Print "Universal Name : ” & uni.IpUniversalName
Debug.Print
Debug.Print "Connection Name: " & rni.IpConnectionName
Debug.Print "Remaining Path : " & rni.IpRemainingPath
Debug.Print "Universal Name : " & rni.IpUniversalName End Sub
Для пути типа T:\Updates, где вы подключаете диск Т к \\GATEWAY\JAZ, результаты будут выглядеть так:
Universal Name : \\GATEWAY\jaz\Updates
Connection Path: \\GATEWAY\jaz
Remaining Path : \Updates
Universal Name : \\GATEWAY\jaz\Updates
Таким образом, эти функции избавляют вас от необходимости анализировать полный UNC-путь.
Составление списков сетевых ресурсов
При обсуждении в этой главе вопросов, связанных с подключением к сети, отключением от сети и получением информации о сетевых ресурсах, предполагалось, что вы и ваши пользователи знают, какие устройства доступны. Кроме dhCon- nectoig, не представлены другие интерфейсы, позволяющие выбрать устройство для работы. Как поступить в том случае, если вы хотите предоставить пользователям собственный интерфейс для выбора сетевых устройств? Для этого необходимо составить список доступных устройств с помощью функций Windows API.
Процесс составления списка сетевых устройств состоит из трех этапов:
Получение дескриптора списка по заданной отправной точке.
При наличии дескриптора списка просмотр всех имеющихся ресурсов и выполнение необходимых операций с ними. Обычно нужно только получить имя устройства.
Закрытие дескриптора списка.
Windows API предоставляет три функции, которые реализуют указанные этапы: WNe'tOpenEnum, WNetEnumResource и WNetCloseEnum. Для составления списка устройств понадобится вызвать каждую из них. Дескриптор списка можно представить себе как канал, по которому API передает информацию об устройствах, и прежде чем у вас появится возможность составить список устройств, вы должны получить от Windows дескриптор списка. Для этого следует вызвать функцию WNe tOpenEnum, задав ей несколько флагов, определяющих тип ресурсов, которые необходимо включать в список, а она возвратит дескриптор списка. Этот дескриптор используется для получения списка устройств с помощью функции WNetEnumResource, которая вызывается многократно, пока не будут просмотрены все доступные сетевые ресурсы, при этом информация о каждом ресурсе записывается в структуру netresource. Завершив составление списка устройств, нужно уничтожить дескриптор списка посредством функции WNetCloseEnum. Хотя подобная методика может быть и не самая идеальная для VBA, альтернативой для Windows было бы разрешить вам указать функцию обратного вызова, которую VBA вызывал бы по мере обнаружения требуемых ресурсов. Поскольку VBA не обеспечивает прямой поддержки функций обратного вызова, эта альтернатива еще менее привлекательна для программистов на VBA.
В следующих разделах рассмотрен процесс формирования списка доступных сетевых ресурсов С использованием функций WNetOpenEnum, WNetEnumResource и WNetCloseEnum.
Получение дескриптора списка
Получение дескриптора списка — наиболее важный этап процесса составления списка сетевых ресурсов, поскольку эта единственная возможность сообщить Windows, какие именно ресурсы вас интересуют. Поскольку вы можете составлять список всего, что угодно (от соединений, установленных в данный момент на вашей машине, до совместно используемых сетевых ресурсов на другой машине), от способа, которым вы осуществляете вызов функции WNetOpenEnum, зависит, какую именно информацию вы получите от Windows. При вызове WNetOpenEnum необходимо указать следующие сведения:
область составления списка;
тип ресурсов, включаемых в список;
характер использования ресурсов, включаемых в список.
Возможные значения каждого параметра приведены в табл. 6.6—6.7.
Таблица 6. б. Константы области составления списка
Константа
Значение
Описание
dhcResourceConnected
&H1
Все подключенные в данный момент ресурсы
dhcResourceGlobalNet
&H2
Все ресурсы в сети
dhcResourceRemembered
&H3
Все постоянные ресурсы
Таблица 6.7. Константы типа ресурсов, вносимых в список
Константа
Значение
Описание
dhcResourceTypeAny
&H0
Все типы ресурсов
dhcResourceTypeDisk
&H1
Все дисковые ресурсы
dhcResourceTypePrint
&H2
Все принтерные ресурсы
Таблица 6.8. Константы, задающие характер использования ресурсов, вносимых в список
Константа
Значение
Описание
dhcResourceUsageAll
&H0
Все ресурсы
dhcResourceUsageConnectable
&H1
Все подключаемые ресурсы
dhcResourceUsageContainer
&H2
Все ресурсы-контейнеры
Константы области составления списка сообщают Windows о том, где выполнять поиск ресурсов. Для задания области поиска следует указать соответствующее значение из табл. 6.6. Если задать константу dhcResourceConnected или dhcRe- sourceRemembered, список формируется в пределах устройств одного компьютера. (Кроме того, игнорируется значение третьего параметра, поскольку он применим только для глобального поиска в масштабах всей сети.)
Назначение констант, указывающих тип ресурсов, очевидно: они позволяют просматривать устройства только определенного типа, например диски или принтеры. Кроме того, если сервер, на котором составляется список, поддерживает совместное использование других ресурсов, к примеру COM-портов, эти ресурсы будут отображаться только в том случае, когда вы зададите просмотр всех ресурсов, используя константу dhcResourceTypeAny. Если операционная система не различает типы ресурсов, она возвратит список устройств всех типов и проигнорирует флаг типа ресурсов.
Константы, задающие характер использования ресурсов, применимы при задании константы области поиска dhcResourceGlobalNet. Все сводится к тому, что к некоторым ресурсам (например, к совместно используемым сетевым ресурсам) можно подключаться, а некоторые ресурсы содержат другие ресурсы. Выбрав нужный флаг из табл. 6.8, вы указываете тип глобальных ресурсов, которые вы хотите включить в список. Здесь допустима такая аналогия: если ресурс представляет собой контейнер, вы можете снова использовать его для вызова функции WNetOpenEnum, чтобы посмотреть, какие ресурсы содержатся внутри этого контейнера. Если ресурс является подключаемым, вы можете передать строку, содержащую имя удаленного устройства, прямо в функцию WNetAddConnection2 (или в нашу функцию-оболочку dhAddConnection2) и подключиться к ресурсу.
VAJnpl Совет: Далее приведены детали, касающиеся использования низкоуровне- I I Ц вых функций составления списков. В разделе "Собираем все вместе" пред- ставлено несколько функций-оболочек, которые упрощают составление списка ресурсов за счет того, что вам не приходится работать с дескрипторами напрямую. Скорее всего, вы согласитесь, что использовать функции-оболочки проще, и захотите применять их вместо функций WNetEnum, однако сначала разберитесь с особенностями операций низкого уровня.
Ниже представлено описание функции WNetOpenEnum:
Private Declare Function WNetOpenEnum Lib "mpr" Alias _ "WNetOpenEnumA" (ByVai IngScope As Long, _ ByVai IngType As Long, ByVai IngUsage As Long, _ IpNetResource As Any, IngEnum As Long) As Long
Параметр IpNetResource может быть либо переменной типа NETRESOURCE, либо значением Null. Тип netresource рассматривается в разделе "Подключение к сетевому ресурсу". Определение переменной выглядит следующим образом: Private Type NETRESOURCE dwScope As Long dwType As Long dwDisplayType As Long dwUsage As Long IpLocalName As String IpRemoteName As String IpComment As String IpProvider As String End Type
Эта структура содержит информацию об области составления списка, о типе и использовании сетевого ресурса, а также о типе отображения. Список возможных значений данного параметра приведен в табл. 6.9.
Таблица 6.9. Константы типа отображения структуры NETRESOURCE
Константа
Значение
Описание
dhcResourceDisplayTypeGeneric
&H0
Тип отображения не имеет значения
dhcResourceDisplayTypeDomain
&H1
Объект должен отображаться как домен
dhcResourceDisplayTypeServer
&H2
Объект должен отображаться как сервер
dhcResourdeDisplayTypeShare
&H3
Объект должен отображаться как совместно используемый сетевой ресурс
Если передать значение Null параметру IpNetResource, составление списка начнется с корня сети. Если задать в качестве этого параметра структуру netresource, вы можете составлять список ресурсов конкретного сетевого ресурса, например определенного диска. Другими словами, когда элемент dwUsage структуры NETRESOURCE имеет значение dhcResourceUsageContainer, вы можете также вызывать функцию WNetOpenEnum с заполненной структурой NETRESOURCE. (Вы нс имеете возможности составить список ресурсов того ресурса, который не содержит других ресурсов, например принтера.)
Существует два способа использования функции WNetOpenEnum:
вызвать се дважды, передав ей при первом обращении Null (после получения списка всех доступных ресурсов можно использовать любой ресурс и вызвать функцию WNetOpenEnum снова, воспользовавшись информацией о конкретном ресурсе);
если вы уже знаете имя ресурса, для которого хотите составить список, можно вызвать функцию WNetOpenEnum один раз (если вы в состоянии самостоятельно
заполнить структуру NETRESOURCE, нет необходимости вызывать функцию WNetOpenEnum дважды).
Для того чтобы показать два способа использования функции WNetOpenEnum, мы предоставили две функции-оболочки — dhGetTopLevelEnumHandle и dhGetNetResourceEnumHandle. Первая из них позволяет получить дескриптор списка для всей сети, а вторая — дескриптор для определенного устройства-контейнера. Обе функции представлены в примере 6.13.
Примечание: Помните, что функция WNetOpenEnum возвращает дескриптор списка, который можно использовать для составления списка ресурсов (т.е. для получения информации обо всех указанных сетевых ресурсах). Однако для этого необходимо воспользоваться функцией API WNetEnum- Resource, рассмотренной в разделе "Составление списка ресурсов". Примеры, приведенные в этом разделе, возвращают дескрипторы, которыми можно воспользоваться впоследствии для составления списка ресурсов.
Пример 6.13. Получение дескриптора списка с помощью функции WNetOpenEnum
Public Function dhGetTopLevelEnumHandle(IngScope As Long, _
IngType As Long, IngUsage As Long) As Long
’ Открывает дескриптор списка сетевых ресурсов верхнего уровня.
Dim IngReturn As Long
Dim hEnum As Long
' Поскольку это элемент верхнего уровня, в качестве
’ четвертого параметра используется значение Null.
IngReturn = WNetOpenEnum(IngScope, IngType, IngUsage, _ Null, hEnum)
If IngReturn = dhcNoError Then dhGetTopLevelEnumHandle = hEnum End If
End Function
Public Function dhGetNetResourceEnumHandle(IngScope As Long, _ IngType As Long, IngUsage As Long, usrNetResource _ As NETRESOURCE) As Long
’ Открывает дескриптор списка ресурсов для заданного NetResource.
Dim IngReturn As Long
Dim hEnum As Long
IngReturn = WNetOpenEnum(IngScope, IngType, IngUsage, _ usrNetResource, hEnum)
If IngReturn = dhcNoError Then dhGetNetResourceEnumHandle = hEnum
End If
End Function
Давайте рассмотрим несколько примеров применения этой функции. Простейшим примером является вызов функции dhGetTopLevelEnumHandle для поиска всех подключенных в данный момент сетевых дисков. (Заметим, что в Windows NT некоторые диски могут быть подключены, однако могут не иметь связанной с ними буквы диска.) Вызов функции для открытия дескриптора списка выглядит следующим образом:
hEnum = dhGetTopLevelEnumHandle(dhcResourceConnected, _ dhcResourceTypeDisk, dhcResourceUsageAll)
Чтобы получить дескриптор списка для всей сети (в диалоговом окне подключения Windows NT над всеми доменами расположен узел Microsoft Windows Network), необходимо:
hEnum = dhGetTopLevelEnumHandle(dhcResourceGlobalNet, _ dhcResourceTypeAny, dhcResourceUsageAll)
После вызова dhGetTopLevelEnumHandle вы получаете возможность исследовать все сетевые ресурсы с помощью функции WNetEnumResource. Для любого ресурса, который является контейнером, например домен или сервер, можно вызвать функцию dhGetNetResourceEnumHandle и составить список содержащихся в нем ресурсов. Нечто подобное делает диалоговое окно подключения к сети Windows NT для отображения структуры ресурсов в нижней половине окна (рис. 6.1).
Например, если вы хотите просмотреть совместно используемые сетевые диски на другом компьютере, зная его имя, можно воспользоваться следующим фрагментом программы для открытия дескриптора списка:
Dim usrNetResource As NETRESOURCE
Dim hEnum As Long
With usrNetResource
.dwDisplayType = dhcResourceDisplayTypeServer
.dwScope = dhcResourceGlobalNet
.dwUsage = dhcResourceUsageContainer
.dwType = dhcResourceTypeDisk
.IpRemoteName = "\\YourServer"
End With
hEnum = dhGetNetResourceEnumHandle(dhcResourceGlobalNet, _ dhcResourceTypeDisk, dhcResourceUsageConnectable, usrNetResource)
Затем этот дескриптор можно применять для получения всех доступных совместно используемых сетевых ресурсов на сервере, имя которого вы укажете. А если вы хотите получить имена всех компьютеров определенного домена, воспользуйтесь такой программой:
Dim usrNetResource As NETRESOURCE
Dim hEnum As Long
With usrNetResource
.dwDisplayType = dhcResourceDisplayTypeDomain
.dwScope = dhcResourceGlobalNet
.dwUsage = dhcResourceUsageContainer
.dwType = dhcResourceTypeDisk
.IpRemoteName = ”WYourDomain”
End With
hEnum = dhGetNetResourceEnumHandle(dhcResourceGlobalNet, _ dhcResourceTypeDisk, dhcResourcellsageContainer, usrNetResource)
Этот дескриптор позволит вам составить список всех компьютеров в сети, заданной именем домена. Программа из раздела "Составление списка ресурсов" показывает, как используются дескрипторы списка, которые вы открыли.
Закрытие дескриптора списка
Вы должны всегда закрывать то, что открываете (золотое правило, которое применимо как в жизни, так и в программировании), и дескрипторы списка нс являются исключением. Для этого используется API-функция WNetcloseEnum. Ес описание выглядит следующим образом:
Private Declare Function WNetcloseEnum Lib ’’mpr.dll’’ _
(ByVai hEnum As Long) As Long
С целью сохранения единообразия мы предоставляем оболочку и для этой функции, хотя ее непосредственный вызов осуществляется очень просто. Функция dhcl-oseEnum представлена в примере 6.14.
Пример 6.14. Использование функции dhcloseEnum для закрытия дескриптора списка
Public Function dhcloseEnum(hEnum As Long) As Long
’ Закрывает дескриптор списка ресурсов.
dhcloseEnum = WNetcloseEnum(hEnum) End Function
По завершении работы дескрипторы лучше закрывать. Теоретически, даже если вы не закроете их, они будут закрыты автоматически при выходе из базового приложения, в котором они открыты (Word, Excel, Access и т.д.), однако закрывать дескрипторы вручную все же предпочтительнее, так как сразу же освобождается память.
Составление списка ресурсов
Наконец, пришло время использовать дескрипторы списка для получения информации! В данном разделе рассказывается о функции API WNetEnumResource, которая является "рабочей лошадкой". Она принимает дескриптор списка от функции WNetOpenEnum и с его помощью находит информацию обо всех доступных сетевых ресурсах.
Описание WNetEnumResource выглядит следующим образом:
Private Declare Function WNetEnumResource Lib "mpr.dll" _
Alias "WNetEnumResourceA" (ByVai hEnum As Long, _
IngCount As Long, IpBuffer As Any, IngBufferSize As Long) _ As Long
Параметр hEnum представляет собой дескриптор, полученный от функции WNetOpenEnum. (Для получения дескриптора можно воспользоваться функциями dhGetNetResourceEnumHandle или dhGetTopLevelEnumHandle.) Параметр IngCount указывает число элементов, которое вы хотите получить. При попытке получения нескольких элементов за один раз мы наблюдали непредсказуемые результаты, поэтому
функция-оболочка dhEnumNext запрашивает каждый раз только один элемент. Как оказалось, при запросе нескольких элементов за один раз не произойдет существенного повышения быстродействия программы, поскольку WNetEnumResource, в отличие от WNetOpenEnum, и так выполняет свою работу быстро.
Параметры IpBuffer и IngBufferSize идентичны параметрам, которые рассматривались при описании функции API WNetGetUniversalName. Как говорилось в разделе "Как решить проблемы, связанные с буфером", для получения строковых значений из IpBuffer необходимо воспользоваться функцией dhConvertByteAr- rayPartToString.
Сердцевиной функции dhEnumNext является тот же цикл Do. . . Loop, используемый для некоторых других вызовов функций WNet:
’ Инициализация буфера байтового массива. ReDim abyt(0)
Do
’ Задание переменной длины буфера.
IngBufferLength = UBound(abyt)
IngReturn = WNetEnumResource(hEnum, 1, _
abyt(0), IngBufferLength)
* Устанавливает размер буфера, если он оказался недостаточно
* большим. (А это произойдет почти наверняка, поскольку
’ мы начали всего с одного байта!)
If IngReturn = dhcErrorMoreData Then
ReDim abyt(IngBufferLength)
End If
' Цикл продолжается, пока функция не завершится успешно
' или не возникнет любая ошибка,
' кроме той, которая показывает, что буфер слишком мал.
Loop Until IngReturn о dhcErrorMoreData
Вторая часть функции dhEnumNext берет элементы из байтового массива и помещает их в структуру netresource. Прежде чем сделать это, давайте еще раз посмотрим на определение структуры netresource:
Private Type NETRESOURCE
dwScope As Long
dwType As Long
dwDisplayType As Long
dwUsage As Long
IpLocalName As String
IpRemoteName As String
IpComment As String
IpProvider As String
End Type
Например, если вы составляете список подключенных сетевых ресурсов (логических обозначений дисков соответствующих UNC-путей), то при получении буфера IpBuffer из функции WNetEnumResource, его структура может быть такой, как показано на рис. 6.6.
Рис. 6.6. Буфер Ip Buffer после заполнения данными структуры Net Resource
Первые 16 байтов заняты четырьмя значениями типа Long структуры NETRESOURCE (dwScope, dwType, dwDisplayType и dwUsage). Программа использует функцию dhCvtBytesToLong для преобразования каждой последовательности из 4 байтов в длинное целое.
Следующие 16 байтов также заняты четырьмя значениями типа Long, однако каждое из них является указателем на строку. Чтобы преобразовать каждое значение, можно воспользоваться функцией dhConvertByteArrayPartToString. Для преобразования всего байтового массива в структуру NETRESOURCE функция dhEnumNext использует следующий фрагмент:
With usrNetResource
.dwScope = dhCvtBytesToLong(abyt, 0)
.dwType = dhCvtBytesToLong(abyt, 4)
.dwDisplayType = dhCvtBytesToLong(abyt, 8)
.dwUsage = dhCvtBytesToLong(abyt, 12)
.IpLocalName = dhConvertByteArrayPartToString(abyt, 16)
.IpRemoteName = dhConvertByteArrayPartToString(abyt, 20)
.IpComment = dhConvertByteArrayPartToString(abyt, 24)
.IpProvider = dhConvertByteArrayPartToString(abyt, 28) End With
В примере 6.15 приведен полный текст функции dhEnumNext.
Пример 6.15. Составление списка с помощью функции dhEnumNext
Public Function dhEnumNext (hEnum As Long, _ usrNetResource As NETRESOURCE) As Long
’ Включает в hEnum следующий ресурс и записывает
информацию в usrNetResource.
Dim IngReturn As Long
Dim IngBufferLength As Long
Dim abyt() As Byte
Инициализирует буфер байтового массива.
ReDim abyt(0)
Do
’ Устанавливает переменную длины буфера.
IngBufferLength = UBound(abyt)
IngReturn = WNetEnumResource(hEnum, 1, abyt(0), _
IngBufferLength)
' Устанавливает размер буфера, если он оказался недостаточно
* большим. (А это произойдет почти наверняка, поскольку
’ мы начали всего с одного байта!)
If IngReturn = dhcErrorMoreData Then
ReDim abyt(IngBufferLength)
End If
' Цикл продолжается, пока функция не завершится успешно
’ или не возникнет любая ошибка, кроме той, ' которая показывает, что буфер слишком мал.
Loop Until IngReturn о dhcErrorMoreData
If IngReturn = dhcNoError Then
’ В результате успешного выполнения получается байтовый массив,
’ заполненный реальными данными и указателями на данные.
' Вся эта информация помещается в структуру NetResource, ' которая задана при вызове процедуры.
With usrNetResource
.dwScope = dhCvtBytesToLong(abyt, 0)
.dwType = dhCvtBytesToLong(abyt, 4)
.dwDisplayType = dhCvtBytesToLong(abyt, 8)
.dwUsage = dhCvtBytesToLong(abyt, 12)
.IpLocalName = dhConvertByteArrayPartToString(abyt, 16)
.IpRemoteName = dhConvertByteArrayPartToString(abyt, 20)
.IpComment = dhConvertByteArrayPartToString(abyt, 24)
.IpProvider = dhConvertByteArrayPartToString(abyt, 28)
End With
End If
dhEnumNext = IngReturn
End Function
В некоторых приложениях вам придется добавлять в список сетевые ресурсы по одному до тех пор, пока вы не обнаружите тот, который ищете. У вас могут быть какие-то другие причины для внесения сетевых ресурсов в список один за другим. Однако в большинстве случаев нужно указать тип необходимых ресурсов и в результате выполнения функции получить массив структур netresource, удовлетворяющих заданному критерию. В следующем разделе представлена функция-оболочка, которая выполняет эти операции.
Собираем все вместе
Функция-оболочка dhGetNetResourcelnfo выполняет всю работу по вызову функций составления списка вместо вас. Эта функция принимает параметры, которые содержатся в таблице ниже.
Параметр
Описание
LngScope
Одно из значений, приведенных в табл. 6.6
LngType
Одно из значений, приведенных в табл. 6.7
LngUsage
Одно из значений, приведенных в табл. 6.8
UsrNetResource
Массив структур netresource (на выходе функции этот массив заполняется данными структур netresource, которые соответствуют требуемым параметрам)
FSpecifyStart
Значение типа Boolean, которое сообщает функции, будет ли передан следующий параметр (usrNetResourceStart); должно быть False, если составление списка нужно начать с корня сети, и True, если вы хотите передать usrNetResourceStart в качестве корня для составления списка
UsrNetResourceStart
Структура типа netresource, определяющая уровень сети, начиная с которого составляется список; этот параметр должен быть задан, даже когда значение параметра fspecifyStart равно False, однако в этом случае элементы структуры могут быть пусты, поскольку они все равно нс используются
Описание
Параметр
Полный текст функции приведен в примере 6.16.
Пример 6.16. Все операции выполняются функцией dhGetNetResourcelnfo
Public Function dhGetNetResourcelnfо(IngScope As Long, _
IngType As Long, IngUsage As Long, _ usrNetResource() As NETRESOURCE, _ fSpecifyStart As Boolean, usrNetResourceStart As NETRESOURCE) _ As Boolean
Возвращает все структуры NETRESOURCE, заданные параметрами.
Dim hEnum As Long
Dim IngCount As Integer
Dim IngReturn As Long
Если параметр fSpecifyStart имеет значение True, информация в ' usrNetResourceStart используется в качестве первого уровня
списка. В противном случае поиск начинается с верхнего уровня. If fSpecifyStart Then
hEnum = dhGetNetResourceEnumHandle(IngScope, IngType, _ IngUsage, usrNetResourceStart)
Else
hEnum = dhGetTopLevelEnumHandle(IngScope, IngType, _ IngUsage)
End If
If hEnum <> 0 Then
Do While IngReturn = dhcNoError
ReDim Preserve usrNetResource(IngCount)
IngReturn = dhEnumNext(hEnum, _ usrNetResource(IngCount))
По завершении поиска значением переменной IngReturn ' будет константа dhcErrorNoMoreltems. При этом ' следует удалить последний элемент массива, 1 поскольку константа никогда не заполняется. If IngReturn = dhcErrorNoMoreltems Then ReDim Preserve usrNetResource(IngCount - 1) End If IngCount = IngCount + 1 Loop IngReturn = dhCloseEnum(hEnum) dhGetNetResourcelnfo = True
Else
* Дескриптор не был открыт. dhGetNetResourcelnfo = False End If End Function
Эта функция выполняет все операции по открытию и закрытию дескрипторов и заполняет массив структур netresource, который вы ей задаете. Любой из элементов массива можно передать функции dhGetNetResourcelnfo, если его компонент dwUsage содержит константу dhcResourceUsageContainer, указывающую, что данный элемент сети сам является контейнером для других сетевых ресурсов.
В примере 6.17 представлены три варианта вызова функции dhGetNetResourcelnfo. В первом случае формируется список всех компьютеров в данном домене (dhcSampleEnumPCsInDomain), во втором — список всех сетевых совместно используемых ресурсов, доступных на данном компьютере (dhSampleEnumSharesOnPC), а в третьем — список всех подключенных к данному компьютеру сетевых ресурсов (dhSampleEnumConnectedResourcesOnLocalPC). Различие между этими примерами заключается в задании определенных параметров и, возможно, в значениях, помещаемых в параметр usrNetResourceStart (в зависимости от вызываемой функции).
!!Примечание: Во всех примерах функция dhGetNetResourcelnfo получает пустой массив структур netresource. Функция заполняет массив, и в результате ее выполнения массив будет содержать по одному элементу для каждого обнаруженного сетевого ресурса, который соответствует критериям, заданным при вызове функции. В каждом примере производится просмотр в цикле всех элементов массива с отображением их содержимого в
!
!
окне отладки.
Пример 6.17. Функции, вызывающие dhGetNetResourcelnfo
Public Sub dhSampleEnumPCsInDomain(strDomainName As String) ' Определяет все компьютеры в данном домене. Например, * dhSampleEnumPCsInDomain "Warburton" * собирает список всех компьютеров в домене "Warburton".
Dim usrNetResource() As NETRESOURCE
Dim usrNetResourceStart As NETRESOURCE
Dim i As Integer
With usrNetResourceStart
.dwScope = dhcResourceGlobalNet
.IpRemoteName = strDomainName End With
If dhGetNetResourcelnfо(dhcResourceGlobalNet, _ dhcResourceTypeDisk, dhcResourceUsageContainer, _ usrNetResource (), True, _ usrNetResourceStart) Then
For i = LBound(usrNetResource) To UBound(usrNetResource) Debug.Print usrNetResource(i).IpRemoteName
Next i
End If
End Sub
Public Sub dhSampleEnumSharesOnPC(strMachineName As String)
* Определяет все совместно используемые
* сетевые ресурсы данного компьютера. Например,
dhSampleEnumSharesOnPC "Marge"
собирает список всех сетевых ресурсов компьютера "Marge".
Dim usrNetResource() As NETRESOURCE
Dim usrNetResourceStart As NETRESOURCE
Dim i As Integer
With usrNetResourceStart
.dwScope = dhcResourceGlobalNet
.IpRemoteName = Ilf(Left$(strMachineName, 2) = "\\", _ vbNullString, "W) & strMachineName
End With
If dhGetNetResourcelnfо(dhcResourceGlobalNet, _ dhcResourceTypeDisk, dhcResourceUsageConnectable, _ usrNetResource (), True, usrNetResourceStart) Then
For i = LBound(usrNetResource) To UBound(usrNetResource) Debug.Print usrNetResource(i).IpRemoteName
Next i
End If
End Sub
Public Sub dhSampleEnumConnectedResourcesOnLocalPC()
* Определяет все подключения к текущему компьютеру. Например,
* dhSampleEnumConnectedResourcesOnLocalPC
’ собирает список всех локальных и удаленных имен для всех
подключенных совместно используемых устройств.
Dim usrNetResource() As NETRESOURCE
Dim usrNetResourceStart As NETRESOURCE
Dim i As Integer
If dhGetNetResourcelnfо(dhcResourceConnected, _ dhcResourceTypeDisk, 0, usrNetResource(), False, _ usrNetResourceStart) Then
For i = LBound(usrNetResource) To UBound(usrNetResource) Debug.Print usrNetResource(i).IpLocalName & _
” подключен к ” & usrNetResource(i).IpRemoteName Next i
End If
End Sub
Вы убедились, что функция WNetEnumResource исключительно мощная: она может выполнять множество различных операций и возвращает большой объем полезной информации. К сожалению, наш обзор лишь поверхностно затрагивает возможности данной функции, но для начала этого достаточно.
Использование диалогового окна просмотра из библиотеки Shell32
Функции библиотеки Shell32 позволяют формировать диалоговое окно, которое нужно для просмотра каталогов на локальном компьютере и на всех компьютерах сети или совместно используемых сетевых ресурсов. Это диалоговое окно применяется в некоторых приложениях Windows, например в Windows 95 Network Monitor и в редакторе реестра (рис. 6.7). Его можно вызвать, открыв редактор реестра и выбрав команду Подключение сетевого реестра (Connect Network Registry) из меню Реестр (Registry), а затем щелкнув на кнопке Просмотр (Browse).
Обзор компьюг грог
BillieBillieв^ в-в^ в-Выберите компьютер, к которому необходимо подключиться.
Billie
Billie
в^ в-
в^ в-
-^ Вся сеть
'Ш A^en2
•Ш Cad2
■^ Dragon2
■Д, Intruder м Kiselev
Ш Multi3"S Scanet
j Отмена |
Рис. 6.7. Диалоговое окно просмотра из библиотеки Shell32
Если вы уверены, что ваше VBA-приложение будет работать только с Windows 95 или NT 4.0 (в Windows NT 3.51 это диалоговое окно недоступно), вы можете спокойно
использовать данное окно в своих программах. Ниже перечислены три необходимые для этого функции API:
Private Declare Function SHGetSpecialFolderLocation _
Lib "Shell32.DLL" _
(ByVai hwndOwner As Long, ByVai IngFolder As Long, _
ByRef idl As Long) As Long
Private Declare Function SHGetPathFromIDList Lib "Shell32.DLL" _
(ByVai idl As Long, ByVai Path As String) As Integer
Private Declare Function SHBrowseForFolder Lib "Shell32.DLL" _
(ByRef bi As Browseinfo) As Long
Эти функции, работая вместе, позволяют просмотреть сетевой ресурс любого типа с помощью одного и того же интерфейса. Данные функции можно вызывать непосредственно или использовать предлагаемую функцию-оболочку dhBrowseForFolder (пример 6.18). В следующих нескольких абзацах перечислены этапы выполнения функции dhBrowseForFolder, что поможет вам в написании собственных функций, использующих общее диалоговое окно просмотра.
В дополнение к трем показанным выше функциям API необходимо применять структуру данных Browseinfo:
Private Type Browseinfo
hwndOwner As Long
pidlRoot As Long
strDisplayName As String
strTitle As String
ulFlags As Long
’ Следующие поля из VBA не используются.
Ipfn As Long
IParam As Long
ilmage As Long
End Type
Работа с этими функциями выполняется в четыре этапа. (Здесь используются фрагменты функции dhBrowseForFolder из примера 6.18.)
Необходимо указать корневой объект, выбрав его из списка элементов табл. 6.10.
Для определения внутреннего значения данного объекта нужно вызвать функцию SHGetSpecialFolderLocation, задав ей выбранную константу. Этот идентификатор называется IDList объекта (или IDL).
If SHGetSpecialFolderLocation(hWnd, IngCSIDL, IngIDL) = 0 Then
Получив допустимое значение IDL, вы должны заполнить пользовательскую структуру данных Browserinfo. Наряду с IDL в нее могут входить флаги из табл. 6.11, объединенные с помощью оператора Or. Эти флаги определяют, какие объекты должны отображаться в диалоговом окне.
With usrBrws
.hwndOwner = hWnd
.pidlRoot = IngIDL
.strDisplayName = String$(dhcMaxPath, vbNullChar)
.strTitle = strTitle
.ulFlags = IngBifFlags
End With
Теперь можно вызывать функцию SHBrowseForFolder, передав ей только что заполненную структуру Browseinfo:
* Открыть диалоговое окно просмотра.
IngIDL = SHBrowseForFolder(usrBrws)
В результате выполнения функции SHBrowseForFolder компонент strDisplayName структуры Browse Info будет содержать имя, выбранное в диалоговом окне. Если вы хотите получить полный путь выбранного объекта (а, скорее всего, это так), вы должны сначала создать буфер достаточно большого размера для записи пути максимальной длины, а затем вызвать функцию SHGetPath- FromlDList. Она принимает возвращаемое функцией SHBrowseForFolder значение и преобразует его в полный путь выбранного объекта:
strFolder = String$(dhcMaxPath, vbNullChar)
* Пытается преобразовать IngIDL в реальный путь.
If SHGetPathFromIDList(IngIDL, strFolder) Then
* Обрезает символы Null.
strFolder = dhTrimNull(strFolder)
IngReturn = dhcNoError
Else
* Реальный путь каталога недоступен, поэтому, по крайней
* мере, возвращает выбранное имя.
strFolder = dhTrimNull(usrBrws.strDisplayName)
IngReturn = dhcNoError
End If
Список констант, которые можно использовать при задании корневого каталога для диалогового окна, приведен в табл. 6.10. (Обратите внимание, что в отличие от диалогового окна Открытие файла (File Open) вы не можете подняться выше корневого каталога, поэтому будьте внимательны при выборе начальной точки!) Константы, ограничивающие возможность выбора, представлены в табл. 6.11. Если выбирать какой-либо объект, отображаемый в диалоговом окне, недопустимо, кнопка ОК становится недоступной.
Таблица 6.10. Константы, определяющие корневой каталог в диалоговом окне просмотра
Константа
Значение
Описание
dhcCSldlDesktop
&H0
Основной (виртуальный) каталог рабочего стола
dhcCSldlPrograms
&H2
Каталог файловой системы, соответствующий меню Программы (Programs)
dhcCSldlControlPanel
&H3
Виртуальная папка, содержащая пиктограммы панели управления
dhcCSldllnstalledPrinters
&H4
Виртуальная папка, содержащая установленные принтеры
dhcCSldlPersonal
&H5
Личная папка пользователя
dhcCSldlFavorites
&H6
Папка избранного пользователя
Константа
Значение
Описание
dhcCSldlStartupPmGroup
&H7
Группа автозагрузки пользователя в меню
Программы
dhcCSldlRecentDocDir
&H8
Каталог последних документов
dhcCSldlSendToItemDir
&H9
Объекты, перечисленные в подменю Отправить (SendTo) меню Файл (File) Windows Explorer
dhcCSldlRecycleBin
&HA
Корзина
dhcCSldlStartMenu
&HB
Корневой каталог стартового меню (открывается после нажатия кнопки Пуск
(Start))
dhcCSldlDesktopDirectory
&H10
Каталог рабочего стола файловой системы
dhcCSldlMyComputer
&H11
Виртуальный каталог Мой компьютер (Му
Computer)
DhcCSldlNetworkNeighborhood
&H12
Виртуальный каталог Сетевое окружение (Network Neighborhood)
DhcCSldlNetHoodFileSystemDir
&H13
Каталог NetHood файловой системы
dhcCSldlFonts
&H14
Каталог шрифтов
dhcCSldlTemplates
&H15
Каталог шаблонов новых документов
Таблица 6.11. Константы ограничения выбора из библиотеки Shell32 в диалоговом окне просмотра
Константа
Значение
Описание
dhcBifReturnAll
&H0
Нет ограничений
DhcBifReturnOnlyFileSys- temDirs
&H1
Могут быть выбраны только те объекты, которые можно отнести к реальным каталогам
dhcBifDontGoBelowDomain
&H2
Не включать сетевые папки ниже уровня домена
dhcBifIncludestatusText
&H4
Включить место для информации о состоянии (заполняется через функцию обратного вызова, поэтому данная константа не может использоваться в VBA)
dhcBi fFileSystemAncestors
&H8
Пользователь должен выбрать предшествующий объект файловой системы
dhcBifBrowseForСотриter
&H1000
Возвращает только имена компьютеров
dhcBifBrowseForPrinter
&H2000
Возвращает только имена принтеров
ffВнимание: Если вы хотите использовать функцию dhBrowseForFolder, чтобы позволить пользователям выбирать компьютер, необходимо учесть, что API-функция SHBrowseForFolder имеет один существенный недостаток, который приведет к проблемам: при последующем вызове функция SHGetPathFromIDList не понимает IDList, выданный функцией SHBrowseForFolder, в качестве допустимого значения пути и возвращает ошибку. (Если выбрать ресурс ниже уровня компьютера, все прекрасно работает — проблема возникает только, когда вы выбираете компьютер.) При выборе компьютера, даже если вы не можете определить полный путь, его имя в диалоговом окне все равно доступно. Этой информации достаточно при работе с именами компьютеров. Предложенная нами функция SHBrowseForFolder, если информация о полном пути недоступна, всегда возвращает окне имя объекта, отображаемого в диалоговом окне.
f
f
Функция dhBrowseForFolder, вызывающая диалоговое окно, представлена в примере 6.18.
Пример 6.18. Вызов диалогового окна просмотра из библиотеки Shell32
Public Function dhBrowseForFolder (ByVai IngCSIDL As Long, _
ByVai IngBifFlags As Long, strFolder As String, _
Optional ByVai hWnd As Long = 0, _
Optional strTitle As String = "Select Directory") As Long
' Выполняет поиск папки.
Dim usrBrws As Browseinfo
Dim IngReturn As Long
Dim IngIDL As Long
Сначала определяет особую папку,
заданную переменными IngCSIDL и IngID.
If SHGetSpecialFolderLocation(hWnd, IngCSIDL, IngIDL) = 0 Then
* Задание структуры просмотра. With usrBrws
.hwndOwner = hWnd
.pidlRoot = IngIDL
.strDisplayName = String$(dhcMaxPath, vbNullChar) .strTitle = strTitle
.ulFlags = IngBifFlags End With
' Открытие диалогового окна просмотра. IngIDL = SHBrowseForFolder(usrBrws)
’ Если диалоговое окно открылось успешно. If IngIDL Then
strFolder = String$(dhcMaxPath, vbNullChar)
' Пытается преобразовать IngIDL в реальный путь.
If SHGetPathFromIDList(IngIDL, strFolder) Then ' Обрезает символы Null.
strFolder = dhTrimNull(strFolder)
IngReturn = dhcNoError
Else
* Реальный путь каталога недоступен, поэтому, по крайней
* мере, возвращает выбранное имя.
strFolder = dhTrimNull(usrBrws.strDisplayName) IngReturn = dhcNoError
End If
Else
* Произошла неизвестная ошибка.
IngReturn = dhcErrorExtendedError End If
Else
* Произошла неизвестная ошибка.
IngReturn = dhcErrorExtendedError
End If
dhBrowseForFolder = IngReturn
End Function
Для вызова этой функции достаточно просто задать ей соответствующие значения флагов из табл. 6.10 и 6.11. Например, следующий фрагмент программы отображает диалоговое окно, представленное на рис. 6.7:
Dim strFolder As String
* При этом происходит заполнение strFolder выбранным значением.
Call dhBrowseForFolder(dhcCSIdlNetworkNeighborhood, _ dhcBifReturnOnlyFileSystemDirs, strFolder, _ strTitle:="Select a Special Location:")
Хотя это диалоговое окно и не безупречно (далеко не всегда с его помощью удается получить полезные результаты), оно дает общее представление, и его использовать намного проще, чем создавать собственное!
LAN Manager API
Функции, которые рассматривались до сих пор, предоставляют широкий набор возможностей, обеспечивающих работу вашего приложения в сети. Они доступны как в Windows NT, так и в Windows 95. Однако некоторые нужные функции в WNet API отсутствуют. Если вам требуются эти возможности, воспользуйтесь функциями LAN Manager API, которые доступны только в Windows NT. Полный API состоит более чем из 120 функций (по сравнению с 14 функциями WNet API), и, невзирая на то, что эти функции не полностью описаны в документации (и что лучшая документация находится в старых руководствах по OS/2), имеется ряд вещей, которые невозможно реализовать без использования LAN Manager API. Функции, рассмотренные в этом разделе, управляют:
добавлением сетевых совместно используемых ресурсов;
удалением сетевых совместно используемых ресурсов;
изменением сетевого пароля пользователя.
Функции LAN Manager имеют четыре интересных особенности:
работают только под Windows NT и не доступны пользователям Windows 95;
все строки, которые передаются функциям LAN Manager API, должны быть в формате Unicode, а не в формате ANSI (в представленных ниже примерах соответствующее преобразование выполняется посредством функции StrConv; более подробную информацию о функции strConv вы найдете в главе 1);
функции API используют понятную и согласованную систему имен, поэтому из их названий видно, для чего они предназначены (имя каждой функции начинается с префикса Net, за которым следуют существительное, описывающее, с каким объектом будет выполняться операция, и глагол, указывающий, какое действие выполняет функция по отношению к данному объекту; например, при желании добавить пользователя используйте функцию NetUserAdd);
познакомившись с одной из функций LAN Manager API, вы сможете работать с десятками подобных ей функций.
Внимание: Конечно, не все новости являются хорошими. Многие из функций LAN Manager недоступны для программистов на VBA, поскольку они возвращают указатели на указатели адресов памяти. В VBA нет механизма для переадресации этих указателей, поэтому без написания вспомогательной DLL вы не сможете вызывать эти функции.
Добавление сетевого совместно используемого ресурса
Функции WNet позволяют составлять список сетевых совместно используемых ресурсов, однако не дают возможности создавать их. LAN Manager API включает функцию NetShareAdd, которая позволяет создать новый сетевой ресурс. Описание этой функции имеет следующий вид:
Private Declare Function NetShareAdd Lib "NETAPI32. DLL” _
(ByVai strServername As String, ByVai IngLevel As Long, _
strbuf As SHARE_INFO_2, IngParamErr As Long) As Long
Ниже описаны параметры функции NetShareAdd.
Параметр
Описание
strServerName
Имя компьютера, на котором добавляется ресурс
IngLevel
Определяет уровень информации о ресурсе, которая записывается в буфер (можно задать одну из двух структур данных, share_info_2 или share_info_502, в зависимости от количества информации, которое вы собираетесь указывать; в наших примерах используется структура share_info_2)
IngParamErr
После вызова функции указывает, какой из параметров задан неправильно (если бы все функции API давали такую возможность!)
11Внимание: Чтобы применять функцию NetShareAdd, пользователь должен иметь право добавлять совместно используемый ресурс. В противном случае произойдет ошибка. Если вы не входите в состав локальных групп администраторов (Administrators) или операторов бюджета (Account Operators) и не представлены в группах операторов связи (Communication), принт-операторов (Print) или сервер-операторов (Server), вы не сможете успешно выполнить функцию NetShareAdd. Даже обладая этими правами, принт-оператор может добавлять задания только в очередь на печать, а оператор связи — только в очередь устройств связи.
1
1
Структура share_info_2 содержит параметры, описанные в следующем списке. Типы сетевых совместно используемых ресурсов представлены в табл. 6.12, а флаги прав доступа — в табл. 6.13.
Параметр
Описание
Shi2_netname
Имя нового ресурса
Shi2_type
Тип ресурса (возможные значения этого параметра приведены в табл. 6.12)
Shi2_remark
Комментарий, который появится в Windows Explorer и в диалоговом окне сетевого подключения в Windows NT
Shi2_permissions
Флаг, определяющий права доступа (параметр игнорируется, если на компьютере не установлена система безопасности уровня совместного использования; возможные значения этого параметра приведены в табл. 6.13)
shi2_max_uses
Максимально допустимое число одновременно работающих пользователей (-1, если их число не ограничено)
shi2_current_uses
Текущее число пользователей (игнорируется при добавлении нового ресурса)
shi2_path
Локальный путь совместно используемого ресурса на данном компьютере
shi2_passwd
Пароль доступа к ресурсу (параметр игнорируется, если на компьютере не установлена система безопасности уровня совместного использования)
Таблица 6.12. Типы сетевых ресурсов (используются в качестве значений параметра shi2_type)
Константа
Значение Описание
dhcLanManStypeDisktree
&H0Дисковый ресурс
dhcLanManStypePrintq
&Н 1Принтерный ресурс
dhcLanManStypeDeviсе
&Н2Устройство (например, СОМ-порт)
dhcLanManStypelpc
&НЗ1РС-ресурс
Таблица 6.13. Флаги прав доступа (используются в качестве значений параметра shi2jpermissions)
Константа
Значение
Описание
dhcLanManAccessNone
&H0
Доступ не разрешен
dhcLanManAccessRead
&H1
Доступ на чтение из ресурса
dhcLanManAccessWrite
&H2
Доступ на запись в ресурс
dhcLanManAccessCreate
&H4
Доступ на создание нового экземпляра ресурса
dhcLanManAccessExec
&H8
Доступ на выполнение ресурса
dhcLanManAccessDelete
&H10
Доступ на удаление ресурса
dhcLanManAccessAtrib
&H20
Доступ на изменение атрибутов
dhcLanManAccessPerm
&H40
Доступ на изменение прав доступа
dhcLanManAccessAll
&HFF
Битовая маска, включающая все предыдущие флаги
Сама по себе эта функция достаточно проста; необходимо лишь преобразовать строки в формат Unicode и вызвать NetShareAdd. Соответствующая функция-оболочка dhAddNetworkShare представлена в примере 6.19. Обратите внимание на то, что многие параметры являются необязательными. В табл. 6.14 описаны все параметры функции dhAddNetworkShare.
Таблица 6.14. Параметры функции dhAddNetworkShare
Параметр
Значение по умолчанию
Описание
strServer
(необязательный)
Имя компьютера, на котором добавляется ресурс
strShareName
(необязательный)
Имя нового ресурса
strPath
(необязательный)
Локальный путь совместно используемого ресурса на компьютере
fDisk
(обязательный)
True
При значении True ресурс является диском, а при значении False — принтером
IngMaxUsers
(обязательный)
-1
Максимально допустимое количество одновременно работающих пользователей (при значении -1 количество не ограничено)
strPassWord
(обязательный)
Пароль ресурса (параметр игнорируется, если на компьютере не установлена система безопасности уровня совместного использования)
IngPermissions
(обязательный)
dhcLanMan-
AccessAll
Флаг, определяющий права доступа (игнорируется, если на компьютере не установлена система безопасности уровня совместного использования; список допустимых значений приведен в табл. 6.13)
Пример 6.19. Добавление сетевого ресурса
Public Function dhAddNetworkShare(ByVai strServer As String, _ strShareName As String, strPath As String, _ Optional strRemarks As String, _ Optional fDisk As Boolean = True, _ Optional IngMaxUsers As Long = -1, _ Optional IngPermissions As Long = dhcLanManAccessAll, _ Optional strPassword As String = ”") As Long
’ Добавляет сетевой ресурс.
Dim si2 As SHARE_INFO_2
Dim IngParamError As Long
IngParamError = 0
' При необходимости преобразует все строки в формат
1 Unicode и помещает их в структуру данных. strServer = StrConv(strServer, vbUnicode)
With si2
.shi2_netname = StrConv(strShareName, vbUnicode)
.shi2_type = Ilf(fDisk, dhcLanManStypeDisktree, _ dhcLanManStypePrintq)
.shi2_remark = StrConv(strRemarks, vbUnicode)
.shi2_permissions = IngPermissions
.shi2_max_uses = IngMaxUsers
.shi2_path = StrConv(strPath, vbUnicode)
.shi2_passwd = StrConv(strPassword, vbUnicode) End With dhAddNetworkShare = _
NetShareAdd(strServer, dhcShareInfo2, si2, IngParamError) End Function
Эта функция также вызывается очень просто. Например, чтобы сделать диск С компьютера с именем Hopper сетевым ресурсом под именем ROOT, доступным для совместного использования, нужно выполнить следующее:
IngReturn = dhAddNetworkShare ("WHopper", "ROOT", "с:\")
Совет: Параметр strPath должен содержать полный путь. Не попадитесь, подобно нам, в такую ловушку: если вы зададите с: без косой черты, вы получите ошибку 123 — Файл, путь или имя диска задано неверно.
Удаление сетевого совместно используемого ресурса
Удалить сетевой ресурс гораздо проще, чем добавить. Определение соответствующей API-функции NetShareDel выглядит следующим образом:
Private Declare Function NetShareDel Lib "NETAPI32.DLL" _
(ByVai strServername As String, ByVai strNetname As String, _
ByVai IngReserved As Long) As Long
Функция NetShareDel принимает в качестве аргументов имя сервера и имя ресурса, который необходимо удалить. (Параметр IngReserved недокументирован, и его значение следует задать равным 0.) В примере 6.20 приведена функция dhDeleteNetworkShare — оболочка для NetShareDel. (Функция-оболочка также выполняет преобразование в формат Unicode.)
Внимание: Если вы не входите в состав локальных групп администраторов (Administrators) или операторов бюджета (Account Operators), или не представлены в группах операторов связи (Communication), принт-опе- раторов (Print) или сервер-операторов (Server), вы не сможете успешно выполнить функцию NetShareDel. Даже обладая этими Правами, принт-оператор может удалять задания только из очереди на печать, а оператор связи — только из очереди устройств связи.
Пример 6.20. Удаление сетевого ресурса
Public Function dhDeleteNetworkShare(ByVai strServer As String, _ ByVai strShareName As String) As Long ’ Удаление сетевого ресурса.
’ Преобразование строк в формат Unicode. strServer = StrConv(strServer, vbUnicode) strShareName = StrConv(strShareName, vbUnicode)
dhDeleteNetworkShare = NetShareDel(strServer, _ strShareName, 0&)
End Function
Изменение пароля пользователя
Пароль пользователя несложно изменить с помощью функции LAN Manager API
NetUserChangePassword, которая принимает четыре параметра.
Параметр Описание
DomainName Удаленный сервер или домен (если задать в качестве значения это
го параметра 0&, функция будет работать с исходным доменом пользователя, который вызвал функцию)
UserNameUserNameИмя пользователя, пароль которого должен быть изменен (если задать в качестве значения этого параметра 0&, функция будет работать с именем текущего пользователя)
UserName
UserName
OidPassword Старый пароль пользователя
NewPassword Новый пароль пользователя
Описание функции NewUserChangePassword выглядит следующим образом:
Private Declare Function NetUserChangePassword Lib _ ’’NETAPI32.DLL” (DomainName As Any, ByVai UserName As String, _ ByVai OidPassword As String, ByVai NewPassword As String) _ As Long
Как и для остальных функций LAN Manager API, строки должны быть преобразованы в формат Unicode. Функция-оболочка dhChangeUserPassword, которая выполняет эти подготовительные операции, представлена в примере 6.21.
Пример 6.21. Изменение сетевого пароля пользователя
Public Function dhChangeUserPassword( _
ByVai strOldPwd As String, ByVai strNewPwd As String, _ Optional strUser As String, Optional strDomain As String) _ As Long
Изменяет пароль пользователя. Dim IngReturn As Long
’ Если имя пользователя не задано, по умолчанию ' используется текущий пользователь.
If Len(strUser) = 0 Then strUser = dhGetUserName()
Преобразует строки в формат Unicode.
strOldPwd = StrConv(strOldPwd, vbUnicode) strNewPwd = StrConv(strNewPwd, vbUnicode) strUser = StrConv(strUser, vbUnicode) strDomain = StrConv(strDomain, vbUnicode)
’ Если домен указан, используется заданное значение.
1 В противном случае применяется значение ByVai 0& (Null). If Len(strDomain) > 0 Then
IngReturn = NetUserChangePassword(ByVai strDomain
strUserstrUserstrOldPwd, strNewPwd)strOldPwd, strNewPwd)
strUser
strUser
strOldPwd, strNewPwd)
strOldPwd, strNewPwd)
Else
IngReturn = NetUserChangePassword(ByVai 0&, _ strUser, strOldPwd, strNewPwd) End If dhChangeUserPassword = IngReturn End Function
Например, чтобы изменить собственный пароль простофиля на болтун, нужно вызвать функцию таким образом:
If dhChangeUserPassword("простофиля", "болтун") _ = dhcNoError Then ’ Пароль изменен успешно.
End I f
Ниже приведен список ошибок, которые чаще всего возникают при выполнении функции dhChangeUserPassword.
Ошибка
Описание
dhcErrorlnvalidPassword
Старый пароль задан неправильно
dhcErrorAccessDenied
Пользователь не имеет доступа к запрашиваемой информации
dhcErrorInvalidComputer
Имя компьютера задано неправильно
dhcErrorNotPrimary
Операция разрешена только на первичном контроллере домена
dhcErrorUserNotFound
Имя пользователя не найдено
dhcErrorPasswordTooShort
Пароль короче, чем нужно
О функциях, которые не рассматривались
В этом разделе функции LAN Manager API описаны лишь поверхностно. Среди большого набора функций LAN Manager API можно обнаружить много полезных (равно как и устаревших или замененных) функций. Эти функции охватывают все аспекты работы в сети, в том числе:
информацию о домене;
информацию об удаленных файлах;
управление локальными и глобальными группами;
отправку сообщений;
услуги по составлению расписаний;
управление серверами и сессиями;
передачу информации;
управление пользователями и рабочими станциями.
Хотя многие из этих функций трудно вызвать из VBA, остается еще достаточное число функций LAN Manager, которыми вы можете воспользоваться. Примеры, представленные в данной главе, послужат вам хорошей отправной точкой.
Совет: Где можно получить более подробную информацию о LAN Manager API? Один из возможных вариантов — это, как всегда компакт-диск MSDN (Microsoft Developer Network). Если вас интересуют подобные сведения, вы не пожалеете, что приобрели этот исключительно полезный источник. Конечно, значительную часть данной информации можно найти на Web-узле Microsoft, однако это далеко не так удобно, как использовать информацию на компакт-диске. Компакт-диск MSDN стоит купить, если вы хотите применять в своих разработках функции Windows API.
Резюме
Создание приложений, обладающих подробной информацией о состоянии сети, является одним из наиболее сложных аспектов программирования на VBA, потому что принципы использования сетевых функций API в VBA плохо описаны в документации, а также потому, что многие из этих функций создавались не для разработчиков на VBA. В настоящей главе рассмотрено большинство основных сетевых функций, которые можно применять в приложениях, а также несколько более сложных сетевых функций. Кроме того, мы описали ряд специальных приемов работы с функциями API, которые "не дружат" с VBA. В частности, рассмотрены следующие вопросы: ■ применение стандартных сетевых диалоговых окон:
диалоговое окно подключения к сети;
диалоговое окно отключения от сети;
подключение к ресурсам сети вручную:
установка сетевых соединений;
удаление сетевых соединений;
получение основной информации о сетевых ресурсах:
определение UNC-пути подключенного сетевого ресурса;
определение имени текущего пользователя;
получение (и задание) имени рабочей станции;
получение дополнительной информации о сети;
составление списков сетевых ресурсов
открытие, использование и закрытие дескрипторов списков;
составление списка доступных совместно используемых сетевых ресурсов на данном компьютере;
составление списка подключенных сетевых ресурсов на данном компьютере;
составление списка компьютеров в домене;
применение нового диалогового окна просмотра из библиотеки Shell32;
функции LAN Manager API:
добавление сетевых ресурсов;
удаление сетевых ресурсов;
изменение сетевых паролей пользователей.
Более подробную информацию о работе со строками вы найдете в главе 1.
ГЛАВА 7
Работа с дисками и файлами
Встроенные функции VBA для работы с дисками и файлами
Если нельзя обойтись без операций ввода-вывода
Windows API: настоящая эффективность
При создании VBA-программ рано или поздно вы столкнетесь с необходимостью выполнять определенные операции с дисками и файлами. Это могут быть простые операции, такие как копирование или удаление файлов, или более сложные, например открытие и синтаксический анализ текстовых файлов. Первая часть данной главы посвящена многочисленным встроенным функциям VBA, предназначенным для работы с дисками и файлами. Их вполне достаточно для решения большинства задач, однако они не обеспечивают тех функциональных возможностей, которые стремятся иметь многие разработчики. Поэтому во второй части главы рассматриваются еще и функции Windows API для работы с дисками и файлами. Приведенная в заключительной части объектная модель управления файлами с помощью модулей класса VBA связывает воедино все изложенное в данной главе, подводя итог рассматриваемой теме.
Встроенные функции VBA для работы с дисками и файлами
VBA предлагает несколько встроенных функций, обеспечивающих работу с дисками и файлами. Они служат главным образом для решения самых распространенных и простых задач. В данной главе мы рассматриваем их только ради полноты изложения. В большинстве своем это функции, унаследованные от Basic предыдущих версий. Если вы имеете опыт работы с языком Basic, то, возможно, десятки раз сталкивались с ними ранее. Если же вы новичок в VBA и нуждаетесь в более детальном их рассмотрении, вам следует просмотреть документацию по VBA или другой вводный курс.
Функция DIR
Хотя Dir не самая мощная встроенная функция VBA, она используется чаще других. Ее основное назначение в том, чтобы возвращать имена файлов заданного каталога. Dir принимает в качестве параметра спецификацию файлов и возвращает список файлов, удовлетворяющих заданной спецификации. Существует четыре варианта вызова функции Dir.
Если вводится полное имя файла (без подстановочных символов) и файл хранится в указанном каталоге, Dir возвращает имя файла. Если файла в каталоге нет, Dir возвращает пустую строку.
Если вводится спецификация файла (включая подстановочные символы), Dir возвращает имя первого файла, который соответствует указанной спецификации.
Если вводится имя каталога, Dir ищет первый файл в этом каталоге.
Если вы не вводите никаких параметров, Dir возвращает имя следующего файла, соответствующего спецификации, которая была задана при предыдущем вызове функции. Можно продолжать вызывать функцию Dir без аргументов до тех пор, пока она не возвратит пустую строку, указывающую на отсутствие подходящих файлов.
При вызове функции Dir следует руководствоваться следующими правилами:
при первом вызове функции Dir или после того, как функция возвратит пустую строку, необходимо указывать путь; в противном случае возникает ошибка;
можно вводить полный, относительный или UNC-путь;
если путь не указан, функция Dir выполняет поиск в текущем каталоге;
каждый раз, указывая при вызове функции новый путь, вы отказываетесь от результатов предыдущего поиска.
Процедура в примере 7.1 содержит несколько вариантов вызова функции Dir.
Пример 7.1. Различные способы вызова функции Dir
Sub dhTestDir ()
Dim strFile As String
' Выводит "WIN.INI", если такой файл существует. Debug.Print Dir ("C:\WINDOWS\WIN.INI")
’ Выводит имя первого файла, начинающееся ' с символа W.
Debug.Print Dir("С:\WINDOWS\W*")
' Выводит имя следующего файла, начинающееся 1 с символа W.
Debug.Print Dir
Выводит имена всех файлов текущего каталога. strFile = Dir("*") Do Until strFile = ""
Debug.Print strFile strFile = Dir
Loop
End Sub
I MF Совет: ®° всех Функциях VBA работы с файлами для представления теку- I I у щего и родительского каталогов можно использовать одинарную (.) и двойную (..) точки соответственно. Например, команда Dir (" . . V . *") возвращает имя первого файла каталога, расположенного сразу же над текущим.
Проверка наличия файла
Нередко разработчику необходимо проверить существование файла. Это может понадобиться перед использованием других функций или операторов, скажем, оператора Kill (который удаляет файл). В решении этой проблемы на помощь приходит функция Dir. В примере 7.2 приведена функция dhFileExists, которая по полному пути файла (без подстановочных символов) определяет, существует файл (возвращается значение True) или нет (возвращается значение False).
Пример 7.2. Функция проверки существования файла в каталоге
Function dhFileExists(strFile As String) As Boolean
Вызывает функцию Dir, передавая ей имя файла;
если файл существует, Dir возвращает
имя файла, а длина строки будет больше 0.
dhFileExists = (Len(Dir(strFile)) > 0) End Function
Использование атрибутов файла
В файловых системах FAT или NTFS атрибуты файлов указывают, являются ли эти файлы доступными только для чтения, а также скрытыми, архивными или системными. Обычно функция Dir не возвращает имена скрытых или системных файлов, но вы можете включить их в поиск. Можно проводить поиск каталогов, а также определять метку тома диска. Для этого параметры поиска следует задавать в качестве необязательного второго аргумента функции.
Значение
КонстантаОписание
0
vbNormalАтрибуты файла, определенные по умолчанию
2
vbHiddenИщет скрытые файлы
4
vbSystemИщет системные файлы
8
vbVolumeВозвращает метку тома диска
16
vbDirectoryИщет каталоги
Например, для того чтобы включить скрытые и системные файлы в список, создаваемый командой Dir, можно использовать следующий оператор:
strFile = Dir ("С:\", vbHidden + vbSystem)
Примечание: Задание в качестве второго аргумента атрибута файла не означает, что будут выведены имена только тех файлов, для которых этот атрибут установлен. Соответствующие файлы добавляются в список обычных файлов, возвращаемых функцией Dir.
GetAttr и SetAttr
Для определения значения и задания атрибутов файла VBA предоставляет две функции: GetAttr и SetAttr. Функция GetAttr получает имя файла и возвращает длинное целое, которое является битовой маской атрибутов файла. Кроме атрибутов, перечисленных в приведенной выше таблице, можно также проверять наличие у файла атрибута vbReadOnly (только для чтения, значение 1) или vbArchive (архивного атрибута, значение 32). Функции SetAttr передаются имя файла и битовая маска, которая задает атрибуты файла. В примере 7.3 представлена функция dhlsAttr. В качестве аргументов ей передается имя файла и набор атрибутов. Функция возвращает значение True, если атрибуты заданного файла соответствуют переданным функции.
Пример 7.3. Использование функции dhlsAttr для проверки атрибутов файла
Function dhlsAttr(strFile As String, IngAttr As Long)_ As Boolean
* Проверяет соответствие атрибутов файла заданным
* значениям — возвращает значение True, если атрибуты совпадают. dhlsAttr = ((GetAttr(strFile) And IngAttr) = IngAttr)
End Function
В качестве аргумента функции можно задавать значение любого атрибута или их комбинацию. Например:
' Является ли файл "C:\MSDOS.SYS" системным?
Debug.Print dhlsAttr("С:\MSDOS.SYS", vbSystem)
' Является ли файл "C:\MSDOS.SYS" системным файлом и * одновременно файлом, доступным только для чтения? Debug.Print dhlsAttr("С:\MSDOS.SYS", vbSystem + vbReadOnly)
Приведем другой способ использования функции GetAttr (пример 7.4). В этом примере представлены три процедуры, которые выводят список всех файлов корневого каталога вместе с их атрибутами. Процедура dhPrintAttr с помощью функции Dir определяет все файлы и каталоги (обратите внимание на комбинацию значений, заданную в качестве второго аргумента). Процедуры dhBuildAttrString и dhBuildAttr сравнивают атрибуты каждого файла с заданными атрибутами и формируют строку с результатами этих сравнений. На рис. 7.1 показан результат выполнения процедуры dhPrintAttr.
Пример 7.4. Две процедуры, которые выводят файлы и их атрибуты
Sub dhPrintAttr()
Dim strFile As String
Dim IngAttr As Long
Dim strAttr As String
' Использует корневой каталог текущего диска.
Const dhcDir = "\"
' Ищет файлы всех типов.
strFile = Dir(dhcDir, _
vbHidden + vbSystem + vbDirectory)
* Выполняет цикл до тех пор, пока все соответствующие
* спецификации файлы не будут найдены.
Do Until strFile = ”"
' Использует функцию GetAttr для определения
* атрибутов файла.
IngAttr = GetAttr(dhcDir & strFile)
* Выводит имя файла с его атрибутами.
Debug.Print strFile, dhBuildAttrString(IngAttr)
’ Получает следующий файл и сбрасывает значения в строке атрибутов. strFile = Dir strAttr = ""
Loop
End Sub
Function dhBuildAttrString(IngAttr As Long) As String
Dim strAttr As String
' Формирует строку атрибутов.
dhBuildAttr strAttr, IngAttr, vbReadOnly, ”R”
dhBuildAttr strAttr, IngAttr, vbHidden, "H"
dhBuildAttr strAttr, IngAttr, vbSystem, "S"
dhBuildAttr strAttr, IngAttr, vbArchive, "A"
dhBuildAttr strAttr, IngAttr, vbDirectory, "D"
Возвращает строку атрибутов.
dhBuildAttrString = strAttr
End Function
Sub dhBuildAttr(strAttr As String, IngAttr As Long, IngMask As Long, strSymbol As String)
Сравнивает атрибуты каждого файла с маской
и, если они совпадают, присоединяет
переданный символ к строке.
If (IngAttr And IngMask) = IngMask Then strAttr = strAttr & strSymbol
Else strAttr = strAttr & " "
End If
End Sub
Qi Проверка
dhPrintAtr IO.DOSRHS
MSDOS.DOSRHS
COMMAND.DOSA
COMMAND.COMA
BOOTLOG.TXTH A
AUTOEXEC.BATA
AUTOEXEC.DOSA
CONFIG.DOSA
SUHDLOG.DAT RH MSDOS.H
SETUPLOG.TXTHA
CONFIG.SYSA
WINDOWSD
MSDOS.SYSRHSA
DBLSPACE.BINRHS
BOOTLOG.PRVH A
Program Files R D SYSTEM.1STRHS
DRVSPACE.BINRHS
DETLOG.OLDHS
IO.SYSRHS
RECYCLEDHSD
Puc. 7.1. Список файлов и их атрибутов
Вывод списка имен каталогов
Знание атрибутов файлов весьма существенно для нахождения на диске файлов определенных типов, а также каталогов. (Для файловой системы каталоги являются теми же файлами, но с характерным набором атрибутов.) В примере 7.5 содержится процедура, которая составляет список всех подкаталогов, включенных в данный каталог. Обратите внимание на то, что после вызова функции Dir для формирования списка процедура использует функцию dhlsAttr, чтобы определить, какие из найденных имен являются именами каталогов.
Пример 7.5. Процедура, которая выводит имена подкаталогов
Sub dhListSubDirs(strPath As String)
Dim strFile As String
* Проверка того, что strPath является каталогом.
If Right(strPath, 1) <> "\" Then strPath = strPath & "\"
End If
If dhlsAttr(strPath, vbDirectory) Then
' Находит все файлы, включая каталоги. strFile = Dir(strPath, vbDirectory) Do Until strFile = ""
* Если элемент является каталогом, выводит его имя. If dhlsAttr(strPath & strFile, vbDirectory) Then
* Игнорирует "." и "..".
If strFile <> "." And _ strFile <> ".." Then Debug.Print strFile
End If
End If
' Получает следующий файл. strFile = Dir
Loop
End If
End Sub
Эти надоедливые точки в функции Dir
Вы, наверное, обратили внимание на то, что в приведенной выше процедуре конструкция if . . . Then отбрасывает два возвращаемых функцией Dir значения: " . " и " . . ", представляющие текущий и родительский каталоги соответственно. Функция Dir возвращает эти значения всякий раз, когда вы просматриваете содержимое любого каталога, расположенного ниже корневого. С точки зрения операционной системы это вполне корректные имена каталогов. Однако в большинстве случаев вы вряд ли захотите включать их в создаваемый список.
Операторы перемещения файлов на диске
Вероятно, большая часть операций обработки файлов связана с копированием, перемещением, переименованием и удалением. VBA предлагает несколько простых операторов, предназначенных для выполнения этих задач.
ОператорОписание
FilecopyКопирует файл
NameПеремещает или переименовывает файл
KillУдаляет файл
Пример 7.6 содержит небольшую процедуру, демонстрирующую работу этих операторов. Данная процедура копирует файл WIN. INI из каталога Windows в корневой каталог, переименовывает его в WIN.TMP, а затем удаляет. Что может быть проще?
Пример 7.6. Копирование, переименование и удаление файлов
Sub dhCopyRenameDelete()
* Копирует WIN.INI в корневой каталог.
FileCopy "C:\WINDOWS\WIN.INI", "C:\WIN.INI"
Переименовывает его в WIN.TMP.
Name "C:\WIN.INI" As "С:\WIN.ТМР"
Удаляет переименованный файл.
Kill "C:\WIN.TMP"
End Sub
Хотя в этих операторах нет ничего особенного (за исключением, возможно, необычного синтаксиса оператора Name), о них необходимо знать следующее:
оператор Filecopy перезаписывает указанный файл, если он уже существует;
оператор Name не перезаписывает существующий файл;
копировать и переименовывать файл можно одним оператором, задав ему другое имя назначения;
■ посредством оператора Name можно перемещать файлы, задав им другие имена назначения.
Немного информации о файле: FileLen и FileDateTime
Следующие достойные упоминания функции для работы с файлами — это FileLen и FileDateTime. Функция FileLen, получив в качестве аргумента имя файла, возвращает его размер в байтах. Функция FileDateTime по имени файла предоставляет информацию о дате и времени его последней модификации. Пример 7.7 содержит очень простую процедуру, которая демонстрирует синтаксис обеих функций. В разделах "Эффективная замена функции Dir" и "Быстрое получение информации о файле" настоящей главы рассмотрены принципы применения функций Windows API для получения дополнительной информации о файлах.
Пример 7.7. Вывод объема файла и даты его последней модификации
Sub dhMoreFilelnfо()
* Каков объема файла "WIN.INI"?
Debug.Print FileLen("С:\WINDOWS\WIN.INI”) & " bytes"
Когда он был изменен последний раз?
Debug.Print FileDateTime("С:\WINDOWS\WIN.INI")
End Sub
Работа с каталогами
Для работы с каталогами VBA предоставляет несколько функций. Они так же просты, как и функции работы с файлами. Используя эти функции, вы сможете определять текущий каталог или назначать его, назначать текущий диск, а также создавать и удалять каталоги.
Путаница с текущими каталогами
Что действительно может внести путаницу при использовании функций работы с каталогами, так это представления о текущем диске и текущем каталоге. В Windows каждый процесс (приложение) может задавать текущий каталог. Любые не совсем корректные операции над файлами могут повлиять на это назначение. Например, результатом выполнения команды:
Debug.Print Dir ("*.*")
является имя первого файла в текущем каталоге. Хотя мы рекомендуем избегать применения текущего каталога и советуем работать только с реальными путями (полный путь с указанием диска и каталога), иногда использование текущего каталога весьма удобно. Как же выяснить, какой каталог является текущим в данный момент? Это можно сделать с помощью функции Си г Di г. Например, команда:
Debug.Print CurDir
выводит на экран имя текущего каталога и имя текущего диска. Для смены текущего каталога используется оператор ChDir:
ChDir "C:\WINDOWS"
Подобным образом для смены текущего диска применяется оператор ChDrive. Вот здесь и начинается путаница. Можно ли использовать оператор ChDir для смены диска? Ответ на этот вопрос отрицательный. Дело в том, что VBA отслеживает диски и каталоги отдельно. Несмотря на то, что в операторе ChDir можно указать другое имя диска, данный диск не станет текущим до тех пор, пока вы не выполните оператор ChDrive. В этом можно убедиться, запустив программу из примера 7.8. (Только обязательно проверьте, что соответствующие диски и каталоги существуют на вашем компьютере.)
Пример 7.8. Путаница, возникающая при использовании операторов ChDir и ChDrive
Sub dhTestCurrent()
'Вывод имени первого файла в текущем каталоге.
Debug.Print Dir ("*.*"), "in " & CurDir
* Попытка перейти на диск D, используя функцию ChDir.
ChDir "D:\SOMEDIR"
' Повторный вывод имени первого файла —
' он все равно находится на диске С!
Debug.Print Dir("*.*") , "in " & CurDir
' Использование функции ChDrive для смены диска.
ChDrive "D"
* Повторный вывод имени первого файла — на этот раз
* текущим является уже диск D!
Debug.Print Dir("*.*") , "in " & CurDir End Sub
Сначала выводится имя первого файла в текущем каталоге и имя каталога. Затем процедура пытается осуществить переход на диск D, используя только оператор ChDir. Хотя оператор и выполняется без ошибки, имя первого файла оказывается тем же, что и раньше. Имя текущего каталога также не меняется, поскольку не удалось перейти на диск D с помощью оператора ChDir. Затем процедура использует для перехода на диск D оператор chDrive. Когда имя файла выводится в третий раз, это уже первый файл каталога D:\SOMEDIR. Данный пример подтверждает, что хотя оператор ChDir допускается использовать для перехода на другой диск и в другой каталог, VBA не распознает новый каталог до тех пор, пока не выполнен оператор chDrive.
Создание и удаление каталогов и деревьев каталогов
Операторы MkDir и RmDir служат соответственно для создания и удаления каталогов. Параметром обоих операторов является путь к каталогу. Причем можно задавать как полный путь, включая букву диска, так и неполный. Если вы опускаете букву диска, то операторы MkDir и RmDir определяют, какой диск является в данный момент текущим, и дополняют до полного все пути, указываемые относительно текущего каталога. Другими словами, они работают так же, как и одноименные команды MS-DOS.
Следует упомянуть и о том, что, подобно соответствующей команде MS-DOS, оператор RmDir не выполняется, если вы попытаетесь удалить каталог, содержащий файлы или подкаталоги. Сначала вы должны удалить все хранящиеся в нем файлы оператором Kill. А поскольку подкаталоги могут оказаться вложенными, то удаление каталога верхнего уровня становится проблематичным и потребует проведения рекурсивного поиска подкаталогов и удаления всех хранящихся в них файлов. Однако это не так просто, поскольку каждый рекурсивный вызов функции Dir уничтожает результаты ее предыдущего вызова.
Одно из возможных решений удаления каталога и всего его содержимого представлено в примере 7.9. Функция dhDelTree, названная по аналогии с соответствующей утилитой MS-DOS 5.0, получает в качестве аргумента имя каталога и последовательно удаляет все находящиеся в нем файлы и подкаталоги. Сначала она присваивает имя текущего каталога переменной strOriginalDir, а затем переходит к заданному каталогу и удаляет все находящиеся в нем файлы. После этого выполняется поиск подкаталогов, и как только будет найден один из них, функция рекурсивно вызывает саму себя. Обработав рекурсивный вызов, функция ищет очередной подкаталог. Так продолжается до тех пор, пока в заданном каталоге нс останется ни одного подкаталога. В конце концов, она уничтожает заданный каталог и присваивает статус текущего тому каталогу, который был таковым до начала работы процедуры.
Пример 7.9. Функция для удаления всего дерева каталогов
Function dhDelTree (strDirectory As String) As Boolean
On Error GoTo HandleError
Dim strOriginalDir As String
Dim strFilename As String
* Проверяет, действительно ли существует заданный каталог.
* Если нет, процедура заканчивает свою работу.
If Len(Dir(dhFixPath(strDirectory), vbDirectory)) = 0 Then Exit Function
End If
' Сохраняет имя текущего каталога и переходит к тому, * который должен быть удален.
strOriginalDir = CurDir
ChDir strDirectory
* Удаляет все файлы в текущем каталоге.
strFilename = Dir ("*.*") Do Until strFilename = "" Kill strFilename strFilename = Dir
Loop
* Затем формирует список подкаталогов. Do
strFilename = Dir ("*.*", vbDirectory)
* Пропускает "." и "..".
Do While strFilename = "." Or strFilename = ".." strFilename = Dir
Loop
* Если файлов больше нет, выходит из цикла, ' иначе снова вызывает функцию dhDelTree для удаления 1 всего подкаталога.
If strFilename = "" Then Exit Do
Else
If Not dhDelTree(strFilename) Then GoTo ExitHere
End If
End If
Loop
* Возвращается в исходный каталог. ChDir strOriginalDir
' В заключение удаляет заданный каталог.
RmDir strDirectory dhDelTree = True
ExitHere:
Exit Function
HandleError:
Select Case Err.Number Case Else
MsgBox Err.Description, vbExclamation, _ "Error " & Err.Number & " in dhDelTree"
Resume ExitHere
End Select
End Function
Примечание: Функция dhFixPath используется в этой процедуре лишь для того, чтобы проверить, содержит ли заданный путь заключительную обратную косую черту.
Как вы, наверное, заметили, данное решение не слишком эффективно из-за того, что необходимо менять каталоги и возобновлять поиск подкаталогов каждый раз после завершения рекурсивного вызова процедуры (а для больших деревьев каталогов это происходит многократно).
!!Внимание: Будьте крайне осторожны при вызове функции dhDelTree. Результаты выполнения операторов, подобных dhDelTree ("С:\"), могут оказаться весьма плачевными.
!
!
Если нельзя обойтись без операций ввода-вывода
Язык Basic, который лежит в основе VBA, появился намного раньше персональных компьютеров. Он разработан еще в те времена, когда не существовало высокоуровневых объектно-ориентированных инструментальных средств разработки баз данных. Поэтому он содержит низкоуровневые функции ввода-вывода файлов. Хотя сейчас существуют более эффективные методы обработки содержимого файла, иногда все еще приходится закатывать рукава и копаться в байтах и битах. В этом разделе кратко описаны функции VBA для ввода-вывода файлов. Поскольку они остались такими же, как и в ранних версиях Basic, для углубленного их рассмотрения мы адресуем читателей к многочисленным книгам по языку Basic.
Получение дескриптора файла
Перед тем как приступить к работе с файлами, следует понять роль дескрипторов файлов (иногда в документации по VBA их еще называют номерами файлов). Дескрипторы файлов, как и другие дескрипторы в Windows, представляют собой целые числа, однозначно идентифицирующие открытые файлы. Первым шагом при открытии файла с помощью VBA является получение неиспользуемого дескриптора файла. Это можно сделать посредством функции FreeFile. Например:
Dim hFile As Long
’ Получение следующего свободного дескриптора файла. hFile = FreeFile
Если это удалось сделать, FreeFile возвращает ненулевое значение, которое можно использовать в операторе Open, описанном в следующем разделе. Дескриптор файла является основой использования остальных функций ввода-вывода.
Примечание: Метод назначения дескрипторов файлов в VBA отличается от метода, используемого Windows API. Функции Windows API возвращают дескрипторы при открытии или создании новых файлов. В разделе "Получение дескрипторов файлов (Windows)" этот вопрос рассмотрен более подробно.
В других книгах, посвященных языку Basic, при описании функций работы с файлами редко упоминают функцию FreeFile, а используют фиксированные дескрипторы файлов (например, "#1"). Несмотря на то, что это вполне допустимый синтаксис, для назначения дескрипторов файлов мы рекомендуем использовать функцию FreeFile, особенно в случае одновременной работы с несколькими файлами. Фиксированные дескрипторы файлов могут привести к путанице и нежелательным последствиям, если по невнимательности вы запишете данные не в тот файл.
Использование оператора Open
Оператор VBA Open применяется для открытия на диске любых файлов для чтения, записи или для того и другого одновременно. Однако избыточный синтаксис оператора и множество необязательных параметров могут стать источником путаницы. Ниже представлен общий вид оператора Open. (Квадратные скобки обозначают необязательные параметры.)
Open pathname For mode [Access access] [lock]_As [#]filenumber [Len=reclength]
В табл. 7.1 приведен список параметров этого оператора и описано назначение каждого из них. Как видите, тут очень легко запутаться!
Таблица 7.1. Параметры оператора Open
Параметр
Описание
pathname
Путь открываемого файла
mode
Режим открытия файла (принимает одно из следующих значений: Append, Binary, Input, Output ИЛИ Random)
access
Режим доступа (принимает одно из следующих значений: Read (по умолчанию), Write ИЛИ Read Write)
lock
Режим совместного использования (принимает одно из следующих значений:
Shared (по умолчанию), Lock Read, Lock Write ИЛИ Lock Read Write)
filenumber
Дескриптор файла (может быть или фиксированным номером, или переменной, которая содержит дескриптор, полученный из функции FreeFile)
reclength
Длина записи (для режима Random) или количество байтов в буфере (для режимов Append, Input И Output)
В следующих трех разделах рассматриваются режим открытия файла, режим доступа и режим совместного использования.
Режим открытия файла
Режим открытия файла определяет, каким образом Windows будет обрабатывать открытый файл и какие операции можно над ним выполнять. Если только вы не создаете базу данных с произвольным доступом, то, скорее всего, вы будете открывать файл в одном из режимов последовательного доступа: Append, Input или Output. Эти три режима рекомендуются для работы с текстовыми файлами. Файлы, открытые в режиме input, доступны только для чтения. Файлы, открытые в режимах Output или Append, могут быть доступны как для чтения, так и для записи. Разница в том, что в режиме output всегда создается новый файл и удаляется уже существующий с тем же самым именем, в то время как в режиме Append текст добавляется в конец файла.
Если необходимо считывать данные из файла и записывать в него данные по байтам (например, при работе с файлами изображений), откройте файл в режиме Binary. Режим Random обычно используют для файлов с записями фиксированной длины, например файлов dBase. Операции с файлами, открытыми в этом режиме, производятся над блоками, соответствующими размеру записи, который определен параметром reclength оператора Open.
Доступ к файлу
Режим доступа к файлу непосредственно связан с режимом его открытия. Параметр доступа может принимать одно из значений: Read, Write и Read Write. Предназначение каждого из них очевидно. Если вы не имеете соответствующих прав доступа, чтобы открыть файл с указанными параметрами, возможно, из-за установок защиты операционной системы или сети возникнет ошибка.
Блокировка
В многопользовательских приложениях полезно установить один из параметров блокировки. По умолчанию задается значение Shared. Установка этого значения позволяет другим пользователям читать данные из файла и записывать в него информацию, пока он открыт. Если вас это не устраивает, можете указать другой параметр. Режим Lock Read запрещает пользователям считывать данные из файла, но разрешает записывать в него информацию. Режим Lock Write позволяет другим пользователям читать содержимое файла, но не дает возможности вносить изменения. Опция Lock Read Write задает самые жесткие ограничения. Она не позволяет другим пользователям ни читать, ни записывать данные.
ffПримечание: После открытия файла вы можете также блокировать определенные области файла (мы не рассматриваем этого в данной главе). Более подробная информация об операторе Lock содержится в справке VBA.
f
f
Пример
Давайте рассмотрим простой пример использования оператора open. Процедура, которая представлена в примере 7.10, открывает заданный файл в режиме последовательного доступа. Обратите внимание на тот факт, что параметры оператора разрешают выполнение только операций чтения из файла.
Пример 7.10. Открытие файла, доступного только для чтения, в режиме последовательного доступа
Sub dhTestOpen ()
Dim hFile As Long
Получает новый дескриптор файла. hFile = FreeFile
Открывает файл для последовательного доступа.
Open ”С:\TESTPROC.BAS" For Input Access Read _ Shared As hFile
’ Выполняет с ним какие-то действия...
’ Закрывает файл.
Close hFile
End Sub
He забывайте закрывать файлы!
Обратите внимание на то, что в процедуре примера 7.10 перед завершением работы выполняется оператор close. Этот оператор закрывает открытый файл. Очень важно закрывать файл, хранящийся на диске, после выполнения операций над ним. Если вы забудете закрыть файл, то рискуете заблокировать доступ к нему других пользователей (если он открыт в режиме индивидуального доступа) или потерять данные (при выходе из Windows).
Одновременно можно закрыть любое число файлов, передав оператору close несколько дескрипторов файлов, разделенных запятыми. Хотя для закрытия каждого файла сразу по завершении работы с ним мы рекомендуем использовать оператор Close, вы можете применить и оператор Reset для закрытия всех файлов, открытых приложением.
Операции с указателем текущей позиции файла
В большинстве случаев нецелесообразно без предварительной проверки читать и записывать данные в файлы на диске. Сначала следует получить информацию о том, насколько велик файл, над какими данными будет выполняться следующая операция чтения или записи и достигнут ли конец файла. VBA предлагает несколько функций, которые предоставят вам эти сведения.
Функции LOF и EOF
Функции lof и EOF информируют вас о размере файла и о том, достигнут ли конец файла. Каждая из них получает в качестве аргумента дескриптор файла, lof просто возвращает размер открытого файла в байтах.
Функция EOF возвращает значение типа Boolean, указывающее, находится ли текущая позиция для следующей операции чтения или записи в конце файла. Указатель текущей позиции файла поддерживается внутренними средствами VBA. Когда вы впервые открываете файл, VBA устанавливает значение указателя текущей позиции файла равным 0. При использовании функций ввода-вывода VBA изменяет это
значение. Например, после чтения двух байтов из файла, открытого в режиме Binary, значение указателя текущей позиции файла становится равным 2. Следующая операция чтения будет выполняться над третим байтом и т.д. После чтения всего файла позиция указателя соответствует последнему байту файла, и функция EOF возвращает значение True. Любая попытка прочитать большее количество данных приведет к ошибке.
Примечание: Для файлов, которые открыты в режиме Output, функция EOF всегда возвращает значение True.
Функция Loc и Seek
Функция Log используется для получения значения текущей позиции файла. В качестве аргумента функции указывается дескриптор файла. Результат, возвращаемый функцией, зависит от режима открытия файла. Для файлов, открытых в режиме Binary, функция Loc возвращает реальное значение (в байтах) текущей позиции файла. В режиме Random функция Loc возвращает не номер байта, а номер записи. В режиме последовательного доступа (Append, Input или Output), результат представляет собой число, полученное при делении значения текущей позиции файла (в байтах) на 128. (Нет, мы не знаем почему.)
Чтобы определить фактическую позицию (в байтах) указателя файла, открытого в режиме последовательного доступа, следует использовать Seek. Она работает подобно функции Loc в режимах Binary и Random, однако возвращает истинную позицию указателя в режиме последовательного доступа.
В VBA имеется также оператор Seek, который используется для установки позиции указателя файла, открытого в режиме Binary (в байтах) или Random (в записях). (Сам термин последовательный подсказывает, что вы не можете изменить текущую позицию для файлов, открытых в режиме последовательного доступа.)
Операторы чтения и записи
В VBA существует множество операторов для чтения и записи файлов. Выбор оператора зависит от режима открытия файла и формата, в котором вы хотите представить входные или выходные данные. В табл. 7.2 приведен список возможных операторов ввода-вывода.
Таблица 7.2. Операторы VBA для операций ввода-вывода файла
Режим открытияОператор(ы) чтенияОператор(ы) записи
Последовательный (input, input, Line input #, input #
Print #, Write #
Output ИЛИ Append)
Произвольный (Random) Get
Put
ДВОИЧНЫЙ (Binary)Input, Get
Put
Последовательный доступ
Файлы, открытые в режимах input, Output или Append, можно обрабатывать посредством одной из рассматриваемых ниже функций или операторов. Для чтения можно воспользоваться функцией input, операторами Line input # или input #. Запись в файлы последовательного доступа осуществляется с помощью операторов Print # И Write #.
Input
input является простейшей функцией чтения данных, которая используется при работе с текстами, не содержащими символов перевода строки, а также с файлами, включающими двоичные данные.
В примере 7.11 представлена процедура, которая по заданному имени файла отображает его содержимое в окне Проверка. Обратите внимание на то, что для определения конца файла используется функция EOF.
Пример 7.11. Вывод содержимого файла по байтам
Sub dhPrintBytes(strFile As String)
Dim hFile As Long
Получает новый дескриптор файла. hFile = FfeeFile
Открывает файл для последовательного доступа.
Open strFile For Input Access Read Shared As hFile
Выводит содержимое файла.
Do Until EOF(hFile)
Debug.Print Input (1, hFile);
Loop
’ Закрывает файл.
Close hFile End Sub
Эта процедура недостаточно эффективна, поскольку для обработки каждого байта файла вызываются две функции (EOF и input). Функция input более удобна при поиске информации, которая хранится поблочно. В примере 7.12 представлена улучшенная версия приведенной выше процедуры. Вместе с именем файла задается размер блока, при этом процедура считывает данные по блокам, а не по байтам.
Пример 7.12. Вывод содержимого файла по блокам
Sub dhPrintChunks(strFile As String, intSize As Integer)
Dim hFile As Long
Dim IngSize As Long
Dim intChunk As Integer
Получает новый дескриптор файла.
hFile = FreeFile
1 Открывает файл для последовательного доступа.
Open strFile For Input Access Read Shared As hFile
1 Определяет размер файла.
IngSize = LOF(hFile)
1 Выводит содержимое файла (сначала по блокам).
For intChunk = 1 То IngSize \ intSize
Debug.Print Input(intSize, hFile);
Next
1 Затем оставшиеся данные.
If (IngSize Mod intSize) > 0 Then
Debug.Print Input ( (IngSize Mod intSize), hFile)
End If
Закрывает файл.
Close hFile
End Sub
Обратите внимание на то, что для чтения каждого блока данных в процедуре используется цикл For. . .Next. Число итераций определяется путем целочисленного деления размера файла на размер блока. Если размер файла не делится на размер блока без остатка, то заключительная функция input считывает оставшуюся часть данных.
Эти методы извлечения данных были разработаны для Basic ранних версий, в которых возникали трудности при обработке данных больших объемов (например, строковые переменные раньше были ограничены 32 Кб). Теперь в 32-разрядной версии VBA строки переменной длины могут содержать более 2 миллиардов символов. Таким образом, всю операцию считывания данных из файла можно выполнить с помощью следующего оператора:
Debug.Print Input (LOF(hFile), hFile)
Line Input #
Для текстовых файлов, содержащих символы перевода строки (например, файлов конфигурации типа AUTOEXEC.BAT и WIN.INI), вместо функции input следует использовать оператор Line input #. В качестве параметров указываются дескриптор файла и переменная, в которую поочередно считываются строки из файла. (Оператор удаляет символы возврата каретки и перехода на новую строку.) Пример 7.13 содержит процедуру, которая в качестве параметра получает имя файла, открывает его в режиме доступа только для чтения и выводит каждую его строку в окне Проверка. Используя оператор Line input #, можно не беспокоиться о текущей позиции в байтах или размере буфера.
Пример 7.13. Вывод текстовых файлов, содержащих символы перевода строки, посредством оператора Line Input #
Sub dhPrintLines(strFile As String)
Dim hFile As Long
Dim strLine As String
1 Получает новый дескриптор файла.
hFile = FreeFile
1 Открывает файл для последовательного доступа.
Open strFile For Input Access Read Shared As hFile
1 Выводит содержимое файла.
Do Until EOF(hFile)
Line Input #hFile, strLine
Debug.Print strLine
Loop
1 Закрывает файл.
Close hFile
End Sub
Input #
Оператор input # — последний из рассматриваемых операторов, который используется для чтения файлов в режиме последовательного доступа. Получив дескриптор файла и набор переменных (разделенных между собой запятыми), он записывает данные из файла в эти переменные. Оператор input # удобно применять в том случае, если данные разделены запятыми, поскольку он отдельно воспринимает каждый фрагмент данных и записывает его в отдельную переменную. Кроме того, оператор input # автоматически удаляет кавычки по обе стороны текстовых значений и преобразует строки с датами в формат даты VBA.
Write #
Этот оператор является двойником оператора input #. В качестве параметров он получает дескриптор файла и список значений, разделенных запятыми. В результате выполнения оператора все значения записываются в файл. Кроме того, оператор разделяет выходные данные запятыми, заключает текст в кавычки, а значения в формате даты помещает между символами #. После этого файл идеально подходит для чтения посредством оператора input #. В примере 7.14 приведена процедура, демонстрирующая работу обоих операторов.
Пример 7.14. Использование операторов Write # и Input #
Sub dhWriteAndlnput (strFile As String)
Dim hFile As Long
ReDim varDataU To 5) As Variant
Dim i As Integer
Открывает файл для записи, заносит в него информацию и
’ закрывает.
hFile = FreeFile
Open strFile For Output Access Write As hFile
Write #hFile, "Some Text", "A Date:", Date, "A Number", 100
Close hFile
Затем снова открывает файл для чтения.
hFile = FreeFile
Open strFile For Input Access Read As hFile
Input #hFile, varData(l), varData(2), varData(3)z _
varData(4), varData(5) Close hFile
Выводит информацию. For i=l To 5
Debug.Print varData(i)
Next
End Sub
Процедура открывает файл для записи и записывает в него несколько значений. Если запустить программу на пошаговое выполнение и остановить сразу после первого оператора close, то, открыв файл, вы увидите, что его содержимое выглядит следующим образом:
"Some Text","A Date:",#1999-01-21#,"A Number",100
Затем процедура открывает этот файл снова, теперь уже как доступный только для чтения; с помощью оператора input # записывает данные в пять переменных типа
Variant, хранящихся в виде массива, а затем выводит их в окне Проверка.
!!Примечание: При использовании оператора input # количество переменных должно соответствовать количеству элементов данных в файле.
!
!
Print #
И наконец, рассмотрим оператор Print #. Он напоминает метод Print объекта Debug. И действительно, они во многом похожи. Print # записывает набор значений в файл, открытый в режиме Output или Append. Однако, в отличие от Write #, оператор Print # не выделяет данные кавычками или символами номера #. Кроме того, выводимые оператором Print # значения разделяются не запятыми, а символами табуляции. Если заменить в предыдущем примере оператор write # на Print #, то результирующий файл будет выглядеть следующим образом:
Some Text A Date: 1/21/99 A Number: 100
Поскольку файл не содержит запятых, его нельзя прочитать посредством оператора input #. Вместо него следует использовать функцию input или оператор Line input #.
Оператор Print # обычно работает с единственным фрагментом данных, как правило, с текстовой строкой. Пример 7.15 содержит процедуру для добавления номеров строк в текстовый файл. Она открывает один файл в режиме ввода, а другой, новый файл, — в режиме вывода. Затем циклически считывает каждую строку исходного файла и записывает посредством оператора Print # номер этой строки и саму строку в выходной файл.
Пример 7.15. Использование оператора Print # для вставки номеров строк в текстовый файл
Sub dhAddLineNumbers(strFileln As String, strFileOut As String) Dim hFileln As Long Dim hFileOut As Long Dim strinput As String Dim i As Integer
1 Открывает первый файл для чтения. hFileln = FreeFile
Open strFileln For Input Access Read As hFileln
1 Открывает второй файл для записи. hFileOut = FreeFile
Open strFileOut For Output Access Write As hFileOut
1 Поочередно читает каждую строку исходного файла, 1 добавляет ее номер и записывает в выходной файл. Do Until EOF(hFileln) i = i + 1 Line Input #hFileIn, strinput Print #hFileOut, i & ":”, strinput
Loop
1 Закрывает оба файла. Close hFileln Close hFileOut
End Sub
Внимание: При одновременной работе с несколькими файлами всегда вызывайте функцию FreeFile для получения дескриптора второго файла после открытия первого файла. В противном случае оба дескриптора совпадут (это объясняется тем, что первый дескриптор еще не задействован и остается свободным), поэтому при попытке открыть второй файл произойдет ошибка.
Произвольный доступ
Несмотря на свою непопулярность в мире современных средств работы с базами данных типа Microsoft Jet, функции VBA для произвольного доступа к файлам все еще используются для эмуляции работы с базами данных. Эти функции обрабатывают записи в форме определенных пользователями типов данных. При открытии файла для произвольного доступа в операторе Open должно быть задано значение размера записи. Все последующие операции ввода-вывода будут записывать (и считывать) данные в переменные (из переменных) заданного пользовательского типа. Кроме того, такие операторы, как Get и Put, осуществляют перезапись существующих внутри файла записей.
Для иллюстрации описанных возможностей введем следующий пользовательский тип данных, который служит для формирования списка сотрудников:
Type dhEmployee
ID As Integer
FirstName As String * 10 LastName As String * 10 Department As Integer HireDate As Date Salary As Currency End Type
Обратите внимание на строковые переменные фиксированной длины. Они необходимы, чтобы избежать ошибок при обработке записей. Поэтому размер записи следует указать при открытии файла. Если запись окажется переменной длины, то возможно появление ошибок.
В примере 7.16 приведены две функции для чтения из файла и записи в него данных в заданном выше формате. В каждой из них используется один и тот же оператор Open:
Open strFile For Random Access Read Write _
As hFile Len = Len(empln)
Обратите внимание на то, что файл открывается в режиме произвольного доступа и длина записи (которая определяется с помощью функции Len) передается оператору Open.
Пример 7.16. Операции чтения и записи в файл с произвольным доступом
Function dhReadEmp(strFile As String, _ emp As dhEmployee) As Boolean
Dim hFile As Long
Dim empln As dhEmployee
Открывает файл для произвольного доступа. hFile = FreeFile
Open strFile For Random Access Read Write _ As hFile Len = Len(empln)
Пытается найти определенного сотрудника в 1 существующих записях.
Do Until EOF(hFile)
Считывает запись. Get hFile, , empln
Сверяет идентификаторы.
If empln.ID = emp.ID Then emp = empln dhReadEmp = True Exit Do
End If
Loop
Close hFile
End Function
Function dhSaveEmp(strFile As String, _ empToSave As dhEmployee) As Boolean
Dim hFile As Long
Dim empln As dhEmployee
Dim IngRec As Long
Открывает файл для произвольного доступа.
hFile = FreeFile
Open strFile For Random Access Read Write _ As hFile Len = Len(empln)
Пытается найти определенного сотрудника в
существующих записях.
Do Until EOF(hFile)
IngRec = IngRec + 1
Считывает запись. Get hFile, IngRec, empln
Сверяет идентификаторы.
If empln.ID = empToSave.ID Then
Записывает новые данные и выходит.
Put hFile, IngRec, empToSave GoTo ExitHere
End If
Loop
Запись отсутствует, поэтому записывает
информацию в конец файла.
Put hFile, , empToSave
ExitHere:
dhSaveEmp = True
Close hFile
End Function
Каждая из функций просматривает в цикле содержимое всего файла, поочередно записывая данные о сотрудниках в переменную empln. Сама функция dhReadEmp не обрабатывает номер записи. Если запись найдена, функция возвращает ее в точку вызова. В свою очередь, процедура dhSaveEmp отслеживает номер записи посредством одной из переменных (IngRec). Если необходимая запись найдена, то процедура заменяет ее новой записью путем указания ее номера в качестве аргумента оператора Put. Это главная причина высокой эффективности рассматриваемого режима доступа к данным. Вы можете считывать из файла и записывать в него любую информацию, основываясь лишь на номере записи!
Конечно, никто не поверит в то, что эти программы смогут поддерживать работу настоящей базы данных. Ведь для поиска заданной записи эти процедуры циклически просматривают все записи в файле. Однако, используя сложные структуры данных, такие как связные списки и вложенные указатели, с помощью этих функций можно создавать довольно сложные базы данных. Зачем это нужно при наличии специальных технологий — это уже другой вопрос.
Windows API: настоящая эффективность
Как бы ни были хороши встроенные функции VBA, все же существует предел их возможностей, поскольку они не в состоянии обеспечить выполнение многих операций, необходимых для организации приложений. Например, они не позволяют определить объем свободной памяти на жестком диске или изменить метку тома диска. В этих и многих других случаях приходится обращаться за помощью к функциям API. В этом разделе рассмотрены лишь некоторые из многочисленных функций Windows API для работы с файлами и дисками.
Сравнение функций API с функциями VBA
Давайте для начала сравним уже известные вам функции VBA с их эквивалентами в Windows API. Вы заметите у них много общего. Это и неудивительно, ведь в конце концов при вызове VBA-функции VBA вызывает соответствующую функцию Windows API.
В табл. 7.3 приведен список функций VBA для работы с дисками и файлами в сравнении с аналогичными функциями Windows API. Если возможности функций Windows API чем-то отличаются от возможностей функций VBA, это отмечается в третьем столбце таблицы. Поскольку интерфейс вызова функций Windows API сло- женее по сравнению с вызовом функций VBA, то при отсутствии существенных преимуществ в их использовании мы не сочли нужным говорить о них.
Таблица 7.3. Сравнение функций Windows API и VBA для работы с файлами и дисками
Функция или оператор VBA
Эквивалентные функции API
Отличие функций API
ChDir, hDrive
SetCurrentDirectorу
Нет дополнительных функциональных возможностей
CurDir
GetCurrentDirectory
Нет дополнительных функциональных возможностей
Dir
FindFirstFile,
FindNextFile,
FindClose
Требуется больше вызовов функций, но и предоставляется больше информации о каждом файле
FileCopy
CopyFile
Позволяет избежать перезаписи существующего файла
FileDateTime
GetFileTime
Гораздо сложнее, но предоставляет информацию о времени всех событий, связанных с файлом (создания, последнего открытия и последней модификации)
FileLen
GetFileSize
Работает с огромными файлами (более 2 Гб)
GetAttr
•GetFileAttributes
Нет дополнительных функциональных возможностей
Kill
DeleteFile
Нет дополнительных функциональных возможностей
Функция или Эквивалентные оператор VBA функции API
Отличие функций API
MkDir, RmDirCreateDirectory,
CreateDirectoryEx, RemoveDirectorу
Поддержка атрибута защиты NT
NameMovefile
Нет дополнительных функциональных возможностей
SetAttrSetFileAttributes
Нет дополнительных функциональных возможностей
Open, Input, CreateFile,
Print, Write, ReadFile, WriteFile,
CloseCloseHandle
Все равно, что сравнивать яблоки с апельсинами (функцию API CreateFile необходимо использовать для получения дескрипторов файлов, но в общем применять функции VBA для операций ввода-вывода файлов гораздо проще)
Помимо этого, API включает множество других функций, не имеющих аналогов в VBA. В табл. 7.4 приведен список тех из них, которые упоминаются в настоящей главе. Существуют и другие, однако они большей частью слишком экзотичны или слишком сложны, чтобы их стоило здесь рассматривать.
Таблица 7.4. Функции Windows API, не имеющие аналогов в VBA
Функция
Описание
CompareFileTime
Сравнивает значения времени двух файлов
FindFiгstChangeNotification, FindNextChangeNotification, FindCloseChangeNotification
Сообщает Windows о необходимости уведомлять ваше приложение об изменениях файла или каталога
GetBinaryType
Выясняет, является ли файл исполняемым, и если является, то определяет его тип
GetDiskFreeSpace
Предоставляет информацию о диске, в том числе объем свободного дискового пространства
GetDriveType
Определяет тип диска (фиксированный, сменный, сетевой, компакт-диск или RAM-диск)
GetFileInformationByHandle
Предоставляет подробную информацию о файле при единственном вызове функции
GetFullPathName
Определяет полный путь доступа к файлу по указанному относительному пути
GetLogicalDrives, GetLogicalDrivestrings
Определяют имена логических дисков компьютера в виде битовой маски либо в виде строки, разделенной нулевыми символами
GetShortPathName
Определяет короткое (8.3) имя файла по указанному длинному имени файла
Функция
Описание
GetTempFileName
Определяет имя временного файла по имени каталога, символам префикса и необязательному уникальному целому числу
GetTempPath
Находит каталог, предназначенный для хранения временных файлов
GetVolumeInformation
Предоставляет информацию о файловой системе и заданном томе
SearchPath
Ищет файл по заданному пути или по системным путям, установленным по умолчанию
SetFileTime
Изменяет значения времени для заданного файла
SetVolumeLabel
Устанавливает метку тома диска
Получение информации о диске
Несмотря на большое количество функций для работы с файлами, VBA предлагает всего лишь несколько функций для работы с дисками, которых явно недостаточно, чтобы справиться со всеми возникающими проблемами. Например, нет возможности определить объем свободной памяти на диске или количество и тип дисков, установленных на вашем компьютере. К счастью, для решения этих и многих других задач на помощь приходит Windows API с его огромным числом функций.
Количество установленных дисков
Для решения этого вопроса лучше всего использовать функции GetLogicalDrives и GetLogicalDriveStrings, которые определяют количество дисков, установленных на вашем компьютере, включая как физические диски, так и подключенные по сети. Они отличаются друг от друга лишь тем, что представляют результат своей работы в разной форме, поэтому выберите ту из них, которая более удобна для дальнейшей работы.
Функция GetLogicalDrives сама по себе очень проста; она возвращает информацию о диске, собранную в одно значение типа Long. Каждый бит представляет букву диска (первый бит — диск А, второй — диск В и т.д.), где значение 1 является признаком наличия диска. Описание функции имеет следующий вид:
Declare Function GetLogicalDrives Lib ”kernel32” () As Long
Все очень просто. Небольшая сложность возникает лишь при расшифровке битов. Пример 7.17 содержит процедуру dhGetDriversByNum, которая получает результат вызова функции GetLogicalDrives и выполняет побитовое сравнение первых 26 битов. При обнаружении диска процедура добавляет его номер в семейство VBA, заданное в качестве аргумента.
Совет: В подобных ситуациях очень полезны объекты VBA Collection. Рассмотрите возможность использования семейств вместо массивов.
Пример 7.17. Поиск логических дисков по номеру
Function dhGetDrivesByNum(colDrives As Collection) _
As Integer
Dim IngDrives As Long
Dim intDrive As Integer
* Переустанавливает семейство.Set colDrives = New Collection
Определяет логические диски.
IngDrives = GetLogicalDrives()
* Выполняет побитовое сравнение первых 26 битов.
For intDrive = 0 То 25
If (IngDrives And (2 А intDrive)) <> 0 Then colDrives.Add intDrive, Chr(65 + intDrive) End If
Next
Возвращает количество обнаруженных дисков.
dhGetDrivesByNum = colDrives.Count
End Function
Использование функции GetLogicalDriveStrings связано с определенными трудностями, которые, однако, легко преодолимы. Функция GetLogicalDriveStrings возвращает буквы дисков в едином строковом буфере. Буквы разделены между собой символами Null (символами с ASCII-кодом 0), а вся строка завершается двумя символами Null. Пример 7.18 содержит процедуру dhGetDrivesByString, аналог dhGetDrivesByNum. После выделения буфера И вызова функции GetLogicalDriveStrings процедура dhGetDrivesByString производит ПОИСК в буфере символов Null. Подобно функции dhGetDrivesByNum она добавляет найденные буквы дисков в семейство, заданное в качестве аргумента.
Пример 7.18. Поиск логических дисков по букве
Function dhGetDrivesByString(colDrives As Collection) __ As Integer
Dim strBuffer As String
Dim IngBytes As Long
Dim intPos As Integer
Dim intPos2 As Integer
Dim strDrive As String
Переустанавливает семейство.
Set colDrives = New Collection
Устанавливает буфер. strBuffer = Space (255)
Получает строку с буквами логических дисков.
IngBytes = GetLogicalDriveStrings( _
Len(strBuffer), strBuffer)
* Производит поиск
* нулевого символа-разделителя. intPos2 = 1
intPos = InStr (intPos2, strBuffer, vbNullChar) Do Until intPos = 0 Or intPos > IngBytes
Определяет букву диска. strDrive = mid(strBuffer, intPos2, intPos - intPos2)
* Добавляет ее в семейство. colDrives.Add strDrive, strDrive
* Находит букву следующего диска. intPos2 = intPos + 1
intPos = InStr (intPos2, strBuffer, Chr(O)) Loop
Возвращает количество обнаруженных дисков. dhGetDrivesByString = colDrives.Count
End Function
Пример 7.19 содержит процедуру, которая тестирует каждый из рассмотренных методов. Вы можете запустить ее из окна Проверка. На рис. 7.2 представлены результаты работы этой процедуры.
Пример 7.19. Тестирование методов поиска логических дисков
Sub dhPrintDrives()
Dim colDrives As New Collection Dim varDrive As Variant
Сначала по номеру.
Debug.Print "Drives found: " & _ dhGetDrivesByNum(colDrives)
For Each varDrive In colDrives Debug.Print varDrive,
Next
Debug.Print
Затем по букве.
Debug.Print "Drives found: " & _ dhGetDrivesByString(colDrives)
For Each varDrive In colDrives Debug.Print varDrive,
Next
End Sub
| «И Проверка
к® в i
: dhPrintDrives
; Drives found:
5
1 0
2
3
17
25 ;
Drives found:
5
c:\
d:\
r:\
z:\ .
Lii
iilliii^^
iiiieie
Рис. 7.2. Информация о логических дисках
Каков тип дисков?
Определив, какие из дисков установлены в вашей системе, можно вызвать другую функцию Windows API GetDriveType, чтобы выяснить, каков тип каждого из них. Описание функции GetDriveType выглядит следующим образом:
Declare Function GetDriveType Lib "kernel32" _
Alias "GetDriveTypeA" (ByVai nDrive As String) As Long
GetDriveType получает в качестве исходного аргумента строку, представляющую корневой каталог диска (включая двоеточие и обратную косую черту), и возвращает код, указывающий тип диска. Код имеет одно из значений, приведенных в табл. 7.5.
Таблица 7.5. Константы типов дисков функции GetDriveType
Значение
Константа
Описание
0
DRIVEUN KNOWN
Диск не существует или невозможно определить его тип
1
DRIVENOROOT
Переданная строка не является корневым каталогом
2
DRIVER EMOVABLE
Сменный диск
3
DRIVEFIXED
Фиксированный диск
4
DRIVEREMOTE
Сетевой диск
5
DRIVE_CDROM
Компакт-диск
6
DRIVE_RAMDISK
RAM-диск
Совет: Если в качестве буквы диска задать константу vbNullstring, то функция GetDriveType возвращает информацию о текущем диске.
Для демонстрации работы функции API GetDriveType мы создали процедуру dhPrintDriveTypes (приведенную в примере 7.20), которая выводит в окне Проверка тип каждого установленного диска. Обратите внимание на то, что для формирования списка дисков она использует процедуру dhGetDrivesByString. На рис. 7.3 представлен результат выполнения этой процедуры.
Пример 7.20. Вывод типа каждого установленного диска
Sub dhPrintDriveTypes()
Dim colDrives As Collection
Dim varDrive As Variant
Dim IngType As Long
* Определяет буквы установленных дисков.
If dhGetDrivesByString(colDrives) > 0 Then For Each varDrive In colDrives
* Выводит букву диска. Debug.Print varDrive,
* Выводит тип диска.
IngType = GetDriveType(CStr(varDrive))
Select Case IngType
Case DRIVE_UNKNOWN
Debug.Print "Unknown”
Case DRIVE_NOROOT
Debug.Print "Unknown"
Case DRIVE_REMOVABLE
Debug.Print "Removable Media"
Case DRIVE_FIXED
Debug.Print "Fixed Disk"
Case DRIVE_REMOTE
Debug.Print "Network Drive"
Case DRIVE_CDROM
Debug.Print "CD-ROM"
Case DRIVE_RAMDISK
Debug.Print "RAM Disk"
End Select
Next
End If
End Sub
Ln Проверка
ЙЙВ1
! dhPrintDriveTypes
а:\Removable Media
■ с:\Fixed Disk
: d:\Network Drive
r:\CD-ROM
z:\Removable Media
Рис. 7.3. Информация о типе дисков
!!Примечание: Строка, передаваемая функции GetDriveType, должна представлять корневой каталог, иначе функция не сможет определить тип диска.
!
!
Определение объема свободной памяти на диске
По-видимому, данную задачу разработчикам VBA-приложений приходится решать чаще всего. В 16-разрядных версиях Basic ответить на этот вопрос было крайне сложно, поскольку для этого требовалось обрабатывать прерывания DOS — отнюдь нс простая задача для VBA. А в Win32 API для получения этой информации имеется простая функция — GetDiskFreeSpace. Действительно, данная функция может рассказать вам о диске гораздо больше, нежели просто проинформировать об объеме свободной памяти.
Описание функции GetDiskFreeSpace выглядит следующим образом:
Declare Function GetDiskFreeSpace Lib "kernel32" _
Alias "GetDiskFreeSpaceA" _
(ByVai IpRootPathName As String, _
IpSectorsPerCluster As Long, _ lpBytesPerSector As Long, _ lpNumberOfFreeClusters As Long, _ IpTotalNumberOfClusters As Long) As Long
Таким образом, даже в случае вызова специальной функции Microsoft не предоставляет возможности сразу получить значение объема свободной памяти на диске — вам придется вычислять его самостоятельно, основываясь на информации, полученной от этой функции. Так же, как и в случае с GetDriveType, функция должна получить строку, представляющую корневой каталог диска. Ей также передается четыре переменные типа Long, которые функция GetDiskFreeSpace заполняет информацией о диске (количество секторов в кластере, количество байтов в секторе, количество свободных кластеров и общее количество кластеров). При наличии этой информации вычисление объема свободной памяти на диске в байтах превращается в простое арифметическое упражнение.
Совет: Если вы передаете константу vbNulistring вместо буквы диска, то GetDiskFreeSpace возвращает информацию о текущем диске.
Мы свели это вычисление к двум удобным функциям, dhFreeDiskSpace и dhTotalDiskSpace, приведенным в примере 7.21. В нем также содержится тестовая процедура dhPrintDiskSpace, которая показывает, как вызывать эти две функции.
На рис. 7.4 представлен результат ее работы.
!!Примечание: Если функция GetDiskFreeSpace не выполняется (возвращает значение False), то обе функции dhFreeDiskSpace и dhTotalDiskSpace возвращают значение-1.
!
!
|| ;H Проверка
dhPrintDiskSpace&
DriveTotal BytesFree Bytes^
. a:\-1-1|
c:\121156403235028992|
d:\1201274880269778944
: r:\2971074560^
z:\1004318729355264Й
Рис. 7.4. Информация об объеме памяти на установленных дисках
Пример 7.21. Функции для вычисления общего и свободного объема памяти на диске
Function dhTotalDiskSpace(Optional strDrive As
String = vbNullString) As Long
Dim IngSectorsPerCluster As Long
Dim IngBytesPerSector As Long
Dim IngNumberOfFreeClusters As Long
Dim IngTotalNumberOfClusters As Long
' Вызывает функцию GetDiskFreeSpace.
If CBool(GetDiskFreeSpace(strDrive, _ IngSectorsPerCluster, _ IngBytesPerSector, _ IngNumberOfFreeClusters, _ IngTotalNumberOfClusters)) Then
В случае успешного выполнения рассчитывает общий 1 объем диска.
dhTotalDiskSpace = _ IngSectorsPerCluster * _ IngBytesPerSector * _ IngTotalNumberOfClusters
Else dhTotalDiskSpace = -1 End If End Function
Function dhFreeDiskSpace(Optional strDrive As String = vbNullString) As Long
Dim IngSectorsPerCluster As Long
Dim IngBytesPerSector As Long
Dim IngNumberOfFreeClusters As Long
Dim IngTotalNumberOfClusters As Long
* Вызывает функцию GetDiskFreeSpace.
If CBool(GetDiskFreeSpace(strDrive, _
IngSectorsPerCluster, _
IngBytesPerSector, _
IngNumberOfFreeClusters, _
IngTotalNumberOfClusters)) Then
При успешном выполнении рассчитывает объем
* свободной памяти на диске»
dhFreeDiskSpace = _
IngSectorsPerCluster * _
IngBytesPerSector * _
IngNumberOfFreeClusters
Else dhFreeDiskSpace = -1 End If
End Function
Sub dhPrintDiskSpace()
Dim colDrives As New Collection
Dim varDrive As Variant
1 Определяет буквы дисков.
If dhGetDrivesByString(colDrives) > 0 Then
* Выводит заголовок.
Debug.Print "Drive", "Total Bytes", "Free Bytes"
* Вывод информации об объеме памяти на ' всех дисках.
For Each varDrive In colDrives
Debug.Print varDrive, _
dhTotalDiskSpace(CStr(varDrive)), _
dhFreeDiskSpace(CStr(varDrive))
Next
End If
End Sub
Метки дисков
Остальные функции API для работы с дисками, рассмотренные в этой главе, касаются меток тома. Вы знаете о том, что при форматировании диску можно присваивать в качестве метки 11-символьное строковое значение. Пользуясь функцией VBA Dir, можно определить метку тома диска, но нельзя ее задать. Посредством функций Windows API GetVolumelnformation и SetVolumeLabel имеется возможность сделать и это, и многое другое.
Помимо метки тома, GetVolumelnf ormation возвращает информацию о серийном номере тома, максимальной длине имени файла и типе файловой системы. Описание функции GetVolumelnformation выглядит следующим образом:
Declare Function GetVolumelnformation Lib "kernel32" _
Alias "GetVolumelnformationA" _
(ByVai IpRootPathName As String, _
ByVai IpVolumeNameBuffer As String, _
ByVai nVolumeNameSize As Long, _
IpVolumeSerialNumber As Long, _
IpMaximumComponentLength As Long, _
IpFileSystemFlags As Long, _
ByVai IpFileSystemNameBuffer As String, _
ByVai nFileSystemNameSize As Long) As Long
Мы создали процедуру, которая получает в качестве параметра букву корневого каталога диска и выводит в окне Проверка информацию о томе.
Двойник GetVolumelnformation, функция SetVolumeLabel, также очень проста и имеет единственное предназначение — назначать метку тома диска. Описание этой функции выглядит следующим образом:
Declare Function SetVolumeLabel Lib ”kernel32” _
Alias "SetVolumeLabelA" (ByVai IpRootPathName As String,_
ByVai IpVolumeName As String) As Long
Подобно аналогичным функциям, в качестве первого аргумента функции SetVolumeLabel передается строка с буквой корневого каталога. Второй аргумент — строка с новой меткой тома. При желании вы можете удалить метку тома вообще. Однако если в Windows NT для этого достаточно передать функции нулевой указатель (vbNullstring), то в Windows 95 вследствие присутствующей там ошибки, вам придется вводить для этого пустую строку (” ”). Когда функции SetVolumeLabel удается сменить метку тома, она возвращает значение True, в противном случае — False.
Операции с путями доступа
Одна из наиболее распространенных (и наиболее сложных) задач для программистов на VBA заключается в выполнении операций с путями доступа к файлам. Определяется ли полный путь по относительному, проводится ли анализ пути или определяется краткая форма пути — на решение этих вопросов затрачивается немало времени.
Синтаксический анализ пути к файлу
Часто требуется разбить полный путь доступа к файлу на его составляющие. Обычно это означает отделение имени файла от пути доступа к нему. Кроме того, может потребоваться обработать расширение файла как отдельную составляющую. Наконец, иногда необходимо разбить путь на отдельные каталоги. Рассмотренные здесь функции VBA помогут справиться с подобными задачами.
Примечание: Процедуры этой главы используют строковые функции, опи- v I санные в главе 1. Там вы сможете получить о них более подробную инфор- I мацию.
Функция VBA dhParsePath в примере 7.22 получает полный путь доступа к файлу и разделяет его на составляющие. Обратите внимание на то, что она получает переменные для результирующих составляющих по ссылке и изменяет значения этих переменных. Дополнительный необязательный аргумент varExt представляет собой переменную для расширения файла. Если он передается функции dhParsePath, тогда расширение файла отделяется от его имени и помещается в указанную переменную. Если этот аргумент не вводится, dhParsePath включает расширение в имя файла. Вы можете протестировать данную функцию путем вызова процедуры dhTestParsePath, которая рассмотрена немного ниже, в разделе "Пример синтаксического разбора пути".
Пример 7.22. Функция, которая разделяет путь доступа к файлу на составляющие
Sub dhParsePath(ByVai strFullPath As String, _ ByRef strPath As String, ByRef strFile As String, _ Optional ByRef varExt As Variant)
Dim IngPos As Long
’ Если переменная varExt задана, определяет ’ расширение файла.
If Not IsMissing(varExt) Then
IngPos = dhRInstr(strFullPath, ”. ") If IngPos > 0 Then varExt = Mid(strFullPath, IngPos + 1) Else varExt = ”" End If Else varExt = ”" End If ’ Далее находит имя файла, удаляя при необходимости его ’ расширение.
IngPos = dhRInstr(strFullPath, "Х”) If IngPos > 0 Then
strFile = Mid(strFullPath, IngPos + 1, _ Len (strFullPath) - IngPos - Len(varExt)) If Len(varExt) Then strFile = Left(strFile, Len(strFile) - 1) End If strPath = Left (strFullPath, IngPos - 1) End If End Sub
В примере 7.23 приведена более сложная функция dhGetPathParts. Она разбивает полный путь на составляющие элементы, каждый из которых представляет один из подкаталогов в пути доступа к заданному файлу. При вызове функции ей передается путь, а также объект VBA Collection. Функция помещает в семейство элементы пути, в том числе букву диска, каждый из подкаталогов и имя файла.
Пример 7.23. Разбиение пути на составные части
Function dhGetPathParts(strPath As String, _ coIParts As Collection) As Long Dim intPos As Integer Dim intFound As Integer Dim strFound As String intPos = 1 Do
* Ищет первый символ обратной косой черты. intFound = InStr(intPos, strPath, "X")
* Если он найден, добавляет в семейство текст между
* этой и предыдущей косой чертой.
If intFound > 0 Then strFound = Mid(strPath, intPos, intFound - intPos) coIParts.Add strFound intPos = intFound + 1
Else
Символов косой черты больше нет, поэтому
добавляет остаток строки.
coIParts.Add Mid(strPath, intPos)
End If
Loop While intFound > 0
* Возвращает количество элементов пути.
dhGetPathParts = coIParts.Count
End Function
Определение полного пути к файлу и его краткой формы
Windows API также содержит несколько функций для работы с путями доступа. Некоторые из них, подобные GetTempPath, обсуждаются в других разделах этой главы, поскольку имеют отношение к другим темам. Здесь же мы рассмотрим две функции — GetFullPathName и GetShortPathName. Они описываются следующим образом:
Declare Function GetFullPathName Lib "kernel32" _
Alias "GetFullPathNameA" _
(ByVai IpFileName As String, ByVai nBufferLenght As Long, _
ByVai IpBuffer As String, ByVai IpFilePart As String) As Long
Declare Function GetShortPathName Lib "kernel32" _
Alias "GetShortPathNameA" _
(ByVai IpszLongPath As String, _
ByVai IpszShortPath As String, _
ByVai cchBuffer As Long) As Long
Единственной задачей функции GetFullPathName является воссоздание реального полного пути файла по заданным на основе текущего каталога относительному пути и имени файла. Это означает, что если вы передаете только имя файла, функция GetFullPathName присоединяет к нему путь к текущему каталогу. Если передается относительный путь, например "..\..\SOMEFILE.TXT", то GetFullPathName дополняет его до полного пути и возвращает результат. Мы написали VBA-процедуру dhFullPath для вызова функции GetFullPathName, приведенную в примере 7.24.
Пример 7.24. Функция dhFullPath, дополняющая относительные пути и имена файлов
Function dhFullPath(strPath As String) As String
Dim strBuffer As String
Dim strFilePart As String
Dim IngBytes As Long
’ Устанавливает буфер.
strBuffer = Space(МАХ_РАТН)
’ Вызывает функцию GetFullPathName.
IngBytes = GetFullPathName(strPath, Len(strBuffer), _ strBuffer, strFilePart)
При успешном выполнении анализирует содержимое буфера.
If IngBytes > 0 Then dhFullPath = Left(strBuffer, IngBytes) End If
End Function
Чтобы протестировать данную функцию, запустите ее из окна Проверка следующим образом:
?dhFullPath(Dir("*.*"))
Оператор Dir возвращает имя первого файла в текущем каталоге. Затем функция dhFullPath определяет полный путь к файлу. Однако будьте осторожны при ее использовании, поскольку GetFullPathName не проверяет, верно ли полученное в итоге имя файла и действительно ли существует этот файл. Например, следующий оператор отыскивает первый файл в каталоге, расположенном над текущим, и добавляет его к имени текущего каталога.
?dhFullPath(Dir ("..\*.*м))
Даже если такой файл и существует, путь указан неверно.
Функция GetShortPathName применяется в тех случаях, когда вы работаете с именами файлов в старом формате (8.3). Приложению может понадобиться произвести обмен файлами с другой системой, которая не поддерживает длинных имен (например, с Windows 3.x). Функция GetShortPathName получает длинное имя файла и возвращает его краткую форму (8.3). Мы создали для ее вызова простую функцию dhShortPath, приведенную в примере 7.25.
Пример 7.25. Функция dhShortPath определяет краткую форму имени файла
Function dhShortPath(strPath As String) As String
Dim strBuffer As String
Dim IngBytes As Long
’ Устанавливает буфер.
strBuffer = Space(MAX_PATH)
* Вызывает функцию GetShortPathName.
IngBytes = GetShortPathName(strPath, strBuffer, _ Len(strBuffer) )
* Если вызов функции удачен, анализирует содержимое буфера.
If IngBytes > 0 Then dhShortPath = Left(strBuffer, IngBytes) End If
End Function
Пример синтаксического разбора пути
Пример 7.26 содержит процедуру, демонстрирующую работу рассмотренных выше функций. Сначала извлекается первый файл текущего каталога, а затем вызываются другие функции для вычисления полного пути, краткой формы пути и частей пути. На рис. 7.5 показан возможный результат выполнения этой процедуры.
Пример 7.26. Процедура, выполняющая синтаксический разбор пути
Sub dhTestParsePath()
Dim strCurrFile As String
Dim strFullPath As String
Dim strShortPath As String
Dim strPath As String Dim strFile As String Dim varExt As Variant Dim IngParts As Long Dim coIParts As New Collection Dim varPart As Variant
* Определяет первый файл текущего каталога. strCurrFile = Dir ("*.*")
’ Получает полный путь доступа к нему. strFullPath = dhFullPath(strCurrFile)
Получает путь доступа с именем файла в краткой форме. strShortPath = dhShortPath(strFullPath)
Анализирует компоненты пути.
Call dhParsePath(strFullPath, strPath, strFile, varExt)
Разбивает путь на его составные части.
IngParts = dhGetPathParts(strFullPath, coIParts)
Выводит информацию в окне Проверка.
Debug.Print "File:”, strCurrFile
Debug.Print "Full path:", strFullPath
Debug.Print "Short path:", strShortPath
Debug.Print "Path:", strPath
Debug.Print "Filename:", strFile
Debug.Print "Extension:", varExt
Debug.Print "Path parts:", IngParts
IngParts = 2 For Each varPart In coIParts
Debug.Print Space(IngParts) & varPart IngParts = IngParts + 2
Next
End Sub
рЯПроверка
№6]|
dhTestParsePathА
:File:Declaration of Independence.doc
Full pathC:\My.Documents\DeclarationofIndependence.doc
Short path:C:\MYDOCU-1\DECLAR-1.DOC
Path:C:\My Documents'
;File Name:Declaration of Independence
Extension:doc
. Path parts:3
Clr
My Documentsp|;
Declaration ofIndependence.doc* .
Рис. 7.5. Результат синтаксического разбора пути доступа к файлу
Эффективная замена функции Dir
Если функция VBA Dir не вполне подходит для решения стоящих перед вами задач, рассмотрите возможность использования лежащих в ее основе функций Windows API: FindFirstFile, FindNextFile и Findclose. Все они по отдельности воспроизводят функциональные возможности различных вариантов функции Dir. Функция FindFirstFile инициирует поиск в каталоге и возвращает первое удовлетворяющее заданным требованиям имя файла так же, как это делает Dir при передаче ей спецификации файла. Функция FindNextFile определяет следующее подходящее под спецификацию имя файла, а функция Findclose завершает поиск. Этот последний шаг выполняется неявным образом при вызове функции Dir с новой спецификацией файла.
Однако возможности этих функций гораздо шире возможностей функции Dir. Помимо поиска соответствующих имен файлов они возвращают дополнительную информацию о каждом из них, а именно: время создания, размер файла и его имя в формате 8.3. Опсания этих функций имеют следующий вид:
Функции поиска файлов в текущем каталоге.
Declare Function FindFirstFile Lib "kernel32" _
Alias "FindFirstFileA"(ByVai IpFileName As String, _ IpFindFileData As WIN32_FIND_DATA) As Long
Declare Function FindNextFile Lib ”kernel32”
Alias "FindNextFileA"(ByVai hFindFile As Long, _
IpFindFileData As WIN32_FIND_DATA) As Long
Declare Function FindClose Lib ,,kernel32" _
(ByVai hFindFile As Long) As Long
Примечание: В действительности указанные функции не ищут файлы, по крайней мере в том смысле, что они не просматривают весь жесткий диск. Для поиска на всем диске необходимо написать специальную программу на VBA. Если же вам необходимо лишь найти файл по заданному пути поиска, обратитесь к функции API SearchPath, рассматриваемой ниже, в разделе "Поиск файлов" данной главы.
Вызов функций поиска файлов
Для задания спецификации файла при вызове функции FindFirstFile используются те же правила, что и при передаче спецификации файла в функцию Dir: вы можете задавать полный или относительный путь поиска, использовать символы-заменители и UNC-имена серверов. Кроме того, нужно передать указатель на структуру WIN32_FIND_DATA. Функция FindFirstFile заполняет ее элементы информацией о файле, который соответствует заданным параметрам поиска. Структура объявляется следующим образом:
Атрибуты файла.Время создания.Время последнего доступа.Время последнего изменения Размер(старшее слово). Размер(младшее слово).Зарезервирована.Зарезервирована.Имя файла.Имя файла в формате 8.3.Атрибуты файла.Время создания.Время последнего доступа.Время последнего изменения Размер(старшее слово). Размер(младшее слово).Зарезервирована.Зарезервирована.Имя файла.Имя файла в формате 8.3.Type WIN32_FIND_DATA IngFileAttributes As Long ftCreationTime As FILETIME ftLastAccessTime As FILETIME ftLastWriteTime As FILETIME IngFileSizeHigh As Long IngFileSizeLow As Long IngReservedO As Long IngReservedl As Long strFileName As String * MAX_PATH strAlternate As String * 14
Атрибуты файла.
Время создания.
Время последнего доступа.
Время последнего изменения Размер(старшее слово). Размер(младшее слово).
Зарезервирована.
Зарезервирована.
Имя файла.
Имя файла в формате 8.3.
Атрибуты файла.
Время создания.
Время последнего доступа.
Время последнего изменения Размер(старшее слово). Размер(младшее слово).
Зарезервирована.
Зарезервирована.
Имя файла.
Имя файла в формате 8.3.
End Type
Примечание: Переменная мах_ратн определяется равной 260, что составляет максимальную длину пути, которая допускается в Windows.
Как видите, вы можете получить достаточно информации о файле с помощью одного только вызова функции. (Более подробная информация о структуре filetime приведена в разделе “Функции даты и времени Windows API”.)
Если функция FindFirstFile находит файл, соответствующий переданной спецификации, она заполняет структуру win32_find_data и возвращает дескриптор данной операции поиска. Этот дескриптор используется при последующих вызовах функции FindNextFile. Если же вы нс желаете больше продолжать поиск, вызовите функцию Findclose, передав ей дескриптор операции, которую вы хотите завершить.
Если функция FindFirstFile нс выполнена успешно, она возвращает значение -1. В этом случае можно получить код ошибки, возвращаемый функцией DLL, который определен свойством LastDLLError объекта Err. Функция FindNextFile просто возвращает значение True, если следующая операция поиска выполнена успешно, и значение False в противном случае.
Теперь можно сформировать простую функцию, составляющую список файлов заданного каталога. Пример 7.27 содержит процедуру dhFindFiles, которая выполняет эту задачу. Сначала она вызывает функцию FindFirstFile, передавая ей в качестве аргумента путь поиска. Если функция выполняется успешно, процедура вызывает функцию FindNextFile внутри цикла Do. . . Loop, который выполняется до тех пор, пока функция FindNextFile не возвратит значение False.
Пример 7.27. Вывод списка всех файлов заданного каталога с помощью функций API
Sub dhFindFiles(strPath As String)
Dim fd As WIN32_FIND_DATA
Dim hFind As Long
Ищет первый файл.
HFind = FindFirstFile(strPath, fd)
Если поиск удался, ... If hFind > 0 Then
Do
... выводит информацию о файле. With fd
Debug.Print dhTrimNull(.strFileName), _ .IngFileSizeLow & " bytes", _ dhBuildAttrString(.IngFileAttributes) End With
Ищет следующий файл, и все повторяется до тех пор, 1 пока не будут найдены все файлы.
Loop While CBool(FindNeextFile(hFind, fd))
* Завершает операцию поиска.
Call FindClose(hFind)
End If
End Sub
Iba Проверка
: dhFindFiles ("С
IO.DOS
40774 bytes
RHS
MSDOS.DOS
38138 bytes
RHS
COMMAND. DOS
54645 bytes
A
; COMMAND.COM
92870 bytes
A
BOOTLOG.TXT
23952 bytes
H A
AUTOEXEC.BAT
202 bytes
A
AUTOEXEC.DOS
107 bytes
A
CONFIG.DOS
258 bytes
A
: SUHDLOG.DAT
5166 bytes
RH
' MSDOS.
22 bytes
H
; SETUPLOG.TXT
49701 bytes
H A
CONFIG.SYS
245 bytes
A
WINDOWS
0 bytes
D
MSDOS.SYS
1641 bytes
RHSA
; DBLSPACE.BIN
71278 bytes
RHS
BOOTLOG.PRV
23945 bytes
H A
Program Files
0 bytes
R D
SYSTEM.1ST
36194 bytes
RHS
DRVSPACE.BIN
71278 bytes
RHS
DETLOG.OLD
36194 bytes
HS
10.SYS
223148 bytes
RHS
; RECYCLED
1ш_щ' -
0 bytes
HS D
Puc. 7.6. Информация о файлах, полученная путем вызова процедуры dhFindFiles
Поскольку элементы структуры win32_find_data являются строками фиксированной длины, процедура должна производить анализ полученных результатов путем поиска завершающего символа Null.
Для проверки работы процедуры вызовите ее из окна Проверка. На рис. 7.6 показан результат ее выполнения.
Новая модифицированная функция Dir
Наличие этой дополнительной информации предоставляет в наше распоряжение несколько интересных возможностей. Мы воспользовались функциями API для создания замены VBA-функции Dir. Для чего это нужно? Основной недостаток функции Dir заключается в том, что хотя она позволяет производить поиск файлов с определенными атрибутами, она не дает возможности ограничить его только поиском файлов, имеющих заданный набор атрибутов. Предлагаемый нами вариант, dhDir, предоставляет такую возможность.
Пример 7.28 содержит функцию dhDir вместе со вспомогательной функцией dhFindByAttr. Функция dhDir построена таким образом, чтобы вы могли вызывать ее так же, как и функцию VBA Dir. В частности, чтобы начать поиск, вы задаете ей путь поиска, а затем вызываете ее уже без аргументов, чтобы найти другие файлы, которые удовлетворяют требованиям, определенным при первом вызове функции. Однако в отличие от Dir, наша функция выполняет исключающий поиск при передаче ей соответствующего значения атрибута. Вы можете отменить это свойство, задав значение False в качестве третьего необязательного параметра, и таким образом вернуться к тем же самым функциональным возможностям, которыми обладает Dir.
Пример 7.28. Замена VBA-функции Dir
Function dhDir(Optional ByVai strPath As String = "", _
Optional IngAttributes As Long = vbNormal, _
Optional fExclusive As Boolean = True) As String
Dim fd As WIN32_FIND_DATA
Static hFind As Long
Static IngAttr As Long
Static fEx As Boolean
1 Если путь не задан, пытается найти следующий файл.
If strPath = "" Then
If hFind > 0 Then
If CBool(FindNextFile(hFind, fd)) Then dhDir = dhFindByAttr(hFind, fd, IngAttr, fEx) End If End If
* В противном случае начинает новый поиск.
Else
* Закрывает последнюю операцию поиска, ’ если она существовала.
If hFind > 0 Then
Call FindClose(hFind)
End If
* Сохраняет атрибуты и установку исключающего поиска.
IngAttr = IngAttributes fEx = fExclusive
Если путь заканчивается обратной косой чертой, 1 предполагает поиск всех файлов и добавляет ”*.*"
If Right(strPath, 1) = "V Then strPath = strPath & "*.*"
End If
Ищет первый файл.
hFind = FindFirstFile(strPath, fd)
If hFind > 0 Then
dhDir = dhFindByAttr(hFind, fd, IngAttr, fEx) End If
End If
End Function
Function dhFindByAttr(hFind As Long, _
fd As WIN32_FIND_DATA, IngAttr As Long, _ fExclusive As Boolean) As String
Dim fOk As Boolean
Продолжает поиск файлов до тех пор, пока не найдется хоть
’ один файл, в точности соответствующий заданным атрибутам
’ (значение fExclusive равно True) или включающий
* заданные атрибуты (значение fExclusive равно False).
Do
If fExclusive Then
fOk = fd.IngFileAttributes = IngAttr
Else fOk = (fd.IngFileAttributes And IngAttr) = IngAttr End If
If fOk Then
dhFindByAttr = dhTrimNull(fd.strFilename) Exit Do
End If
Loop While FindNextFile(hFind, fd) End Function
Наша функция сначала вызывает FindFirstFile для нового поиска (только в том случае, если в качестве первого аргумента вы задали спецификацию файла). dhFindByAttr проверяет атрибуты рассматриваемого файла на соответствие заданным требованиям и, если соответствие не обнаружено, использует функцию FindNextFile для поиска имени следующего файла. Это продолжается до тех пор, пока не будет найден файл, подходящий под заданную спецификацию, или пока не окажется, что ни одного такого файла не существует. Статические переменные функции dhDir используются для сохранения значений параметров между последовательными вызовами функции.
Рис. 7.7 иллюстрирует пример вызова функции dhDir для поиска всех файлов корневого каталога с атрибутами Read Only, System и Hidden. Первый вызов функции dhDir устанавливает параметры поиска. При последующих вызовах возвращаются другие файлы, у которых установлены именно эти три атрибута.
|| Я Проверка
№В||
?dhDir ("С:\*. | 10.DOS
*", vbReadOnly + vbHidden -
H vbSystern)
] ?dhDir
10.SYS
; DRVSPACE.BIN
; SYSTEM.1ST
DBLSPACE.BIN
MSDOS.DOS
4 1 1 *’ ^''
’'' '' >-Л ф s' i' 1^ A^#'1 * VAX
Whesarf-i
Рис. 7.7. Вызов процедуры dhDir для выполнения исключающего поиска в каталоге
На рис. 7.8 показано содержимое этого же каталога, однако на этот раз проведенный поиск не был исключающим. Он возвращает все файлы, у которых установлены такие атрибуты, независимо от того, установлены у них еще какие-нибудь атрибуты или нет. Таким образом, файл MSDOS.SYS с тремя указанными атрибутами плюс архивный атрибут также включен в результат выполнения данной операции поиска.
№В№В;Я1 Проверка
№В
№В
?dhDir ("C.A*.*", vbReadOnly + vbHidden + vbSystem, False) 10.DOS
?dhDir
IO.SYS DRVSPACE.BIN SYSTEM.1ST DBLSPACE.BIN MSDOS.SYS MSDOS.DOS
>5
Puc. 7.8. Вызов процедуры dhDir без установки исключающего поиска
Использование истинных возможностей функции FindFirstFile
Вас может удивить, зачем нужно преодолевать все эти трудности ради минимального перевеса в функциональных возможностях над функцией Dir. Во-первых, функция dhDir служит для решения определенного круга задач. Вспомните, как часто вам требовалось получить список только подкаталогов, не включая в него имен
файлов. Во-вторых, истинная эффективность функций API заключается в той дополнительной информации, которую они предоставляют как часть результата своей работы.
Функции уведомления Windows
Нуждались ли вы когда-либо в информации о том, что содержимое файла или каталога было изменено? Представьте себе, что нужно написать приложение, которое будет контролировать поступление файлов в сетевой каталог. Было бы очень полезно дать Windows возможность сообщать приложению о получении нового файла. Подобные возможности можно реализовать посредством функций уведомления, которые рассмотрены в этом разделе.
Как работают уведомления об изменениях
Функции FindFirstChangeNotification, FindNextChangeNotification и FindCloseChangeNotification сходны с только что рассмотренными функциями поиска, однако вместо немедленного возвращения результатов они устанавливают дескрипторы изменений, которые передаются операционной системе. Каждый дескриптор изменений указывает на изменение определенного состояния, например имени каталога. Когда происходит подобное событие, Windows уведомляет о нем пользовательское приложение. Затем приложение может продолжить контролировать каталог или отменить действие дескриптора изменений.
Этот эффект можно наблюдать при использовании Проводника Windows. Оставьте окно каталога открытым и посредством другого приложения скопируйте в этот каталог какой-нибудь файл. Проводник незамедлительно отобразит новый файл, и вам даже не потребуется вручную обновлять содержимое окна.
Контролировать можно одно или несколько событий, используя комбинацию значений из табл. 7.6.
Таблица 7.6. Значения для контроля событий
Константа
Описание
FILE_NOTfFY_CHANGE_FILE_NAME
Создание, удаление или переименование файлов
FILE_NOTIFY_CHANGE_DIR_NAME
Создание, удаление или переименование каталогов
FILE_NOTIFY_CHANGE_ATTRIBUTES
Изменения атрибутов файла или каталога
FILE_NOTIFY_CHANGE_SIZE
Изменения размера файла
FILE_NOT1FY_CHANGE_LAST_WRITE
Изменения времени последней модификации файла
FILE_NOTIFY_CHANGE_SECURITY
Изменения параметров защиты файла (только для Windows NT)
Установка уведомлений об изменениях
Вы можете установить уведомление об изменении с помощью функции FindFirstChangeNotif ication. Она описывается следующим образом:
Declare Function FindFirstChangeNotification Lib ”kernel32” _
Alias "FindFirstChangeNotificationA" _
(ByVai IpPathName As String, ByVai bWatchSubtree As Long, _
ByVai dwNotifyFilter As Long) As Long
Аргумент IpPathName — это имя каталога, состояние которого необходимо контролировать. Если вы хотите осуществлять также контроль за изменениями в его подкаталогах, передайте значение 1 в качестве второго аргумента. Третий аргумент — это комбинация значений из табл. 7.6. Если вызов функции прошел успешно, то FindFirstChangeNotification возвратит дескриптор изменения. (В противном случае будет возвращено значение -1.) Полученный дескриптор нужно передать другой функции Windows API, WaitForSingleObject:
Declare Function WaitForSingleObject Lib "kernel32" _
(ByVai hHandle As Long, ByVai dwMilliseconds As Long) _ As Long
Функция WaitForSingleObject представляет собой общую функцию, разработанную для работы с дескрипторами уведомлений Windows. При вызове этой функции она не возвращает результат до тех пор, пока не будет выполнено одно из следующих условий:
произойдет событие, связанное с дескриптором изменения;
пройдет заданное вторым аргументом количество миллисекунд.
Совет: Если хотите ждать в течение неограниченного времени, передайте в ^jjy^ качестве второго аргумента значение &HFFFF.
В этом заключается оборотная сторона медали: процедура VBA будет ждать до тех пор, пока одно из указанных событий не произойдет, тем самым фактически останавливая работу вашего приложения. Поэтому использование данных функций ограничено в однопотоковых средах, подобных текущей версии VBA (хотя, если VBA станет многопотоковой средой, то эти функции будут крайне полезны).
Что делать в случае уведомления
Когда функция WaitForSingleObject возвращает результат, он указывает на то, что произошло событие (WAIT_OBJECT_0) или же истек период ожидания (WAIT_TIMEOUT). Основываясь на полученном значении, вы можете предпринять соответствующие действия. В нашем примере с приходом файлов в сетевой каталог возвращенное значение WAIT_OBJECT_0 послужило бы указанием просмотреть каталог в поисках нового файла и начать процесс его обработки.
Если нужно продолжить процесс ожидания другого изменения, вызовите функцию FindNextChangeNotif ication, передав ей дескриптор изменения, полученный ранее посредством функции FindFirstChangeNotification. Функция FindNextChangeNotification возвращает значение True, если Windows готова продолжать контроль изменений в данном каталоге. Тогда вызовите снова WaitForSingleObject, и процесс повторится. Если контролировать изменения больше нет необходимости, вызовите функцию FindCloseChangeNotification.
Использование уведомлений об изменениях
По указанным выше причинам работу этих функций трудно продемонстрировать с помощью VBA. Однако для иллюстрации использования этих функций мы создали небольшую игру. Ее можно рассматривать как компьютерную версию игрового автомата с маленькими сусликами, которые выскакивают из произвольно открывающихся норок. Цель игры состоит в том, чтобы успеть ударить каждого суслика колотушкой, пока он снова не спрячется в свою нору. Пример 7.29 содержит процедуру dhFunWithNotifу, которая является основой нашей игры.
Пример 7.29. Протестируйте функции уведомления, а заодно немного развлекитесь
Sub dhFunWithNotify(strPath As String, _
ByVai IngTimeout As Long)
Dim colPaths As New Collection
Dim strFile As String
Dim HChange As Long
Dim IngStatus As Long
Dim IngFlags As Long
Dim fKeepGoing As Boolean
Dim IngScore As Long
Dim IngTotalScore As Long
Const dhcBaseScore = 100000
* Создает список подкаталогов, расположенных ниже strPath.
Debug.Print "Building directory list..."
If dhGerSubdirectories(strPath, colPaths) = 0 Then
Debug.Print "Could not build subdirectory list!" Exit Sub
End If
* Устанавливает флаги.
IngFlags = FILE_NOTIFY_CHANGE_FILE_NAME
* Создает первый файл.
Debug.Print "Here we go!!!"
strFile = dhCreateTempFile(dhGetRandomFile(colPaths), _
"~DH")
If strFile = "" Then
Debug.Print "Error creating first file!" Exit Sub
End If
’ Создает первое уведомление об изменении.
hChange = FindFirstChangeNotification(strPath, _
1, IngFlags)
’ Проверяет успешность завершения.
If hChange > 0 Then
* Цикл прекращается, если время ожидания прошло, но функция
’ уведомления не сработала или время ожидания достигло нуля.
Do
Выводит относительное имя файла.
Debug.Print "You have " & IngTimeout / 1000 & _ " seconds to delete:"
Debug.Print ”..\" & Mid(strFile, Len(strPath) + 2)
1 Ожидает изменения.
IngStatus = WaitForSingleObject(hChange, _ IngTimeout)
* Что произошло?
Select Case IngStatus
Case WAIT_OBJECT_0
* Произошло изменение! Проверяет, ’ правильный ли файл был удален. If Dir (strFile) = "" Then
* Файл удален! Вычисляет счет.
IngScore = CLng((dhcBaseScore * _ colPaths.Count) / IngTimeout)
IngTotalScore = IngTotalScore + _ IngScore
Debug.Print "Good job! Score " & _ IngScore & " points"
* Создает новый временный файл.
strFile = dhCreateTempFile( _ dhGetRandomFile(colPaths), "~DH") fKeepGoing = CBool(Len(strFile))
* При успешном выполнении ... If fKeepGoing Then
’ ... вызывает FindNextChangeNotification 1 для освобождения дескриптора изменения. Call FindNextChangeNotification( _
hChange)
* Снова вызывает эту функцию для
* установки следующего события изменения. fKeepGoing = CBool( _
FindNextChangeNotification( _ hChange))
* Уменьшает время ожидания и ожидает снова. IngTimeout = IngTimeout - 500
Else Debug.Print "Error creating file!" End If
Else
* Файл все еще здесь!
Debug.Print _
"Oh, no! You got the wrong file!" fKeepGoing = False End If
Case WAIT_TIMEOUT
’ Время истекло!
Debug.Print "Time's up! Timeout = " & _ IngTimeout & " ms" fKeepGoing = False
Case WAIT_FAILED
* Плохо, ожидание не сработало.
Debug.Print "Wait failed!" fKeepGoing = False End Select
Loop While fKeepGoing And (IngTimeout > 0)
* Закрывает уведомление об изменении.
Call FindCloseChangeNotification(hChange)
' Выводит завершающее сообщение.
Debug.Print "Total score: " & IngTotalScore
Debug.Print "Thanks for playing"
End If
End Sub
шшИгра начинается с вызова процедуры dhFunWithNotifу, которой передается имя каталога и значение времени ожидания в миллисекундах. Затем dhFunWithNotifу строит список всех подкаталогов, расположенных внутри указанного каталога, и создаст файл нулевого размера в одном из них. Цель игры заключается в необходимости найти этот файл, используя Windows Explorer (Проводник), и удалить его до истечения отведенного времени. Если вы успеваете сделать это, dhFunWithNotifу создает новый файл для поиска. Чтобы добавить немного азарта, dhFunWithNotifу также уменьшает время ожидания на 500 миллисекунд (полсекунды) каждый раз, когда вы находите файл. Счет зависит от количества подкаталогов и от того, насколько быстро вы сможете удалить искомый файл. Чем больше подкаталогов используется, тем азартней будет игра.
ш
ш
Совет: Для улучшения своих результатов расположите окно VBA Проверка таким образом, чтобы вы могли его видеть во время работы с Проводником Windows.
Хотя приведенный пример слегка надуман, он наглядно демонстрирует работу функций уведомления. Процедура вызывает FindFirstChangeNotification для установки начального дескриптора изменения, передавая ей имя исходного каталога. Передача в качестве второго аргумента значения 1 принуждает функцию включить подкаталоги в уведомления об изменениях. Кроме того, следует отметить, что FindNextChangeNotification вызывается дважды. Это необходимо потому, что процесс, требующий уведомления об изменениях, также является причиной изменений (в данном случае каждый раз создается новый файл). Первый вызов функции FindNextChangeNotification освобождает дескриптор для вновь создаваемого файла. Второй вызов этой функции устанавливает новое уведомление.
0$0$Совет: Если вы станете играть в эту игру так же часто, как мы, то со временем на жестком диске накопится некоторое количество файлов нулевого размера. Чтобы избавиться от них, воспользуйтесь диалоговым окном Найти (Find) Windows для поиска и удаления всех файлов, начинающихся с "~DH".
0$
0$
Отслеживание нескольких изменений
Можно также контролировать несколько событий, связанных с изменениями, одновременно, например изменения в двух разных каталогах. Для этого нужно создать массив дескрипторов изменений, вызывая функцию FindFirstNotifi- cationHandle по одному разу для каждого условия. Затем следует вызвать другую функцию API, WaitForMultipleObjects, передав ей первый элемент и общее количество дескрипторов в массиве. Кроме того, можно указать, будет ли WaitForMul- tiple-Objects ожидать, когда произойдут все события или какое-то одно из них. Во втором случае, когда событие произойдет, функция WaitForMultipleObjects возвратит значение WAIT_OBJECT_0 плюс номер, указывающий, какое из событий произошло. После этого при желании можно вызвать функцию FindNextNotificationHandle, как это сделано в нашем примере.
Поиск файлов
Другая часто возникающая задача, общая для многих приложений, заключается в поиске заданного файла. Простые варианты поиска в известном каталоге могут быть быстро выполнены с помощью функций Dir или dhDir. Однако не всегда заранее известно, где следует искать файл. Посредством функций API можно написать специальные VBА-процедуры, которые помогут вам искать файлы. В зависимости от ваших потребностей эти процедуры могут быть как простыми, так и достаточно сложными.
Использование функции API SearchPath
Windows API для поиска файлов предлагает функцию SearchPath. Она разработана для поиска файлов в нескольких каталогах. Описание функции имеет такой вид:
Declare Function SearchPath Lib ”kernel32” _
Alias "SearchPathA”(ByVai IpPath As String, _
ByVai IpFileName As String, ByVai IpExtension As String, ByVai nBufferLength As Long, ByVai IpBuffer As String, _ ByVai IpFilePart As String) As Long
Первый аргумент IpPath задает каталоги, которые будет просматривать функция SearchPath. Если передать нулевой указатель (используя константу vbNullString), то SearchPath просматривает каталоги в таком порядке:
Каталог, из которого запущено приложение.
Текущий каталог приложения.
Системный каталог Windows (для Windows 95) или 32-разрядный системный каталог Windows (для Windows NT).
16-разрядный системный каталог Windows (только для Windows NT).
Каталог Windows.
Каталоги, указанные в переменной среды PATH.
Данный режим работы можно изменить путем передачи значения параметру ipPath. Это значение имеет форму оператора DOS PATH, причем отдельные каталоги разделяются между собой точкой с запятой. Например:
С:\MYDATA;С:\MYAPPS;D:\SOME OTHER FILES;С:\BACKUPS
В качестве значения аргумента IpFileName задается имя искомого файла. Если имя файла не содержит расширения, в качестве аргумента IpExtension можно ввести любое расширение, и тогда функция SearchPath присоединит его к полученному результату. Пример 7.30 содержит функцию dhSearch. Функция SearchPath помещена внутри нее для того, чтобы ее было легче вызывать. Необходимо только задать имя файла, а также путь поиска, и функция возвратит полный путь доступа к файлу, если он будет найден. На рис. 7.9 представлен пример вызова функции dhSearch.
Пример 7.30. Поиск файлов
в указанных каталогах с помощью функции dhSearch
Function dhSearch(strFile As String, _
Optional strPath As String = vbNullString) As String
Dim strBuffer As String
Dim IngBytes As Long
Dim strFilePart As String
’ Формирует буфер.
strBuffer = Space(MAX_PATH)
* Вызывает SearchPath.
IngBytes = SearchPath(strPath, strFile, vbNullString, Len(strBuffer), strBuffer, strFilePart)
* При успешном выполнении извлекает имя файла.
If IngBytes > 0 Then dhSearch = Left(strBuffer, IngBytes)
End If
End Function
•Я ПроверкиHF^1&!
T Current PATH=C:\WINDOWS;C:\WINDOWS\COMMAND;C:\;C:\DOS
?dhSearch("DRVS PACE.BIN")
C:\WINDOWS\COMMAND\DRVSPACE.BIN
?dhSearch("DRVSPACE.ВIN", "C:\WINDOWS\COMMAND") C:\DRVSPACE.BIN
?dhSearch("DRVSPACE.BIN", "C:\WINDOWS") ’ Nothing!
Рис. 7.9. Вызов функции dhSearch
Рекурсивный поиск в VBA
Searchpath является удобной и эффективной функцией, но как быть в том случае, если необходимо выполнить поиск на всем жестком диске? К сожалению, таких функций API не существует, однако можно написать собственную VBA-функцию для выполнения этой задачи. Ключевым моментом является создание процедуры, которая могла бы рекурсивно вызывать саму себя.
Функция dhFindAIIFiles
Мы уже создали одну такую функцию, dhFindAIIFiles (пример 7.31). Она может показаться сложной, однако это главным образом обусловлено тем, что ее работа сопровождается различными эффектами, которые совершенно необязательны, но делают процедуру более "живой”.
Пример 7.31. Поиск файлов на жестком диске с помощью функции dhFindAIIFiles
Function dhFindAIIFiles(strSpec As String, _
ByVai strPath As String, colFound As Collection, _
Optional IngAttr As Long = -1, _
Optional fRecursive As Boolean = True, _
Optional objCallback As Object) As Long
Dim strFile As String
Dim colSubDir As New Collection
Dim varDir As Variant
’ Обеспечивает то, чтобы strPath заканчивалась
* обратной косой чертой.
If Right(strPath, 1) о ”\” Then strPath = strPath & ”\”
End If
* Если задан объект обратного вызова,
’ вызывает его метод Searching.
If Not objCallback Is Nothing Then
objCallback.Searching strPath
End If
* Находит все соответствующие спецификации файлы каталога
* (если атрибуты файлов не заданы, применяет неисключающий поиск
всех файлов, в противном случае выполняет поиск в соответствии
* с заданными атрибутами).
If IngAttr = -1 Then
strFile = dhDir(strPath & strSpec, , False)
Else
strFile = dhDir(strPath & strSpec, IngAttr)
End If
Do Until strFile = ””
* При совпадении атрибутов добавляет файл в семейство
* (особый случай — каталоги ”.” и ”..”).
If ((GetAttr(strPath & strFile) And IngAttr) > 0) _
And (strFile <> ”.”) And (strFile <> ”..”) Then colFound.Add strPath & strFile
End If
Если задан объект обратного вызова, ’ вызывает его метод Found.
If Not objCallback Is Nothing Then objCallback.Found strPath, strFile End If
Берет следующий файл. strFile = dhDir
Loop
’ Если установлен флаг рекурсии, формирует список ’ всех подкаталогов.
If fRecursive Then
strFile = dhDir(strPath, vbDirectory) Do Until strFile = ""
* Игнорирует ”.” и ”..”.
If strFile <> ”.” And strFile <> ”..” Then
Добавляет каталог в семейство. colSubDir.Add strPath & strFile End If strFile = dhDir
Loop
* Выполняет рекурсию по всем подкаталогам.
For Each varDir In colSubDir dhFindAllFiles strSpec, varDir, colFound, _ IngAttr, fRecursive, objCallback
Next
End If
* Возвращает количество найденных файлов. dhFindAllFiles = colFound.Count
End Function
Функция работает следующим образом:
Получив спецификацию файла (например, *.ТХТ) и исходный каталог, она просматривает его в поиске всех файлов, которые соответствуют заданной спецификации.
Добавляет имя каждого файла, который находит, в объект VBA Collection, заданный при вызове данной процедуры.
После того как все файлы в заданном каталоге найдены, она просматривает его второй раз в поиске подкаталогов.
Добавляет все подкаталоги в локальный объект Collection.
Просматривает каждый из подкаталогов в объекте Collection и рекурсивно вызывает саму себя, передавая имя подкаталога как новую исходную точку для поиска.
Функция dhFindAllFiles передает одно и то же семейство найденных файлов по цепи вызовов на все более и более глубокие уровни подкаталогов. После просмотра всех подкаталогов семейство будет содержать полный список файлов, соответствующих заданной спецификации.
Тестирование функции dhFindAllFiles
Для демонстрации работы этой функции создана процедура dhPrintFoundFiles, приведенная в примере 7.32. Она получает спецификацию файла и путь, которые сразу передает функции dhFindAllFiles. Процедура также выполняет и другую задачу: она объявляет новый объект VBA Collection и передает его функции dhFindAllFiles вместе с другими параметрами. На рис. 7.10 представлен вызов процедуры dhPrintFoundFiles и результат ее выполнения.
АН ПроверкиИ® И
dhPrintFoundFiles "*.bak,r, "С:\WINDOWS”£
Starting search...
Done. Found: 4
What we found:$
C:\WINDOWS\WININIT.BAKf
C:\WINDOWS\HELP\mouse_cnt.bakJ
C:\WINDOWS\HELP\mouse_hlp.bak»
C:\WINDOWS\FORMS\frmcache.bak5
Puc. 7.10. Выполнение поиска файлов посредством процедуры dhPrintFoundFiles
Пример 7.32. Процедура тестирования функции dhFindAllFiles
Sub dhPrintFoundFiles(strSpec As String, strPath As String)
Dim colFound As New Collection
Dim IngFound As Long Dim varFound As Variant
* Тестирует логическую схему поиска файлов. Debug.Print "Starting search..."
* Вызов dhFindAllFiles. IngFound = dhFindAllFiles(strSpec, strPath, colFound)
* Выводит результаты. Debug.Print "Done. Found: " & IngFound
Семейство имен файлов дает возможность
* выполнять с ними любые операции.
Debug.Print
Debug.Print "What we found:"
Debug.Print "=============="
For Each varFound In. colFound
Debug.Print varFound
Next
End Sub
При наличии семейства найденных файлов его можно использовать для выполнения каких-либо других задач. Мы просто вывели имена файлов в окне Проверка с помощью цикла For Each, а вы можете использовать его в других операциях с файлами (копирование, удаление и т.д.).
"Украшения" для dhFindAIIFiles
Мы несколько расширили функциональные возможности процедуры dhFindAIIFiles. Во-первых, добавили необязательный аргумент IngAttr, чтобы ограничить поиск файлами с заданным набором атрибутов. По умолчанию он равен -1, что задает функции dhDir поиск всех файлов, соответствующих спецификации. Эту установку можно изменить, передав собственный набор атрибутов. Тогда функция dhDir проведет поиск только тех файлов, которые будут иметь указанные атрибуты.
Во-вторых, мы ввели параметр (fRecursive) для управления рекурсивным вызовом функции. Если присвоить этому параметру значение False, поиск ограничивается только заданным исходным каталогом.
В-третьих, добавили возможность использования объекта обратного вызова (ob j Callback). Объект обратного вызова представляет собой указатель на класс VBA, который содержит набор необходимых методов, Searching и Found. Когда поиск осуществляется в очередном каталоге, функция dhFindAIIFiles вызывает метод Searching, передавая ему имя подкаталога. Кроме того, когда функция dhFindAIIFiles находит какой-то файл, она вызывает метод Found, передавая ему путь и имя файла. Поскольку процесс поиска может занять много времени (особенно когда поиск выполняется по всему жесткому диску), целесообразно обеспечить пользователей обратной связью по мере его выполнения. Если вы когда-либо использовали диалоговое окно Найти в Windows 95 или Windows NT, то знаете, что в нем, в строке состояния, отображаются каталог, в котором в данный момент производится поиск, а также список найденных файлов. Вы можете обеспечить выполнение аналогичных операций, используя собственный объект обратного вызова.
Для того чтобы продемонстрировать эти возможности, мы создали очень простой класс объектов обратного вызова FileFindCallback, который приведен в примере 7.33. Он включает обязательные методы Searching и Found. Метод Found выводит имена найденных файлов в окне Проверка.
Пример 7.33. Простой класс обратного вызова
Public Sub Found(Path As String, File As String)
’ Он вызывается каждый раз,
’ когда найден соответствующий файл
Debug.Print Path & File End Sub
Public Sub Searching(Path As String)
’ Он вызывается каждый раз,
’ когда поиск начинается в новом каталоге.
End Sub
Примечание: Объект обратного вызова, переданный в функцию dhFindAllFiles, описан с использованием типа данных Object, а не конкретного типа данных, например FileFindCallback. Это сделано для того, чтобы вы могли передавать указатель на любой объект, который содержит необходимые методы.
Пример 7.34 содержит процедуру dhPrintFoundFilesWithFeedback, представляющую собой модифицированную версию процедуры dhPrintFoundFiles. Процедура объявляет новый экземпляр класса FileFindCallback и передает его функции dhFindAllFiles. При обнаружении соответствующего имени файла оно выводится в окне Проверка. Проверьте функционирование процедуры dhPrintFoundFilesWithFeedback самостоятельно, вызвав ее из окна Проверка.
Пример 7.34. Процедура dhPrintFoundFilesWithFeedback, использующая объект обратного вызова
Sub dhPrintFoundFilesWithFeedback(strSpec As String, _ strPath As String)
Dim colFound As New Collection
Dim IngFound As Long
Dim objCallback As New FileFindCallback
* Тестирует логическую схему поиска файлов.
Debug.Print "Starting search..."
Вызывает функцию dhFindAllFiles, передавая ей объект
обратного вызова, с помощью которого имена найденных
файлов будут выводиться в окне Проверка.
IngFound = dhFindAllFiles(strSpec, strPath, _ colFound, , , objCallback)
Выводит результаты.
Debug.Print "Done. Found: " & IngFound End Sub
Впрочем, эти "украшения" — только начало. Например, диалоговое окно Windows Найти позволяет настроить процесс поиска путем задания файлов, дат и даже текста, который они содержат. При желании можно расширить функцию dhFindAllFiles, используя другие функции, упомянутые в этой главе.
Определение имен временных файлов
Многие приложения используют временные файлы для хранения промежуточных результатов при обработке данных. Все основные приложения компании Microsoft также применяютют их. Если приложению нужны временные файлы, выбор их имен и путей можно осуществить с помощью двух функций API. Функция Get- TempPath определяет каталог, предназначенный для хранения временных файлов. Этим каталогом будет один из перечисленных ниже.
Каталог, определенный переменной среды ТМР.
Каталог, определенный переменной среды TEMP.
Текущий каталог, если ни ТМР, ни TEMP не определены.
Функция GetTempFileName создает имя временного файла по заданному пути, строке префикса и необязательному уникальному целому числу. Если в качестве упомянутого целого числа передается ненулевое значение, то функция GetTempFileName преобразует его в шестнадцатеричное и присоединяет к пути и префиксу. В этом случае функция не проверяет, существует ли данный файл. Если вы передаете значение 0, то GetTempFileName выбирает номер, исходя из значения системного таймера, и продолжает подбор номера до тех пор, пока не сможет сформировать имя файла, которого еще не существует.
Описания этих функций выглядят следующим образом:
Declare Function GetTempPath Lib "kernel32" _
Alias "GetTempPathA" (ByVai nBufferLength As Long, _
ByVai IpBuffer As String) As Long
Lib "kernel32" _ IpszPath As String, _ ByVai wUnique As Long, As LongLib "kernel32" _ IpszPath As String, _ ByVai wUnique As Long, As LongDeclare Function GetTempFileName Alias "GetTempFileNameA" (ByVai ByVai IpPrefixString As String, ByVai IpTempFileName As String)
Lib "kernel32" _ IpszPath As String, _ ByVai wUnique As Long, As Long
Lib "kernel32" _ IpszPath As String, _ ByVai wUnique As Long, As Long
Используя эти функции вместе, можно создать единую VBA-функцию, которая будет возвращать уникальное имя временного файла. Мы создали для вас такую функцию. Она называется dhTempFileName и приведена в примере 7.35. Обратите внимание, что функция получает префикс файла в качестве необязательного аргумента. Если опустить этот аргумент, используется строка ”-dh”.
Пример 7.35. Функция dhTempFileName для формирования имени временного файла
Function dhTempFileName( _
Optional strPrefix As String = "~DH") As String
Dim strPath As String Dim strBuffer As String Dim IngBytes As Long
Устанавливает буфер. strBuffer = Space(MAX_PATH)
Вызывает функцию GetTempPath. IngBytes = GetTempPath(Len(strBuffer), strBuffer)
При успешном выполнении выделяет путь. If IngBytes > 0 Then
strPath = Left(strBuffer, IngBytes)
* Переустанавливает буфер и вызывает ’ функцию GetTempFileName.
strBuffer = Space(МАХ_РАТН)
IngBytes = GetTempFileName(strPath, _
strPrefix, 0, strBuffer)
При успешном выполнении выделяет имя файла. If IngBytes > 0 Then
dhTempFileName = Left(strBuffer, IngBytes) End If
End If
End Function
Обратите внимание на то, что функция GetTernpFileName действительно создает временный файл. Если он не используется, удалите его посредством оператора VBA Kill.
Получение дескрипторов файлов (Windows)
При рассмотрении функций VBA для ввода-вывода файлов было введено понятие дескриптора файла. Для выполнения операций с файлами посредством некоторых функций Windows API этим функциям также необходимо передавать дескриптор файла. Однако эти дескрипторы не являются эквивалентными. Дескриптор файлов, полученный с помощью функции VBA FreeFile, нельзя использовать в функциях Windows API.
Использование функции CreateFile
Дескрипторы файлов получают путем вызова функции CreateFile. Кроме открытия и создания файлов, существует огромное количество вариантов использования функции CreateFile. Мы могли бы написать целую главу или несколько глав, описывая все случаи ее применения. Однако пока ограничимся открытием файлов в режиме только для чтения.
Описание функции CreateFile выглядит следующим образом:
Declare Function CreateFile Lib "kernel32" _
Alias "CreateFileA" _
(ByVai IpFileName As String, _
ByVai dwDesiredAccess As Long, _
ByVai dwShareMode As Long, _
IpSecurityAttributes As Any, _
ByVai dwCreationDisposition As Long, _
ByVai dwFlagsAndAttributes As Long, _
ByVai hTemplateFile As Long) As Long
Примечание: Параметр IpSecurityAttributes, тип которого в нашем примере объявлен как Алу, обычно является указателем на структуру secu- rity_attributes. Эти структуры используются для установки и получения информации системы безопасности Windows NT. Система безопасности операционной системы является отдельной очень сложной темой, рассмотрение которой не входит в планы авторов. Во всех наших примерах мы передаем нулевые указатели в функции, принимающие атрибуты защиты.
В табл. 7.7 приведен список аргументов функции CreateFile, а также допустимых констант. Для получения большего количества информации о функции CreateFile и ее аргументах обратитесь к документации Win32 Software Development Kit.
Таблица 7.7. Аргументы функции CreateFile
Аргумент
Описание
Допустимые значения
IpFileName
Путь к открываемому файлу или каталогу
Любой корректный реальный путь доступа к файлу
dwDesiredAccess
Режим доступа к файлу (битовая маска)
GENERIC_READ, GENERIC_WRITE
dwShareMode
Режим совместного использования файла (битовая маска)
FILESHAREREAD, FILESHAREWRITE
IpSecurityAttributes
Информация системы безопасности Windows NT
Указатель на структуру
SECURITY-ATTRIBUTES
dwCreationDisposition
Определяет, какие действия должны быть предприняты, если файл существует (или не существует)
CREATE_NEW, CREATE_ALWAYS, OPEN-EXISTING, OPEN_ ALWAYS, TRUNCATE- EXISTING
dwFlagsAndAttributes
Атрибуты файла (битовая маска)
F1LE_ATTRIBUTE_ARCHIVE, FILE_ATTRIBUTE_COMPRESSED, FILE_ATTRIBUTE_NORMAL, FILE_ATTRIBUTE_H1DDEN, FILE_ATTRIBUTE_READONLY, FILE_ATTRIBUTE_SYSTEM, FILE_FLAG_WRITE_THROUG H, FILE_FLAG_OVERLAPPED, FILEFLAGNO-BUFFERING, FILE_FLAG_RANDOM_ACCESS, FILE_FLAG_SEQUENTIAL_SCAN, FILE_FLAG_DELETE_ON_CLOSE, FILE_FLAG_BACKUP_SEMANTICS, FILE_FLAG_POSIX_SEMANTICS
HTemplateFile
Определяет, какой файл следует использовать в качестве шаблона для атрибутов
Дескриптор файла (не используется в Windows 95 и игнорируется в наших примерах)
Если внимательно посмотреть на приведенные в табл. 7.7 аргументы и константы, вы увидите некоторое сходство между ними и параметрами оператора VBA Open.
"Черновая" оболочка
Поскольку в большинстве примеров файлы открываются в режиме доступа только для чтения, мы создали оболочку dhQuickOpenFile для функции CreateFile, приведенную в примере 7.36. Она получает в качестве аргументов имя файла и режим
доступа к нему (GENERIC_READ по умолчанию), а возвращает результат вызова функции CreateFile. Если функции CreateFile удается открыть файл, то результатом будет дескриптор этого файла. При возникновении ошибки возвращается значение -1. Любая процедура, вызывающая dhQuickOpenFile, должна проверять это значение.
Совет: Многие функции Windows API, включая и CreateFile, возвращают коды ошибок посредством функции API GetLastError. Для определения кода ошибки МОЖНО воспользоваться СВОЙСТВОМ LastDLLError VBA-объекта Err.
Пример 7.36. dhQuickOpenFile, функция-оболочка для CreateFile
Function dhQuickOpenFile(strFile As String, _ Optional IngMode As Long = GENERIC_READ) As Long
Вызывает функцию CreateFile для открытия файла в
многопользовательском режиме
с доступом только для чтения,
если пользователь не задал другой вариант доступа.
* Возвращает дескриптор файла.
dhQuickOpenFile = CreateFile(strFile, IngMode, _
FILE_SHARE_READ Or FILE_SHARE_WRITE, _
ByVai 0&, OPEN_EXISTING, _
FILE_ATTRIBUTE_NORMAL Or _
FILE_FLAG_RANDOM_ACCESS, 0&)
End Function
Простой пример
Для демонстрации работы функции dhQuickOpenFile (а также функции CreateFile) приведенная в примере 7.37 процедура открывает заданный файл и выводит данные о его размере в окне Проверка (используя функцию API GetFileSize).
Пример 7.37. Вывод размера файла с помощью API
Sub dhPrintSizeAPI(strFile As String)
Dim hFile As Long
Dim IngSizeLower As Long
Dim IngSizeUpper As Long
' Открывает файл и получает дескриптор.
hFile = dhQuickOpenFile(strFile)
Если удается это сделать, выводит размер файла,
* если нет, то выводит код ошибки DLL.
If hFile > 0 Then
* Получает размер файла.
IngSizeLower = GetFileSize(hFile, IngSizeUpper)
* Выводит результат.
Debug.Print IngSizeLower & " bytes"
Закрывает файл.
Call CloseHandle(hFile)
Else
Debug.Print "Error calling CreateFile: " & _
Err.LastDllError
End If
End Sub
По аналогии с примерами использования функций VBA процедура закрывает файл по завершении работы с ним. В данном случае она вызывает функцию API CloseHandle. (Функция CloseHandle используется для закрытия дескрипторов различных объектов.)
Быстрое получение информации о файле
Теперь, когда вы знаете, что такое дескрипторы файлов в Windows и как их получить, вы готовы к рассмотрению жемчужины функций API — функции GetFi- leinf ormationByHandle. Одним вызовом этой функции можно получить почти всю необходимую информацию о файле. Раньше подобную информацию мы получали посредством функций поиска, которые описаны выше в этой главе. Теперь же нет необходимости просматривать весь каталог — достаточно просто передать функции дескриптор открытого файла.
Type BY_HANDLE_FILE_INFORMATION IngFileAttributes As Long ftCreationTime As FILETIME ftLastAccessTime As FILETIME ftLastWriteTime As FILETIME IngVolumeSerialNumber As Long IngFileSizeHigh As Long IngFileSizeLow As Long IngNumberOfLinks As Long IngFLlelndexHigh As Long IngFilelndexLow As LongEnd TypeType BY_HANDLE_FILE_INFORMATION IngFileAttributes As Long ftCreationTime As FILETIME ftLastAccessTime As FILETIME ftLastWriteTime As FILETIME IngVolumeSerialNumber As Long IngFileSizeHigh As Long IngFileSizeLow As Long IngNumberOfLinks As Long IngFLlelndexHigh As Long IngFilelndexLow As LongEnd TypeДля записи информации функция GetFilelnformationByHandle использует пользовательский тип данных. Описание этого типа, BYHANDLEFILEINFORMATION, выглядит следующим образом:
Type BY_HANDLE_FILE_INFORMATION IngFileAttributes As Long ftCreationTime As FILETIME ftLastAccessTime As FILETIME ftLastWriteTime As FILETIME IngVolumeSerialNumber As Long IngFileSizeHigh As Long IngFileSizeLow As Long IngNumberOfLinks As Long IngFLlelndexHigh As Long IngFilelndexLow As Long
End Type
Type BY_HANDLE_FILE_INFORMATION IngFileAttributes As Long ftCreationTime As FILETIME ftLastAccessTime As FILETIME ftLastWriteTime As FILETIME IngVolumeSerialNumber As Long IngFileSizeHigh As Long IngFileSizeLow As Long IngNumberOfLinks As Long IngFLlelndexHigh As Long IngFilelndexLow As Long
End Type
Атрибуты файла.
Время создания.
Время последнего доступа.
Время последнего изменения.
Серийный номер.
Старшее слово размера файла.
Младшее слово размера файла.
Связи с файлом (1 для FAT).
Старшее слово уникального идентификатора.
Младшее слово уникального идентификатора.
Функция GetFilelnformationByHandle получает дескриптор файла и указа тель на экземпляр данной структуры, а возвращает значение True или False, указы вающие на успешное или ошибочное завершение, соответственно.
Функции даты и времени Windows API
Прежде чем продолжить обсуждение функций Windows API для работы с файлами и дисками, необходимо рассмотреть вопрос о дате и времени, поскольку некоторые задачи (например, установка времени создания файла) требуют представления
значений времени в формате, понятном для Windows API. API использует два формата времени — системное время и время файла, — ни один из которых не совместим с VBA. Эти два формата рассмотрены в последующих разделах.
Системное время
Системное время представляет собой внутренний формат времени, который используется операционными системами Windows 95 и Windows NT. Функции, работающие с системным временем, обращаются к нему посредством пользовательского типа данных SYSTEMTIME. Структура этого типа данных такова:
Type SYSTEMTIME
intYear As Integer intMonth As Integer intDayOfWeek As Integer intDay As Integer intHour As Integer intMinute As Integer intSecond As Integer intMilliseconds As Integer End Type
Очевидно, что каждый элемент даты и времени представлен отдельным целым значением. С помощью этого формата можно представить любую дату, начиная с 1 января 32768 года до нашей эры, вплоть до 31 декабря 32767 года. Данный диапазон значений достаточно широк для большей части приложений, за исключением, быть может, археологических или палеонтологических программ.
Нами разработаны две процедуры для преобразования значений системного времени и времени VBA, которые приведены в примере 7.38.
Пример 7.38. Преобразование формата системного времени в формат времени УВА и наоборот
Function SysTimeToVBATime(stSysTime As SYSTEMTIME) As Date ' Формирует значение даты/времени в формате VBA 1 при помощи функций DateSerial и TimeSerial.
With stSysTime
SysTimeToVBATime = _
DateSerial(.intYear, .intMonth, .intDay) + _ TimeSerial(.intHour, .intMinute, .intSecond)
End With
End Function
Sub VBATimeToSysTime(datTime As Date, stSysTime _ As SYSTEMTIME)
Заполняет структуру данных компонентами даты и времени. With stSysTime
.intMonth = Month(datTime)
.intDay = Day(datTime) .intYear = Year(datTime)
.intHour = Hour(datTime)
.intMinute = Minute(datTime)
.intSecond = Second(datTime)
End With
End Sub
При работе с системным временем не забывайте о том, что внутренне Windows отслеживает время в соответствии со Всеобщим скоординированным временем (Universal Coordinated Time — UTC). Оно определено как текущее время в Гринвиче (Англия); иногда его называют временем по Гринвичу (Greenwich Mean Time — GMT). Текущее значение системного времени можно определить посредством вызова функции GetSystemTime. Большая часть функций, использующих тип данных SYSTEMTIME, предполагает, что передаваемое в них время введено в формате UTC.
Местное время — это текущее время для вашей местности, т.е. время часового пояса, заданного в вашей системе. Для определения текущего местного времени вызовите функцию API GetLocalTime. Информацию о часовом поясе можно получить при помощи функции GetTimeZonelnformation. Результат (который записывается в переменную пользовательского типа TIMEZONEIN FORMATION) содержит, среди прочего, смещение часового пояса — разницу между местным временем и временем по Гринвичу.
Время файла
Другой формат времени, с которым приходится иметь дело при использовании функций для работы с файлами и дисками (будем называть его время файла), применяется для установки и получения трех временных значений, связанных с файлами и каталогами. 32-разрядная операционная система Windows отслеживает время, когда файл был создан, время, когда файл последний раз использовался, и время, когда файл последний раз изменялся.
Подобно системному времени, время файла использует пользовательский тип данных FILETIME, имеющий такую структуру:
Type FILETIME
IngLowDateTime As Long
IngHighDateTime As Long
End Type
Содержание структуры FILETIME не настолько понятно, как в случае с systemtime. Две переменные типа Long, составляющие тип FILETIME, представляют 64-разряд ное число, содержащее (да, мы ничего не перепутали) количество 100-нано- секундных интервалов, прошедших с 1 января 1601 года!
К счастью, редко приходится сталкиваться с данными типа filetime в их исходном виде. Windows API содержит функции преобразования времени файла в системное и наоборот. Используя эти функции, несложно создать процедуры (пример 7.39) для преобразования времени файла в формат VBA.
Пример 7.39. Функции преобразования
времени файла в формат времени УВА и наоборот
Function FileTimeToVBATime(ftFileTime As FILETIME, _
Optional fLocal As Boolean = True) As Date
Dim stSystem As SYSTEMTIME
Dim ftLocalFileTime As FILETIME
' Если пользователя интересует местное время, преобразует ' время файла в местное время файла.
If fLocal Then
Call FileTimeToLocalFileTime(ftFileTime, _ ftLocalFileTime)
ftFileTime = ftLocalFileTime
End If
' Преобразует время файла в системное время, а затем
' вызывает нашу функцию для преобразования в формат VBA.
If CBool(FileTimeToSystemTime(ftFileTime, stSystem)) Then FileTimeToVBATime = SysTimeToVBATime(stSystem)
End If
End Function
Sub VBATimeToFileTime(datTime As Date, ftTime As FILETIME, _ Optional fLocal As Boolean = True)
Dim stSystem As SYSTEMTIME
Dim ftSystem As FILETIME
* Вызывает нашу функцию для преобразования времени
из формата VBA в системное время.
Call VBATimeToSysTime(datTime, stSystem)
* Преобразует системное время во время файла.
If CBool(SystemTimeToFileTime(stSystem, ftTime)) Then
' Если время в формате VBA было местным, преобразует ' местное время файла в системное время файла.
If fLocal Then
Call LocalFileTimeToFileTime(ftTime, ftSystem) ftTime -= ftSystem
End If
End If
End Sub
Обратите внимание на то, что каждая функция получает в качестве необязательного аргумента флаг fLocal. Если флаг имеет значение True (значение по умолчанию), то значение времени VBA воспринимается как местное время. Когда fLocal имеет значение False, оно воспринимается как системное, или UTC, время.
Операции с временами файлов
Познакомившись с различными форматами времени, рассмотрим функции API, предназначенные для выполнения различных операций с датами и временами файлов. Во многих приложениях часто нужно устанавливать время создания файла. Как вы, наверное, заметили, при инсталляции программных продуктов таких компаний, как Microsoft, все файлы имеют одну и ту же дату и время создания. Как упоминалось ранее, файловая система отслеживает три временных значения для каждого файла. Используя Windows API, вы сможете устанавливать и определять все три значения.
В данном разделе рассказано о том, как это делается, а также о том, как быстро и легко сравнить временные значения двух файлов.
Определение и установка значений времени файла
Функции GetFileTime и SetFileTime служат для получения и установки времен файла. Описание этих функций имеет следующий вид. (Обратите внимание на то, что в каждую из них в качестве первого аргумента передается дескриптор файла.)
Declare Function GetFileTime Lib "kernel32" _
(ByVai hFile As Long, IpCreationTime As FILETIME, _
IpLastAccessTime As FILETIME, _
IpLastWriteTime As FILETIME) As Long
Declare Function SetFileTime Lib "kernel32" _
(ByVai hFile As Long, IpCreationTime As FILETIME, _
IpLastAccessTime As FILETIME, _
IpLastWriteTime As FILETIME) As Long
Поскольку работать co структурой filetime довольно сложно, мы рассмотрим несколько функций, которые можно использовать для определения и установки времен файлов. Эти функции открывают файл (с помощью dhQuickOpenFile) и управляют преобразованиями форматов времени. В примере 7.40 представлены первые две функции, dhGetFileTimes и dhSetFileFimes. Каждая из них использует специально созданную пользовательскую структуру данных dhtypFileTimes, которая здесь не приведена. В ней сгруппированы три переменные типа Date. Эта структура создана для большего удобства при выполнении операций сразу со всеми тремя значениями времени файла.
Пример 7.40. Две функции для получения и установки значений времени файла
Function dhGetFileTimes(strFile As String, _ dftTimes As dhtypFileTimes) As Boolean
Dim ftCreate As FILETIME
Dim ftAccess As FILETIME
Dim ftWrite As FILETIME
Dim hFile As Long
Dim IngRet As Long
' Открывает файл.
hFile = dhQuickOpenFile(strFile)
If hFile > 0 Then
' Вызывает функцию GetFileTime для помещения
' информации о времени в локальные структуры FILETIME.
If CBool(GetFileTime(hFile, ftCreate, _ ftAccess, ftWrite)) Then
' В случае успешного завершения преобразует
' значения в формат даты VBA и возвращает их в
' заданную структуру dhtypFileTimes.
With dftTimes
.datCreated = FileTimeToVBATime(ftCreate) .datAccessed = FileTimeToVBATime(ftAccess) .datModified = FileTimeToVBATime(ftWrite) End With
* Возвращает флаг успешного завершения. dhGetFileTimes = True End If
’ Закрывает файл.
Call CloseHandle(hFile) End If End Function
Function dhSetFileTimes(strFile As String, _ dftTimes As dhtypFileTimes) As Boolean Dim ftCreated As FILETIME Dim ftAccessed As FILETIME Dim ftModified As FILETIME Dim hFile As Long * Открывает файл с доступом только для записи. hFile = dhQuickOpenFile(strFile, GENERIC_WRIТЕ) ' При успешном завершении... If hFile > 0 Then
* преобразует заданное время в формат FILETIME. With dftTimes Call VBATimeToFileTime(.datCreated, ftCreated)- Call VBATimeToFileTime(.datAccessed, ftAccessed) Call VBATimeToFileTime(.datModified, ftModified) End With
Устанавливает значения времени.
If CBool(SetFileTime(hFile, ftCreated, _ ftAccessed, ftModified)) Then * Возвращает флаг успешного завеершения. dhSetFileTimes = True End If * Закрывает файл. Call CloseHandle(hFile) End If End Function
Например, установим в качестве времени создания, времени последнего открытия и времени последней модификации файла полночь 1 января 1999 года. Для этого можно воспользоваться такой программой:
Dim dft As dhtypFileTimes
With dft
.datCreated = #1/1/99 12:00:00 AM# .datAccessed = #1/1/99 12:00:00 AM# .datModified = #1/1/99 12:00:00 AM# End With
Call dhSetFileTimes("C:\SOMEFILE.EXE", dft)
0
Внимание: При использовании функции dhSetFileTimes для изменения значения времени файла проверьте, не остались ли какие-либо элементы заданной структуры пустыми, поскольку тем самым вы устанавливаете для времени файла значение 0, что соответствует в VBA субботе, 30 декабря 1899 года!
Для использования обеих функций нужно объявить переменную типа dhtyp- FileTimes и передать ее в качестве второго параметра. Если вы хотите получить или изменить только одно из значений времени файла, можно воспользоваться двумя другими функциями VBA, dhGetFileTimesEx и dhSetFileTimesEx. Вместо переменной пользовательского типа эти функции получают целое число, указывающее, какое значение времени вас интересует. В случае dhGetFileTimesEx передается одно число, указывающее одно из значений времени. Функция dhSetFileTimesEx получает комбинацию чисел и изменяет одно или несколько значений времени файла. Обе функции имеют приемлемые значения по умолчанию для этих аргументов, поэтому не нужно передавать соответствующее значение, если в этом нет необходимости. Следующая программа демонстрирует вызов этих функций:
* Получает время последнего изменения файла WIN.INI.
Debug.Print dhGetFileTimesEx("C:\WINDOWS\WIN.INI")
' Получает время последнего открытия файла WIN.INI.
Debug.Print dhGetFileTimesEx("C:\WINDOWS\WIN.INI", _ dhcFileTimeAccessed)
* Устанавливает значение текущего времени в качестве времени
* последнего изменения и последнего открытия файла WIN.INI.
Debug.Print dhSetFileTimesEx("С:\WINDOWS\WIN.INI", Now)
* Присваивает параметру времени создания файла WIN.INI ' вчерашнюю дату.
Debug.Print dhSetFileTimesEx("С:\WINDOWS\WIN.INI", Now - 1, _ dhcFileTimeCreated)
Примечание: Поскольку функции dhGetFileTimesEx и dhSetFileTimesEx по сути являются модификациями функций dhGetFileTimes и dhSetFileTimes, мы их здесь не приводим.
Сравнение значений времени файла
Иногда нужно не просто получить значения времени файла, но и сравнить их с соответствующими значениями другого файла. Например, это может понадобиться
для того, чтобы выяснить, одинаковы ли два файла на разных компьютерах. Конечно, можно воспользоваться описанными выше функциями, однако Windows API содержит простую функцию CompareFileTime, которая предназначена для решения поставленной задачи:
Declare Function CompareFileTime Lib "kernel32"
(IpFileTimel As FILETIME, lpFileTime2 As FILETIME) As Long
Функция CompareFileTime получает указатели на две структуры типа filetime и возвращает результат, определяющий разницу между ними, если таковая имеется. Функция возвращает -1, если значение времени первого файла меньше, чем второго, значение 1, если больше, и 0, если эти значения совпадают. В примере 7.41 приведена функция, которая использует CompareFileTime для вычисления разницы между значениями времени двух файлов. Вам нужно лишь задать пути к обоим файлам, а заодно и то значение времени, которое вы хотите проверить. Обратите внимание, что функция возвращает те же значения, что и CompareFileTime, за исключением случая, когда происходит ошибка (возможно, из-за неправильно указанного имени файла). В этом случае возвращается -2.
Пример 7.41. Использование Windows API для сравнения значений времени файла
Function dhCompareFileTime(strFilel As String, _ strFile2 As String, Optional intTime As _ Integer = dhcFileTimeModified) As Long
Dim ftCreatel As FILETIME
Dim ftAccessl As FILETIME
Dim ftWritel As FILETIME
Dim hFilel As Long
Dim ftCreate2 As FILETIME
Dim ftAccess2 As FILETIME
Dim ftWrite2 As FILETIME
Dim hFile2 As Long
Устанавливает возвращеное значение для случая
ошибочного завершения.
dhCompareFileTime = -2
Открывает первый файл.
hFilel = dhQuickOpenFile(strFilel)
If hFilel > 0 Then
' Открывает второй файл.
hFile2 = dhQuickOpenFile(strFile2) If hFile2 > 0 Then
Получает значения времени файлов.
If CBool(GetFileTime(hFilel, ftCreatel, ftAccessl, ftWritel)) Then
If CBool(GetFileTime(hFile2, ftCreate2 ftAccess2, ftWrite2)) Then
' Вызывает CompareFileTime для заданного ' значения времени и возвращает результат. Select Case intTime
Case dhcFileTimeCreated dhCompareFileTime = _ CompareFileTime(ftCreatel, _ ftCreate2)
Case dhcFileTimeAccessed dhCompareFileTime = _ CompareFileTime(ftAccessl, _ ftAccess2)
Case dhcFileTimeModified dhCompareFileTime = _ CompareFileTime(ftWritel, _ ftWrite2)
End Select
End If
End If
1 Закрывает второй файл.
Call CloseHandle(hFile2) End If
* Закрывает первый файл. Call CloseHandle(hFilel) End If
End Function
Примечание: Для получения более подробной информации о сравнении и обработке значений дат и времени посредством VBA обратитесь к главе 2.
Использование стандартных диалоговых окон Windows
Хотя эта тема непосредственно не связана с обработкой файлов, умение использовать стандартные диалоговые окона Windows для работы с файлами пригодится во многих приложениях. Если вы когда-либо сталкивались с проектированием диалогового окна для выбора файлов, вы знаете, сколько это причиняет головной боли. Нужно обеспечить пользователям возможность просматривать файлы и каталоги, отфильтровывать файлы и вводить имена файлов непосредственно. Стандартные диалоговые окна Windows обеспечивают выполнение этих и других задач и сравнительно просты в работе. В данном разделе описано, как открываются и используются диалоговые окна Открытие файла (Open) и Сохранение файла (Save As), которые являются лишь вариантами одной и той же формы. На рис. 7.11 показано диалоговое окно Открытие файла.
Открытие файлаИВОткрытие файлаИВ□ CommandЙ MediaЙ spool2] ConfigSiMsappsQ Sysbckup2j Cursorsii PersonalЙ System21 FavoritesQ Photo cdBiTemp2] FontsDPifBi Г лавное меню21 FormsО RecentQ Рабочий стол~ 21 HelpQjSendTo□_msrstrt.exe2J InfBShellNewГЛ mssetup.bat□ CommandЙ MediaЙ spool2] ConfigSiMsappsQ Sysbckup2j Cursorsii PersonalЙ System21 FavoritesQ Photo cdBiTemp2] FontsDPifBi Г лавное меню21 FormsО RecentQ Рабочий стол~ 21 HelpQjSendTo□_msrstrt.exe2J InfBShellNewГЛ mssetup.bat
Открытие файлаИВ
Открытие файлаИВ
□ Command
Й Media
Й spool
2] Config
SiMsapps
Q Sysbckup
2j Cursors
ii Personal
Й System
21 Favorites
Q Photo cd
BiTemp
2] Fonts
DPif
Bi Г лавное меню
21 Forms
О Recent
Q Рабочий стол
~ 21 Help
QjSendTo
□_msrstrt.exe
2J Inf
BShellNew
ГЛ mssetup.bat
□ Command
Й Media
Й spool
2] Config
SiMsapps
Q Sysbckup
2j Cursors
ii Personal
Й System
21 Favorites
Q Photo cd
BiTemp
2] Fonts
DPif
Bi Г лавное меню
21 Forms
О Recent
Q Рабочий стол
~ 21 Help
QjSendTo
□_msrstrt.exe
2J Inf
BShellNew
ГЛ mssetup.bat
ййыйДееиМмебймйеййМйаа^^
Рис. 7. И. Окно Открытие файла
Структура OPENFILENAME
Функциональные возможности обоих диалоговых окон основываются на сложной пользовательской структуре данных OPENFILENAME. В примере 7.42 содержится описание этой структуры, а также описание двух функций API, GetOpenFileName и GetSaveFileName, которые ее используют. Приведенные в примере комментарии кратко описывают переменные — элементы структуры. Наиболее существенные из них рассмотрены в этом разделе ниже.
Пример 7.42. Объявления, необходимые для использовании диалоговых окон Windows работы с файлами
Type OPENFILENAME IngStructSize As Long hwndOwner As Long hlnstance As Long strFilter As String strCustomFilter As String intMaxCustFilter As Long intFilterlndex As Long strFile As String intMaxFile As Long strFileTitle As String intMaxFileTitle As Long strlnitialDir As String strTitle As String IngFlags As Long intFileOffset As IntegerType OPENFILENAME IngStructSize As Long hwndOwner As Long hlnstance As Long strFilter As String strCustomFilter As String intMaxCustFilter As Long intFilterlndex As Long strFile As String intMaxFile As Long strFileTitle As String intMaxFileTitle As Long strlnitialDir As String strTitle As String IngFlags As Long intFileOffset As Integer' Размер структуры.Дескриптор окна владельцаэкземпляра шаблона.' Строка фильтра.' Строка с выбранным фильтром* Len(strCustomFilter).* Индекс строки фильтра.* Выбранный файл и путь' Len(strFile).* Выбранное имя файлаLen(strFileTitle).* Имя каталога.* Заголовок диалогового окна.1 Флаги диалогового окна.* Смещение имени файла.' Размер структуры.Дескриптор окна владельцаэкземпляра шаблона.' Строка фильтра.' Строка с выбранным фильтром* Len(strCustomFilter).* Индекс строки фильтра.* Выбранный файл и путь' Len(strFile).* Выбранное имя файлаLen(strFileTitle).* Имя каталога.* Заголовок диалогового окна.1 Флаги диалогового окна.* Смещение имени файла.
Type OPENFILENAME IngStructSize As Long hwndOwner As Long hlnstance As Long strFilter As String strCustomFilter As String intMaxCustFilter As Long intFilterlndex As Long strFile As String intMaxFile As Long strFileTitle As String intMaxFileTitle As Long strlnitialDir As String strTitle As String IngFlags As Long intFileOffset As Integer
Type OPENFILENAME IngStructSize As Long hwndOwner As Long hlnstance As Long strFilter As String strCustomFilter As String intMaxCustFilter As Long intFilterlndex As Long strFile As String intMaxFile As Long strFileTitle As String intMaxFileTitle As Long strlnitialDir As String strTitle As String IngFlags As Long intFileOffset As Integer
' Размер структуры.
Дескриптор окна владельца
экземпляра шаблона.
' Строка фильтра.
' Строка с выбранным фильтром
* Len(strCustomFilter).
* Индекс строки фильтра.
* Выбранный файл и путь
' Len(strFile).
* Выбранное имя файла
Len(strFileTitle).
* Имя каталога.
* Заголовок диалогового окна.
1 Флаги диалогового окна.
* Смещение имени файла.
' Размер структуры.
Дескриптор окна владельца
экземпляра шаблона.
' Строка фильтра.
' Строка с выбранным фильтром
* Len(strCustomFilter).
* Индекс строки фильтра.
* Выбранный файл и путь
' Len(strFile).
* Выбранное имя файла
Len(strFileTitle).
* Имя каталога.
* Заголовок диалогового окна.
1 Флаги диалогового окна.
* Смещение имени файла.
intFileExtention As Integer strDefExt As String IngCustData As Long IngfnHook As LongintFileExtention As Integer strDefExt As String IngCustData As Long IngfnHook As Long* Смещение расширения файла.* Расширение файла по умолчанию.' Данные для обработки.1 LP для функции обработки.* Смещение расширения файла.* Расширение файла по умолчанию.' Данные для обработки.1 LP для функции обработки.
intFileExtention As Integer strDefExt As String IngCustData As Long IngfnHook As Long
intFileExtention As Integer strDefExt As String IngCustData As Long IngfnHook As Long
* Смещение расширения файла.
* Расширение файла по умолчанию.
' Данные для обработки.
1 LP для функции обработки.
* Смещение расширения файла.
* Расширение файла по умолчанию.
' Данные для обработки.
1 LP для функции обработки.
strTemplateName As String 1 Имя шаблона диалогового окна. End Type
Declare Function GetOpenFileName Lib "comdlg32.dll" _ Alias "GetOpenFileNameA" (ofn As OPENFILENAME) As Boolean
Declare Function GetSaveFileName Lib "comdlg32.dll" _ Alias "GetSaveFileNameA" (ofn As OPENFILENAME) As Boolean
Таким образом, сами функции достаточно просты; они получают указатель на структуру и возвращают значение True или False в зависимости от того, был вызов функции удачным или нет. (Значение True возвращается в том случае, если пользователь выбрал файл, a False — если пользователь закрыл диалоговое окно, либо произошла ошибка.) Для открытия диалогового окна необходимо объявить новый экземпляр типа OPENFILENAME, присвоить значения составляющим его переменным и вызвать одну из указанных функций. Когда функция возвращает результат, вы просматриваете содержимое структуры, чтобы определить, что выбрал пользователь.
Использование структуры OPENFILENAME
Несмотря на то, что OPENFILENAME — довольно сложная структура, редко приходится использовать ее элементы. Список основных переменных приведен ниже.
Переменная
Описание
hwndOwner
Дескриптор окна, из которого вызвано диалоговое окно
strFilter
Строка, которая используется для фильтрации имен файлов (она состоит из пар значений, разделенных символом Null, одно из которых представляет описание ("Text Files"), а второе — спецификацию файла ("* . txt"))
intFilterIndex
Указывает, какой фильтр следует использовать при открытии диалогового окна
strFile
Исходное имя файла (используется для сохранения имени файла, которое выберет пользователь)
strlnitialDir
Исходный каталог, который используется при открытии диалогового окна
strTitle
Заголовок диалогового окна
IngFlags
Комбинация значений из табл. 7.8, управляющая свойствами диалогового окна
По-видимому, IngFlags — это самая важная переменная, ведь она определяет работу диалогового окна.
Содержимое IngFlags составляется из констант, список которых приведен в табл. 7.8. В следующем разделе представлена функция-оболочка для функций API и описаны некоторые наиболее важные константы.
Примечание: Мы намеренно опустили несколько констант, которые не могут быть использованы или избыточны в VBA. Мы также включили в таблицу три пользовательских флага, которые представляют собой удобные и часто используемые комбинации других констант.
Таблица 7.8. Флаги OPENFILENAME
Константа
Окно
Описание
OFN_ALLOWMULTISELECT
Открытие файла
Позволяет выбирать несколько файлов; возвращает каталог, за которым следуют имена файлов в формате 8.3, разделенные между собой пробелами
OFNCREATEPROMT
Открытие файла
Если пользователь набрал имя несуществующего файла, будет предложено создать этот файл
OFNEXPLORER
Оба
Навязывает диалоговое окно типа окна Проводник Windows (в большинстве случаев не нужна)
OFNEXTENSIONDIFFERENT
Оба
Указывает, что пользователь ввел расширение, которое отличается от значения strDefExt
OFN_FILEMUSTEXIST
Оба
Принуждает пользователя ввести или выбрать имя существующего файла (должна сочетаться с константой OFNPATHMUS- TEXIST)
OFN_HIDEREADONLY
Оба
Скрывает флаг Read Only
OFNNOCHANGEDIR
Оба
Восстанавливает исходное значение текущего каталога, если пользователь изменил каталог во время поиска файлов
OFNNODEREFERENCELINKS
Оба
Если этот параметр установлен, диалоговое окно возвращает имя ярлыка Windows 95, а не имя файла, на который ссылается этот ярлык
OFNNOREADONLYRETURN
Оба
Определяет, что для возвращаемого файла не установлен флаг Read Only и файл не находится в защищенном от записи каталоге
OFNNOTESFILECREATE
Сохранение файла
Диалоговое окно по завершении своей работы не проверяет, был ли создан файл
OFNNOVALI DATE
Оба
Диалоговое окно не проверяет, допустимо ли введенное имя файла
OFNOVERWRITEPROMT
Сохранение файла
Предупреждает пользователя, что выбранный файл уже существует
Константа
Окно
Описание
OFNPATHM U STEXIST
Оба
Принуждает пользователя выбрать или ввести имя существующего каталога
OFNREADONLY
Открытие файла
Проверяет флаг Read Only при открытии диалогового окна и возвращает его значение при закрытии
О FN_S HARE AWARE
Оба
Игнорирует нарушение условий коллективного доступа
dfOFN_OPENEXISTING
Открытие файла
Комбинация OFN_PATHMUSTEXIST, OFN_FILEMUSTEXIST и OFN_HIDEREADONLY
dfOFNSAVENEW
Сохранение файла
Комбинация OFN PATHMUSTEXIST, OFN_OVERWRITEPROMT и
OFNHI DEREAD ONLY
dfOFN SAV ENEWPATH
Сохранение файла
Комбинация OFN_OVERWRITEPROMT и OFN_HIDEREADONLY
Примечание: Некоторые из этих флагов (например, OFNREADONLY) устанавливаются диалоговым окном при закрытии. Это позволяет указать, какие опции выбрал пользователь в дополнение к имени файла.
Простой способ вызова функций
Как уже упоминалось, диалоговое окно открывается в результате создания нового экземпляра структуры OPENFILENAME, заполнения ее элементов значениями и последующей передачи в одну из функций API. Поскольку это достаточно кропотливая задача с точки зрения программирования, мы создали функцию dhFileDialog, которая приведена в примере 7.43 и предназначена для выполнения этих операций. Функция dhFileDialog получает аргументы, которые заносятся прямо в переменные структуры OPENFILENAME. Однако для того чтобы вызов функции был более удобным, мы сделали все аргументы необязательными.
Пример 7.43. Функция dhFileDialog
Function dhFileDialog( _
Optional strlnitDir As String, _
Optional strFilter As String = _
"All files (*.*)" & vbNullChar & "*.*" & _
vbNullChar & vbNullChar, _
Optional intFilterlndex As Integer = 1, _
Optional strDefaultExt As String, _
Optional strFilename As String, _
Optional strDialogTitle As String = "Open File",
Optional hwnd As Long = -1, _
Optional fOpenFile As Boolean = True, _ Optional ByRef IngFlags As Long = _ dhOFN_OPENEXISTING) As Variant
Dim ofn As OPENFILENAME
Dim strFileTitle As String
Dim fResult As Boolean
’ Заполняет некоторые пропущенные аргументы.
If strlnitDir = ”” Then strlnitDir = CurDir End. If
If hwnd = -1 Then hwnd = GetActiveWindow() End If
* Устанавливает буферы для результата.
strFilename = strFilename & _
String (255 - Len(strFilename), 0) strFileTitle = String(255, 0)
’ Заполняет элементы структуры OPENFILENAME. With ofn
.IngStructSize = Len (ofn)
.hwndOwner = hwnd
.strFilter = strFilter
.intFilterlndex = intFilterlndex
.strFile = strFilename
.intMaxFile = Len(strFilename) .strFileTitle = strFileTitle .intMaxFileTitle = Len(strFileTitle) .strTitle = strDialogTitle
.IngFlags = IngFlags
.strDefExt = strDefaultExt
.strlnitialDir = strlnitDir .hlnstance = 0
.strCustomFilter = String(255, 0)
.intMaxCustFilter = 255
.IngfnHook = 0
End With
* Вызывает нужную функцию.
If fOpenFile Then fResult = GetOpenFileName(ofn) Else
fResult = GetSaveFileName(ofn) End If
* Если вызов завершился успешно, возвращает имя файла. ’ В противном случае возвращает Null.
If fResult Then
’ Возвращает все флаги в точку вызова. IngFlags = ofn.IngFlags
* Возвращает результат.
If (ofn.IngFlags And OFN_ALLOWMULTISELECT) = 0 Then dhFileDialog = dhTrimNull(ofn.strFile)
Else dhFileDialog = ofn.strFile End'If
Else dhFileDialog = Null End If
End Function
Имена большинства аргументов (например, strinitDir) говорят сами за себя. Им присваиваются значения, которые нужно передать функциям API. Однако некоторые аргументы нуждаются в дополнительных пояснениях.
Аргумент strFilter определяет режим фильтрации файлов в диалоговом окне. Обратите внимание на то, что по умолчанию отображаются все файлы. Если вы хотите изменить этот режим, сформируйте новую строку фильтра. Например, рассмотрим следующую формулу:
strNewFilter = "Word Documents" & vbNullChar & "*.doc" & _
vbNullChar & "Text Files" & vbNullChar & "*.txt" & _
vbNullChar & "All Files" & vbNullChar & "*.*" & _ vbNullChar & vbNullChar
Она задает три режима фильтрации: первый — для документов Word, второй — для текстовых файлов и третий — для всех файлов. Обратите внимание на то, что строка в переменной strNewFilter состоит из подстрок описаний и спецификаций файла. Каждая из них отделена от других символом Null (посредством константы vbNullChar), и каждая пара отделена от следующей пары также символом Null. Вся же строка ограничена двумя символами Null.
Аргумент IngFlags передает диалоговым функциям значения флагов. Мы ввели несколько значений по умолчанию для диалогового окна Открытие файла. Если вы хотите открыть диалоговое окно Сохранение файла или обойти значения по умолчанию, передайте новую комбинацию значений в этот аргумент.
Наконец, аргумент fOpenFile определяет, какое из диалоговых окон следует отображать. Передача значения True (установлено по умолчанию) открывает диалоговое окно Открытие файла, а значения False — диалоговое окно Сохранение файла. Пример 7.44 содержит несколько вариантов использования этой функции. Вышеупомянутые аргументы делают вызов функции более гибким.
Пример 7.44. Различные способы вызова функции dhFlieDialog
Sub dhTestDialog()
* Открывает файл в текущем каталоге.
Debug.Print dhFileDialog()
’ Открывает несколько файлов в каталоге Windows.
Debug.Print dhFileDialog(strinitDir:="С:\WINDOWS", _
IngFlags:=dhOFN_OPENEXISTING Or OFN_ALLOWMULTISELECT _ Or OFN_EXPLORER)
’ Сохраняет файл в текстовом формате.
Debug.Print dhFileDialog(strFilter:=”Text Files" & _
vbNullChar & "*.txt" & vbNullChar & vbNullChar, _ strDialogTitle:="Save As", IngFlags:=dhOFN_SAVENEW, fOpenFile:=False)
End Sub
Резюме
Эта глава посвящена операциям с дисками и файлами в VBA. В частности, рассмотрены следующие вопросы:
получение информации о файлах и каталогах с помощью функций VBA Dir, GetAttr, FileLen И FileDateTime;
копирование, перемещение, создание и удаление файлов и каталогов;
создание и изменение файлов с помощью функций ввода-вывода VBA:
поиск файлов на жестком диске;
операции с путями файлов и временными файлами;
использование функций Windows API для получения информации о файлах и дисках;
получение и установка информации о дате и времени создания, модификации и последнего открытия файла;
уведомление об изменении содержимого каталога;
Кроме того, что мы предложили вам дополнительные функции, которые можно использовать в приложениях, мы пытались помочь разобраться с совместным использованием функций VBA и Windows API, чтобы получить как можно больше информации о файловой системе.
ГЛАВА 8
Добавление средств мультимедиа в пользовательские приложения
Средства мультимедиа для Windows
Мультимедиа за один шаг
Media Control Interface
Запись и воспроизведение звука и видео
Завершая эту книгу, посвященную вопросам разработки приложений в среде VBA, мы не могли не включить главу об элементах мультимедиа. Почему, спросите вы? Прежде всего потому, что книга, претендующая на полноту изложения, не может обойти этого актуального в настоящее время вопроса. В настоящей главе нет возможности коснуться всего того, что необходимо для написания полноценных программ с применением всех возможностей мультимедиа. Наша цель несколько иная: показать, как с помощью всего нескольких приемов можно значительно повысить качество программного продукта, сделать его более наглядным, удобным и приятным в работе. Даже у самых серьезных разработчиков время от времени возникает желание добавить в свои приложения немного музыки или видео. В этой главе рассмотрено несколько простейших методов, которые позволят реализовать эту задачу. Начало главы посвящено анализу возможностей мультимедиа в Windows 9х и Windows NT 4.0, после чего рассматриваются отдельные вопросы — от прослушивания звуковых файлов до воспроизведения видеоклипов в окне приложения. Надеемся, что к концу главы у вас сложится представление о том, как Windows обрабатывает объекты мультимедиа, а также накопится достаточно большой багаж функций VBA, которые пригодятся для разработки приложений в дальнейшем.
Примечание: Для того чтобы протестировать предлагаемые примеры, необходимо иметь компьютер, оснащенный средствами мультимедиа, как минимум, звуковой картой и проигрывателем компакт-дисков.
Средства мультимедиа для Windows
Прежде чем приступать к описанию многочисленных методов управления элементами мультимедиа, давайте проанализируем подсистемы Windows, на которых они основаны. Хотя приведенные в этой главе примеры составляют только верхушку айсберга, этот раздел поможет вам сформировать общее представление о мультимедийных возможностях Windows.
Возможности мультимедиа и MCI
Впервые реальная поддержка мультимедиа была предоставлена компанией Microsoft в качестве дополнения Multimedia Extentions к Windows 3.x. Оно включало в себя дополнительные DLL-файлы с функциями для воспроизведения цифрового звука и видео, а также MIDI-файлов (MIDI — Musical Instrument Digital Interface). В Windows 95 эти функции встроены непосредственно. Хотя они все еще существуют в виде независимых DLL-модулей отдельно от традиционной триады библиотек Windows API — User, Kernel и GDI, — они доступны на любом компьютере, работающем с Windows 95/98 или Windows NT 4.0. (Однако это вовсе не означает, что на любом компьютере вы найдете необходимое для работы этих функций мультимедийное аппаратное обеспечение!)
Поддержка мультимедиа в текущих версиях Windows воплотилась в целостную оболочку, работающую с многочисленными и разнообразными аппаратными устройствами, такими как проигрыватели цифровых аудио- и лазерных дисков, аудиокомпакт-диски, оверлейное видео (известное как "ТВ в окне") и устройства воспроизведения анимации. Многие из них управляются посредством Media Control Interface
(MCI — интерфейс управления мультимедийными устройствами). MCI предоставляет набор функций для управления различными устройствами мультимедиа. Хотя эти устройства сильно отличаются друг от друга, они все же имеют ряд общих характеристик. Например, все они должны открывать определенные мультимедийные данные (которые в этой главе называются медиа-объектами), будь то звуковой файл или дорожка аудиокомпакт-диска. Все они имеют возможность воспроизводить эти данные, а также управлять текущей позицией файла (перемотка вперед, назад и т.д.). В этом разделе рассмотрены возможности некоторых таких устройств, а в последующих — рассказано о том, как ими управлять, используя команды MCI.
Примечание: MCI считается мультимедийным интерфейсом высокого уровня, поскольку устройства сами выполняют большую часть работы по управлению медиа-объектами. Windows содержит также множество мультимедийных функций низкого уровня, которые предоставляют высокую степень контроля над данными мультимедиа. Однако VBA нецелесообразно использовать для разработки приложений, подобных системе распознавания речи, поэтому в этой главе мы ориентируемся на функции высокого уровня VBA.
Волновое аудио
Волновое аудио (waveform audio) — это широко распространенный WAV-формат аудиофайлов, хорошо знакомый пользователям Windows. WAV-файл создается с помощью звуковой карты, которая принимает аналоговый входной сигнал от микрофона и преобразует его в цифровой формат. Волновое аудио представляет собой наиболее распространенный способ создания звуковых эффектов для приложений благодаря своей способности воспроизводить звуки с высокой степенью точности. Пользователь имеет возможность воздействовать на качество воспроизведения звука, управляя следующими параметрами:
■ Частота дискретизации. Количество цифровых выборок, полученных за одну секунду (обозначается термином Герц, сокращенно — Гц). Чем больше количество выборок, тем выше качество звука. Можно установить одно из трех значений этого параметра: 11025 Гц (качество радиотрансляции или голоса), 22050 Гц (среднее качество), 44100 Гц (качество компакт-диска).
Количество разрядов выборки. Определяет точность каждой выборки. Чем выше разрядность выборки, тем лучше качество звука. Запись можно осуществлять посредством 8-разрядных или 16-разрядных выборок.
Количество каналов. Определяется тем, записывается ли стереофонический звук (двухканальная запись) или монофонический звук (одноканальная запись).
Недостатком волнового аудио является большой объем памяти на диске, необходимый для сохранения звуковых данных. Например, 60 секунд 16-разрядного стереозвука с качеством, соответствующим компакт-диску, требует 10584000 байт памяти! (А именно: 60 х 44100 х (16 / 8) х 2.) К счастью, такое качество записи и воспроизведения звука требуется редко. Вследствие невысокого качества многих звуковых карт и несовершенства человеческого уха, для звуковых эффектов и голосовых сообщений обычно вполне достаточно 8-разрядного монофонического звука с частотой 11025 Гц.
Аудиокомпакт-диски
Если в приложении действительно необходим звук высокого качества, можно воспользоваться аудиокомпакт-дисками. MCI содержит простые функции для воспроизведения аудиокомпакт-дисков, а затраты памяти на жестком диске для записи звука при этом сводятся к нулю, поскольку все звуковые фрагменты находятся на компакт-диске. Единственная проблема, связанная с использованием аудиокомпакт-дисков, заключается в сложности получения записи. Если вы не приобретете права на использование существующих записей, вам придется создавать их самостоятельно. А этот процесс не только занимает много времени, но и требует значительных музыкальных способностей.
Звук в формате MIDI
Формат MIDI представляет собой своеобразный компромисс между WAV-файлами и аудиокомпакт-дисками. MIDI — это формат, используемый электронными музыкальными синтезаторами. Большинство звуковых карт содержит микросхему синтезатора с частотной модуляцией (ЧМ), которая может воспроизводить музыку по командам MIDI. В отличие от файла волновой записи, сохраняющего точную копию записываемого звука, звук в MIDI-формате сохраняется в качестве "мелодии", собранной из информации о музыкальных инструментах, нотах, высоте тона, длительности и т.д. Поэтому требования, предъявляемые при сохранении файлов этого формата, невысоки. К примеру, Windows 9х поставляется вместе с закодированной в MIDI-формате версией Бранденбургской сонаты № 3 Иоганна Себастьяна Баха. Несмотря на то, что соната длится более шести минут, размер звукового файла составляет всего лишь около 140 Кб.
Однако у файлов в формате MIDI есть три недостатка. Во-первых, файлы содержат не настоящие звуки, а лишь информацию для управления определенными музыкальными инструментами, поэтому реальное звучание зависит от конкретной звуковой карты. Во-вторых, если вы не используете специальную звуковую карту, центральный процессор должен обрабатывать музыкальную информацию помимо выполнения общей системной нагрузки. В-третьих, звуковые карты, которые используют синтезаторы с ЧМ при воспроизведении, могут только аппроксимировать реальный звук инструмента путем модуляции несущей частоты. Современные звуковые карты, которые сохраняют цифровые образцы звучания реальных инструментов (в виде так называемых волновых таблиц — wave table) воспроизводят музыкальные фрагменты более точно, однако они все еще достаточно дороги и менее распространенны по сравнению со звуковыми картами, основанными на ЧМ-синтезаторах. Таким образом, по рассмотренным выше причинам MIDI-звук обычно используется в качестве фоновой музыки для игр, где точность воспроизведения инструментов не очень существенна.
Цифровое видео
Одним из преимуществ Windows 9х и Windows NT 4.0 является их способность воспроизводить цифровое видео. Вероятно, вы знаете, что эта возможность реализуется посредством AVI-файлов. Компания Microsoft поставляла программный продукт Video for Windows (Видео для Windows), который позволял просматривать AVI-файлы в среде Windows 3.x. Именно в Video for Windows был введен формат AVI-файлов, ставший в настоящее время наиболее популярным форматом цифрового видео. В Windows 95/98 и NT 4.0 цифровое видео является неотъемлемой частью подсистемы мультимедиа.
-% f-% fРис. 7. /. Просмотр программ сжатия видео- и аудиофайлов с помощью Панели управленияРис. 7. /. Просмотр программ сжатия видео- и аудиофайлов с помощью Панели управления
-% f
-% f
Рис. 7. /. Просмотр программ сжатия видео- и аудиофайлов с помощью Панели управления
Рис. 7. /. Просмотр программ сжатия видео- и аудиофайлов с помощью Панели управления
Примечание: Среди других форматов цифрового видео известны Apple QuickTime, MPEG и DVI (Digital Video Interactive — интерактивное цифровое видео)
Файлы цифрового видео создаются с помощью специального аппаратного обеспечения, которое преобразовывает аналоговый сигнал телевизионного тюнера или видеомагнитофона в цифровые изображения. Качество получаемого изображения в большой степени зависит от его размера, скорости воспроизведения (которая измеряется количеством кадров в секунду) и используемых алгоритмов сжатия.
К примеру, применяют такой вид сжатия, при котором не нужно обновлять каждый пиксель изображения, если он не изменяется от кадра к кадру. Различные алгоритмы сжатия обеспечивают различные уровни уменьшения скорости воспроизведения и размера файла.
Кроме использования аппаратных средств аналого-цифрового преобразования сигнала, AVI-файлы можно также создавать с помощью программных продуктов, например Microsoft Camcorder (поставляется как часть Microsoft Office 97). Эти приложения позволяют создавать кадры компьютерного изображения посредством "перехвата экрана" или при помощи других методов. Данная технология особенно полезна при разработке демонстрационных или обучающих видеоклипов для различных программных продуктов.
( ынп I 5,.i Myih- I nt is-jiM -
Аудио | Видео | MIDI ) ' CD Дополнительно I
■■■llilM
■L Аудиоустройства
A^ Устройства и инструменты MIDI'
У Устройства-микшеры;
’', Устройства линейного входа'
? i /; Устройства управления мультимедиа
S3 Видеомагнитофон VISCA (устройство управления)
55 Воспроизведение видео (устройство управления) I
58 Воспроизведение звукозаписей (устройство управл(
52 Проигрыватель видеодисков PIONEER (устройство ^ 3 Проигрыватель компакт-дисков (устройство управл;
S Секвенсер MIDI (устройство управления)I
■■-■-' Программы видеосжатия
ч : В Программы аудиосжатия;
:4j Конвертер Microsoft PCM;
- -j- Устройства видеозаписи
?> Джойстики
Так же, как и в случае с цифровым аудио, основной проблемой цифрового видео является большой объем памяти, который требуется для записи файлов. Даже короткий видеоклип с маленьким размером кадра может занять более 500 Кб памяти на диске. Кроме того, для просмотра AVI-файла вам потребуется та же программа ком- прессии/декомпрессии, которая использовалась для его создания. Несмотря на то, что на большинстве компьютеров установлено несколько подобных программ для воспроизведения различных форматов видео, все же вполне возможна несовместимость. В окне Свойства: Мультимедиа (Multimedia Properties) в Панели управления (Control Panel), представленном на рис. 7.1, можно узнать, какие программы ком- прессии/декомпрессии установлены на вашем компьютере.
Мультимедиа за один шаг
За исключением тех случаев, когда речь идет о создании полностью мультимедийных приложений, потребности в использовании средств мультимедиа оказываются весьма примитивными. Например, вам может понадобиться сгенерировать простейший звук или воспроизвести определенный WAV-файл. В этом разделе рассматривается выполнение подобных задач с помощью отдельных операторов VBA.
Подача звукового сигнала
Если вам необходимо всего лишь сгенерировать какой-нибудь простой и четкий звук, трудно придумать что-либо лучшее, чем использование оператора VBA Веер. Он генерирует звуковой сигнал независимо от того, установлены на вашем компьютере аппаратные средства для поддержки мультимедиа или нет. Когда такие средства отсутствует, звуковой сигнал воспроизводится встроенным динамиком компьютера. Если звуковая карта установлена, VBA воспроизведет стандартный системный звук. (Для получения более подробной информации о системных звуках обратитесь к разделу "Воспроизведение звуков, сопровождающих системные события" ниже в этой главе.)
MessageBeep: шаг вперед
Если стандартный системный звук не удовлетворяет вашим требованиям, обратитесь за помощью к функции MessageBeep Windows API. MessageBeep воспроизводит один из пяти стандартных звуков, связанных с пиктограммой окна сообщения. Они основаны на системных звуках, установленных в окне Свойства: Звук в Панели управления. Можно использовать либо те же константы, что и для функции VBA MsgBox, либо константы, определенные в Windows API. В табл. 7.1 приведены оба набора констант. (Для воспроизведения звука, установленного по умолчанию, передайте в функцию значение &hffffffff.)
Таблица 7.2. Константы VBA и Windows API для воспроизведения системных звуков
ЗначениеКонстанта VBAКонстанта Windows API
0отсутствуетМВ_ОК
16vbCriticalMB ICONHAND или MBJCONSTOP
Значение
Константа VBA
Константа Windows API
32
vbQuestion
MBJCONQUESTION
48
vbExclamation
MB_ICONEXCLAMATION
64
vblnformation
MB ICONASTERISK или MBJCONINFORMATION
MessageBeep представляет собой простую функцию, которая получает в качестве аргумента одну из перечисленных в таблице констант и воспроизводит указанный звук. Если звуковая карта не установлена, звуковой сигнал воспроизводится встроенным динамиком компьютера.
Когда используемое средство разработки не воспроизводит системный звук при отображении окна сообщения, воспользуйтесь функциями MessageBeep и MsgBox. Программа dhMsgBeep приведена в примере 7.1.
Пример 7,1, Заставьте окно сообщения генерировать звук
Function dhMsgBeep (strMsg As String, _
Optional IngType As Long = 0, _
Optional strCaption As String = ””) As Long
Dim IngSound As Long
* Определяет тип звука.
IngSound = IngType And &HF0
Воспроизводит звук.
Call MessageBeep(IngSound)
* Отображает окно сообщения.
If strCaption = ”” Then dhMsgBeep = MsgBox(strMsg, IngType) Else
dhMsgBeep = MsgBox(strMsg, IngType, strCaption)
End If
End Function
Воспроизведение WAV-файлов с помощью функции PlaySound
Простая функция API PlaySound позволяет воспроизвести любой волновой аудиофайл, включая системные звуки и волновые данные, которые содержатся в исполняемых файлах. Описание функции PlaySound имеет следующий вид:
Declare Function PlaySound Lib "winmm.dll" _
Alias "PlaySoundA” (ByVai IpszSoundName As String, _
ByVai hmod As Long, ByVai uFlags As Long) As Long
Таким образом, PlaySound получает всего три аргумента: название звука, дескриптор загружаемого модуля (о котором подробно рассказано ниже) и набор флагов. Несмотря на это, функция является весьма гибкой в работе. В табл. 7.2 приведен список констант, которые можно передавать этой функции.
Таблица 7.2. Константы для функции Play Sound
Константа
Описание
SNDSYNC
Синхронное воспроизведение звука (значение по умолчанию)
SND_ASYNC
Асинхронное воспроизведение звука
SND_NODEFAULT
Если звук указан неверно, то функция PlaySound не проигрывает звук, принятый по умолчанию
SND_LOOP
Повторяет воспроизведение звука до тех пор, пока не произойдет следующий вызов функции PlaySound
SNDNOSTOP
Не останавливает воспроизведение звука, проигрывающегося в данный момент
SNDNOWAIT
Не ждет, если драйвер звуковой карты занят
SNDMEMORY
Аргумент IpszSoundName представляет собой указатель на область оперативной памяти, в которой записан звук
SND_ALIAS
Звук определяется именем системного звука
SNDALIASID
Звук определяется идентификатором системного звука
SND_F1LENAME
Звук определяется именем файла
SNDRESOURCE
Звук определяется именем ресурса
!!Независимо от других флагов, передаваемых функции, необходимо обязательно передать один из следующих параметров: SNDFILENAME, SND_ALIAS или SND_MEMORY. Эти флаги указывают функции PlaySound, каким образом интерпретировать аргумент IpszSoundName — как имя файла, имя системного события или адрес памяти. В следующих разделах рассмотрен каждый из этих способов.
!
!
Примечание: Если ни один из указанных флагов не передается функции, то PlaySound пытается определить значение аргумента IpszSoundName самостоятельно. Но все же лучше это сделать вам.
Примечание: Аргумент hMod используется только для воспроизведения встроенных ресурсов вместе с флагом SND_MEMORY, в противном случае ему необходимо присвоить значение 0.
Наиболее очевидным применением функции PlaySound является воспроизведение произвольных WAV-файлов. Для этого нужно задать функции путь к файлу и набор флагов. Наряду с флагом SND_FILENAME наиболее часто используются SND_SYNC и SNDASYNC. Передавая флаг SND_SYNC, вы принуждаете функцию PlaySound воспроизвести весь файл полностью перед тем, как передать управление обратно приложению; использование же SND ASYNC заставляет PlaySound найти звук, начать его воспроизведение и сразу же передать управление приложению. Например, если нужно воспроизвести WAV-файл, содержащий инструкции по
использованию приложения, не прерывая на это время взаимодействие пользователя с программой, можно воспользоваться следующим рператором:
Call PlaySound ("С:\MYAPP\SOUNDS\HOWTO.WAV",0&, _
SND_ASYNC Or SND_FILENAME)
Такой оператор воспроизводит звук асинхронно, позволяя пользователю одновременно продолжать работать с программой.
Примечание: При использовании функции PlaySound для воспроизведения WAV-файлов обычно нет необходимости включать флаг SNDFILE- NAME в набор передаваемых в функцию параметров. В большинстве случаев PlaySound сама корректно определит, что первый аргумент является именем файла. Кроме того, если вы не укажете путь доступа к файлу, функция PlaySound при поиске файла применит те же правила поиска, что и функция SearchPath, описанная в главе 5.
Если функция PlaySound не может воспроизвести заданный звук, например, потому что указанное имя файла не существует, она воспроизводит стандартный звук Windows по умолчанию (знакомый электронный "колокольчик"). Если вас не устраивает такое положение вещей, передайте функции флаг SND NODEFAULT. В этом случае функция вернет значение, которое позволит определить, был ли воспроизведен заданный звук:
If Not Cbool(PlaySound("MYSOUND.WAV", 0&, SND_NODEFAULT)) Then
Что-то не сработало!
ffEnd If
f
f
Примечание: Если флаг SND_NODEFAULT не установлен, и функция PlaySound не может воспроизвести заданный звук, она проиграет стандартный звук и возвратит значение 1, которое указывает на то, что функция выполнена успешно.
Если нужно воспроизводить какой-то звук непрерывно, передайте функции флаг SND_LOOP. В этом случае PlaySound будет проигрывать этот звук снова и снова до ее повторного вызова. Для того чтобы прекратить циклическое воспроизведение звука, нужно передать функции другой звук или пустую строку. Допустим, вы хотите создать иллюзию собачьего лая для системы отпугивания воров. Для этого можно воспользоваться следующим фрагментом программы:
' Воспроизводит собачий лай.
Call PlaySound ("BARK. WAV", 0&, SND_FILENAME Or SND_LOOP)
* Спасибо, достаточно!
Call PlaySound("", 0&, SND_NODEFAULT)
Воспроизведение звуков, сопровождающих системные события
Функция PlaySound также предоставляет простой способ воспроизведения звуков, соответствующих системным событиям Windows. Немного выше рассмотрены способы использования функции MessageBeep для воспроизведения звуков, сопровождающих некоторые события. Однако существуют и другие системные события, например запуск системы и выход из нес, свертывание и развертывание окна или малоприятный критический останов. (Это событие, связанное с возникновением ошибки доступа к странице памяти.) Звуки, связанные с различными событиями, можно изменить в окне Свойства: Звук в Панели управления (рис. 7.2).
Воспроизведение системных звуков
Для воспроизведения какого-либо системного звука посредством функции PlaySound укажите его название и передайте в функцию флаг SND_ALIAS. Например:
Call PlaySound("AppGPFault", 0&, SND_ALIAS Or SND_NODEFAULT)
В табл. 7.4 приведен список системных звуков, распознаваемых функцией PlaySound, а также их описания, приведенные в окне Свойства: Звук в Панели управления. Имена системных звуков фиксированы, и им соответствует текст описаний, который можно задавать установками реестра. Описания находятся в разделе HKEY_CURRENT_USER\AppEvents\EventLabels.
Рис. 7.2. Системные звуки можно изменить в окне Свойства: Звук в Панели управления
Таблица 7.4. Системные звуки, которые можно воспроизводить с помощью функции Play Sound
Имя системного звука
Описание, принятое по умолчанию в Панели управления
.Default
Стандартный звук
AppGPFault
Ошибка программы
Close
Закрытие программы
EmptyRecycleBin
Пустая корзина
MailBeep
Уведомление о приходе почты
Maximize
Развертывание
MenuCommand
Команда меню
MenuPopup
Всплывающее меню
Minimize
Свертывание
Open
Открытие программы
RestoreDown
Переход из режима полноэкранного представления в режим нормального
RestoreUp
Восстановление окна из значка
SystemAsterisk
Звездочка
SystemExclamation
Восклицание
SystemExit
Выход из Windows
SystemHand
Критическая ошибка
SystemQuestion
Вопрос
SystemStart
Запуск Windows
Воспроизведение звуков событий приложения
В дополнение к системным звукам, приведенным в табл. 7.3, приложения могут добавлять собственные звуки событий, которые также можно определить в окне Свойства: Звук в Панели управления. К сожалению, их нельзя воспроизводить непосредственно с помощью функции PlaySound и переданного ей флага SND_ALIAS. Для этого необходимо сначала обратиться к системному реестру, найти WAV-файл, прочитать его имя, а затем вызвать функцию PlaySound.
Звуки для событий, включая и системные звуки, хранятся в разделе реестра HKEY_CURRENT_USER\AppEvents\Schemes\Apps. Системные звуки Windows сохраняются в подразделе .Default (обратите внимание на начальную точку в имени этого подраздела). Другие приложения могут добавлять дополнительные подразделы с собственными звуками событий. В каждом подразделе находятся другие подразделы для каждого события, а еще ниже — подразделы, представляющие звук, связанный с событием по умолчанию, звук, назначенный событию в настоящее время, и звуки, связанные с какими-либо другими звуковыми схемами, которые вы могли установить. На рис. 7.3 представлено окно Редактор реестра, а в нем одно из подобных ответвлений.
Рис. 7.3. Элементы реестра для звуков событий
Воспроизведение встроенных звуков
Последняя возможность использования функции PlaySound заключается в вое- произведении звуков, встроенных в EXE- или DLL-файлы. Выполняемые файлы (мы рассматриваем DLL-файлы как выполняемые) обычно содержат многочисленные встроенные объекты, которые обобщенно называются ресурсами. Основные ресурсы включают пиктограммы, растровые изображения, меню, строковые таблицы и описания диалоговых окон. Разработчики приложений используют ресурсы, поскольку это сокращает количество дополнительных файлов, которые должны включаться в программный пакет. Кроме того, достаточно сложно (хотя отнюдь не невозможно) извлекать встроенные ресурсы из выполняемого файла. Поэтому встраивание ресурсов является удобным способом защиты интеллектуальной собственности.
Создание встроенных WAV-файлов
Если планируется использование в приложении многочисленных WAV-файлов, целесообразно встроить их в DLL-модуль, даже если он не содержит никаких программных фрагментов. Для этой цели разработчики обычно создают DLL-модули только для ресурсов (resource-only DLL). Для создания такой библиотеки достаточно выполнить всего лишь две операции:
Скомпилировать отдельные WAV-файлы (или другие ресурсы) в файл ресурсов посредством компилятора ресурсов, например RC.EXE, который поставляется с Microsoft Visual C++.
Связать этот файл ресурсов с DLL с помощью компоновщика, например LINK.EXE, который также поставляется с Microsoft Visual C++.
Выполнение первого шага требует от вас создания сценария ресурсов. Сценарий ресурсов сообщает компилятору ресурсов, какие файлы включать в файл ресурсов и каким образом их в нем идентифицировать. В примере 7.2 приведено содержимое файла WAVE.RC, сценария ресурсов, который мы создали для этой главы.
Пример 7.2. Пример сценария ресурсов
////////////// п ///////////I////////////////////// н / и /// И /
// Сценарий ресурсов для создания встроенных WAV-файлов //
// Обязательно проверьте, назначен ли каждому WAV-pecypcy
// уникальный номер!
//
/////////////////////////////////////////////////////////////
101 WAVE DISCARDABLE "GO.WAV"
201
WAVE
DISCARDABLE
"NORTH.WAV"
202
WAVE
DISCARDABLE
"EAST.WAV"
203
WAVE
DISCARDABLE
"SOUTH.WAV"
204
WAVE
DISCARDABLE
"WEST.WAV"
// Конец КС-файла.
Каждая строка файла сценария (за исключением тех строк, которые начинаются двумя символами косой черты и являются комментариями) определяет какой-то ресурс. В каждую из них необходимо включить:
уникальный номер или имя (без кавычек), которое идентифицирует ресурс;
тип ресурса (в данном случае WAVE);
ключевое слово Discardable (которое сообщает системе Windows о том, что она может динамически загружать и выгружать данный ресурс из памяти);
путь доступа к этому ресурсу.
Примечание: Если файлы расположены не в одном каталоге с RC-файлом, то вам необходимо указать путь доступа. Кроме того, поскольку файл RC.EXE подчиняется тем же правилам, что и компилятор С, необходимо продублировать все разделители в задаваемом пути (например, "CAXWAVEWGO.WAV).
Для компиляции сценария ресурсов в файл ресурсов запустите RC.EXE из командной строки. Например, результатом выполнения следующей команды
RC WAVE.RC
будет файл ресурсов с тем же именем, что и сценарий ресурсов, и расширением RES.
Теперь необходимо связать файл ресурсов с DLL. Поскольку эта DLL предназначена только для ресурсов и не требует никаких других объектных файлов, можно
выполнить следующую команду (при использовании компоновщика LINK.EXE компании Microsoft):
LINK /out:WAVE.DLL /dll /machine:1386 /noentry wave.res
Каждая часть командной строки имеет определенное значение:
/out:результирующий_файл определяет результирующий файл;
/dll сообщает LINK.EXE о необходимости создания DLL;
/machine: тип_машины определяет формат двоичного выполняемого кода;
/noentry сообщает компоновщику LINK.EXE об отсутствии точки входа (т.е. о том, что речь идет о DLL только для ресурсов);
остальные параметры — это имена исходных файлов, разделенные пробелами.
Совет: Вместо задания всех этих параметров в командной строке можно ввести строку "LINK @makef ile”, где makefile — это имя файла, который содержит необходимые параметры.
Воспроизведение встроенных WAV-файлов
Скомпилировав несколько WAV-файлов в DLL-модуль, можно воспользоваться функцией PlaySound и воспроизвести один из них. Однако это не так просто, поскольку придется сначала загрузить этот ресурс в память. Эта процедура включает в себя загрузку самой DLL, поиск необходимого ресурса и затем вызов функции PlaySound с указанием флага SND MEMORY. Но не стоит отчаиваться — мы создали функцию-оболочку dhPlayResource, которая самостоятельно выполняет все эти действия (пример 7.3).
Совет: Если вы используете в качестве средства разработки Visual Basic, то для получения указателя адреса ресурса в памяти воспользуйтесь VB-функ-
цией LoadResource, а не процедурой из примера 7.3, которая применяется для загрузки библиотеки и ресурса.
Пример 7.3. Функция dhPlayResource воспроизводит встроенный WAV-файл
Function dhPlayResource(strLibrary As String, _ varResource As Variant, Optional IngFlags As Long = 0) As Boolean
Dim hMod As Long
Dim hRes As Long
Dim IngRes As Long
Dim fOk As Boolean
Загружает библиотеку как файл данных.
hMod = LoadLibraryEx(strLibrary, 0&, _
LOAD_LIBRARY_AS_DATAFILE)
If hMod <> 0 Then
' Если ресурс задан номером, добавляет символ 1 "#", в противном случае просто использует его.
If IsNumeric(varResource) Then varResource = "#" & varResource End If
* Ищет WAVE-pecypc в библиотеке. IngRes = FindResource(hMod, CStr(varResource), "WAVE") If IngRes <> 0 Then
’ Загружает ресурс. hRes = LoadResource(hMod, IngRes) If hRes <> 0 Then
* Блокирует ресурс и воспроизводит его. If CBool(LockResource(hRes)) Then
dhPlayResource = CBool(PlayResSound( _ hRes, 0&, SND_MEMORY Or IngFlags)) End If ' Освобождает ресурс. Call FreeResource(hRes) End If End If
' Освобождает библиотеку. Call FreeLibrary(hMod) End If End Function
Поскольку вопрос о загрузке ресурсов из исполняемых файлов выходит за рамки этой книги, если вы хотите исследовать, как работает эта функция, вам придется сделать это самостоятельно. (Однако обратите внимание на тот факт, что при вызове функции PlaySound указывается флаг SNDMEMORY.)
m
Совет: Если при использовании Visual Basic WAVE-pecypc уже скомпилирован в выполняемый файл, можно воспользоваться гораздо более удобной формой вызова функции PlaySound: передать ей в качестве второго аргумента дескриптор экземпляра VB-приложения в дополнение к имени ресурса и флагу SND_RESOURCE. Например: PlaySound ("WAVENAME”, Арр.hlnstance, SND_RESOURCE) .
Внимание: Никогда не вызывайте функцию dhPlayResource с флагом SNDASYNC! Это приведет к тому, что библиотека будет выгружена из памяти еще до того, как окончится воспроизведение звука, и в результате этих действий произойдет очень неприятная ошибка страницы.
Media Control Interface
Media Control Interface является составной частью мультимедиа для Windows и представляет собой мощный механизм управления мультимедийными устройствами. Посредством всего трех функций он может воспроизвести аудиокомпакт-диск, записать цифровой звук, управлять видеомагнитофоном... Кроме того, он реализует два способа управления устройствами: первый способ основан на использовании командной строки, а второй — на применении управляющих сообщений. В этом разделе подробно рассмотрен сам MCI и оба способа его использования.
Из-за нехватки места мы не можем привести полный перечень команд MCI и их параметров. Если примеров, рассмотренных в этой книге, окажется недостаточно для решения ваших задач, обратитесь к дополнительным источникам, таким как библиотека Microsoft Developer Network (MSDN) на компакт-диске. Кроме того, MSDN можно найти в Internet по адресу http://www.microsoft.coTn/Tnsdn/sdk.
Работа с устройствами MCI
С самого начала MCI разрабатывался для поддержки различных устройств мультимедиа. Каждому из них был назначен уникальный тип устройства, который может быть выражен как числом, так и текстовым описанием. Эти обозначения используются в функциях MCI. Типы устройств вместе с их обозначениями представлены в табл. 7.4.
Таблица 7.4. Типы устройств мультимедиа и их МС1-идентификаторы
Устройство
Константа типа устройства
Тип устройства
Аудиокомпакт-диск
MCI_DEVTYPE_CD_AUDIO
cdaudio
Цифровое видео в окне
MCI_DEVTYPE_D1G1TAL_VIDEO
digitalvideo или avivideo
Цифровой аудиомагнитофон
MCI_DEVTYPE_DAT
dat
Оптический сканер
MCI_DEVTYPE_SCANNER
scanner
Секвенсор MIDI
MCI_DEVTYPE_SEQUENCER
sequencer
Другие MCI-устройства
MCI_DEVTYPE_OTHER
other
Оверлейное видео (аналоговое)
MCI_DEVTYPE_OVERLAY
overlay или avivideo
Кассетный видеомагнитофон или видеоплеер
MCI_DEVTYPE_VCR
ver
Проигрыватель видеодисков
MCIDEVTYPEVIDEODISK
videodisc
Файлы аудиозаписи
MCI_DEVTYPE_WAVEFORM_AUDIO
wave audio
Сравнение сложных и простых устройств
С точки зрения MCI, существует два обширных класса устройств мультимедиа: простые и сложные. Простыми являются устройства, неотделимые от медиа-объектов, которыми они управляют. Например, проигрыватели аудиокомпакт-дисков — это простые устройства, поскольку они одновременно работают только с одним медиа-объектом (компакт-диском), а сам этот медиа-объект либо готов к воспроизведению, либо нет. В противоположность этому виду устройств, сложные устройства способны динамически создавать, сохранять, загружать и выгружать медиа-объекты. Драйвер Windows для обработки файлов аудиозаписи представляет собой пример сложного устройства, которое вы можете использовать для создания и сохранения новых WAV-файлов, а также для загрузки и воспроизведения уже существующих.
Определение устройств, установленных на компьютере
Данные об устройствах мультимедиа сохраняются в системном реестре в разделе HKEY_LOCAL_MACHINE\System\CurrentControlSet\control\MediaResources\mci. На рис. 7.4 представлено окно Редактор реестра с открытой ветвью мультимедиа. Обратите внимание на параметр Disabled раздела реестра, представленного на рис. 7.4. Хотя может быть перечислено несколько устройств MCI, значение параметра Disabled у тех, которые на самом деле не установлены на компьютере, равно 1.
Рис. 7.4. Просмотр в реестре установленных MCI-устройств
Работа с устройствами
Независимо от типа используемого устройства, весь процесс сводится к одному и тому же общему протоколу. Все начинается с открытия этого устройства. Таким образом MCI получает информацию о том, какое устройство будет использоваться, а также готовится к выполнению последующих команд. Если устройство доступно (этого может и не быть — оно может быть занято каким-то другим процессом), MCI назначает ему уникальный идентификатор устройства, который используется приложением для управления устройством.
Открыв устройство, можно управлять им посредством различных команд, применяя один из двух поддерживаемых интерфейсов. В следующих разделах подробно описаны оба интерфейса.
Наконец, по завершении работы с устройством, его необходимо закрыть. При закрытии устройства освобождается память, отведенная для работы MCI. Кроме того, устройство при этом становится доступным для других приложений или процессов. Это очень важно при работе с такими носителями, как аудиокомпакт-диски, поскольку они не могут совместно использоваться несколькими приложениями.
Команды MCI
Независимо от того, какой интерфейс применяется — интерфейс командной строки или интерфейс управляющих сообщений, — устройства управляются посредством фиксированных команд. При использования различных интерфейсов эти команды могут быть представлены как строками, так и числовыми константами. Хотя все устройства должны поддерживать основной набор команд, некоторые из них применимы только для устройств определенного типа. Команды MCI классифицированы по четырем категориям: системные, обязательные, основные и расширенные. Ниже рассматриваются команды всех четырех категорий. Затем приводятся примеры их применения для воспроизведения компакт-дисков, файлов аудиозаписи и цифрового видео.
Системные команды
Системные команды, которых всего две, обрабатываются непосредственно MCI, а не отдельными устройствами. Команда MCIBREAK (представленная также командной строкой "break") устанавливает для MCI-устройства ключ прерывания. Он используется для прерывания таких действий устройства как воспроизведение или запись. Команда MCISYSINFO (командная строка "sysinfo") запрашивает информацию об устройствах MCI.
Обязательные команды
Все устройства MCI должны поддерживать набор обязательных команд. Эти команды вместе со стандартным набором параметров представляют основные возможности всех устройств MCI. В табл. 7.5 приведен список этих команд. (В этой таблице, а также в остальных таблицах этого раздела содержится список констант управляющих сообщений вместе с соответствующими командными строками и их описанием.)
Константа управляющего сообщения
Команда Описание
MCIGETDEVCAPS
Capability Определяет возможности устройства (например, может ли устройство записывать медиа-объекты)
MCICLOSE
CloseЗакрывает устройство
MCIJNFO
InfoПолучает текстовую информацию об устройстве
MCI_OPEN
OpenОткрывает устройство и при необходимости
медиа-объект
MCI_STATUS
StatusПолучает информацию о состоянии устройства
(например, находится ли оно в данный момент в состоянии воспроизведения)
Основные команды
Из табл. 7.5 понятно, что обязательные команды обеспечивают минимум функциональных возможностей, необходимых для инициализации, выполнения запросов и закрытия устройства. Для того чтобы устройство выполняло еще и другие действия, вызовите одну из основных команд, приведенных в табл. 7.6. Большая часть устройств выполняет эти команды, однако не все. Если нужно узнать, поддерживает ли устройство определенную команду, перед ее вызовом воспользуйтесь командой MCI_GET- DEVCAPS, приведенной в табл. 7.5. Эти команды встречаются в большинстве примеров данной главы.
Таблица 7.6. Основные команды MCI
Константа командного сообщения
Команда
Описание
MCI_LOAD
Load
Загружает медиа-объект из файла
MCIPAUSE
Pause
Делает паузу в воспроизведении или записи
MCIPLAY
Play
Начинает воспроизведение
MCIRECORD
Record
Начинает запись
MCIRESUME
Resume
Отменяет паузу в воспроизведении или записи
MC1SAVE
Save
Сохраняет медиа-объект на диске
MCI_SEEK
Seek
Определяет текущую позицию в воспроизведении или записи
MCISET
Set
Устанавливает различные управляющие параметры
MCI_STATUS
Status
Получает информацию о состоянии устройства (заметьте, что эта команда включена также в список обязательных)
MCI_STOP
Stop
Останавливает воспроизведение
Некоторые устройства, такие как проигрыватели видеодисков и цифрового видео, поддерживают расширенные команды. Эти команды являются наиболее сложными из всего множества команд и разработаны для того, чтобы облегчить доступ к параметрам, специфичным для каждого отдельного устройства. В табл. 7.7 приведен список этих команд, а также названия устройств, для работы с которыми они предназначены.
Таблица 7.7. Расширенные команды MCI
Константа командного сообщения
Команда
Описание
Предназначение
MCI.
.CONFIGURE
Configure
Отображает диалоговое окно конфигурации
Цифровое видео
MCI.
.CUE
Cue
Инициирует файл для воспроизведения
Цифровое видео, файлы аудиозаписи
MCI.
.DELETE
Delete
Удаляет часть медиа-объекта
Файлы аудиозаписи
MCI.
.ESCAPE
Escape
Посылает устройству escape-команду
Видеодиск
MCI.
.FREEZE
Freeze
Блокирует поступление видеосигнала
Оверлейное видео
MCI.
.PUT
Put
Определяет исходное окно, окно назначения и окно кадра
Цифровое, оверлейное видео
MCI.
.REALIZE
Realize
Реализует палитру устройства в контексте устройства
Цифровое видео
MCI.
.SETAUDIO
Setaudio
Устанавливает аудиопараметры
Цифровое видео
MCI.
.SETVIDEO
Setvideo
Устанавливает видеопараметры
Цифровое видео
MCI.
.SIGNAL
Signal
Идентифицирует определенную позицию внутри сигнала
Цифровое видео
MCI.
.SPIN
Spin
Начинает или останавливает раскручивание диска
Видеодиск
MCI.
STEP
Step
Осуществляет покадровое воспроизведение
Цифровое видео, видеодиск
MCI.
UNFREEZE
Unfreeze
Разрешает поступление видеосигнала
Оверлейное видео
MCI.
UPDATE
Update
Заново отображает текущий кадр
Цифровое видео
MC1WHERE
Where
Определяет исходную область, область назначения или область кадра
Цифровое, оверлейное видео
MC1_WINDOW
Window
Управляет окном отображения
Цифровое, оверлейное видео
Предназначение
Константа командного Команда Описание сообщенияКонстанта командного Команда Описание сообщенияИнтерфейс командной строки MCI
Константа командного Команда Описание сообщения
Константа командного Команда Описание сообщения
Интерфейс командной строки MCI разработан для того, чтобы обеспечить возможность управления мультимедийными устройствами посредством простых строковых команд. Ориентированный на языки программирования, которые не способны обеспечить простое управление сложными структурами данных, он был вскоре вытеснен более мощным и гибким интерфейсом управляющих сообщений, который описан в следующем разделе. Однако ради полноты изложения мы рассматриваем оба интерфейса. Помимо этого, следует отмстить, что для выполнения простых задач значительно легче использовать командные строки нежели управляющие сообщения.
Функция mciSendString
Все возможности командной строки обеспечиваются одной функцией — mciSendString. Ее описание имеет следующий вид:
Declare Function mciSendString _
Lib "winmm.dll” Alias "mciSendStringA" _
(ByVai IpstrCommand As String, _
ByVai IpstrReturnString As String, _
ByVai uReturnLength As Long, _
ByVai hwndCallback As Long) As Long
Таким образом, функция mciSendString получает четыре исходных параметра, которые определяют выполняемую устройством команду. При успешном завершении функции она возвращает значение 0, а в противном случае — код ошибки, отличный от 0. Коды ошибок тс же, что и в интерфейсе управляющих сообщений. (Для получения более подробной информации обратитесь к разделу "Ошибки МСГ несколько ниже в этой главе.)
Из четырех аргументов функции прежде всего необходимы первые два. Они позволяют направлять команды в MCI и получать результаты. Третий аргумент, uReturnLength, представляет собой индикатор размера возвращаемых данных. Последний аргумент, hwndCallback, представляет собой дескриптор окна, необходимого для получения сообщений о состоянии от MCI. Однако поскольку средствами VBA невозможно создать функцию обратного вызова, этот аргумент практически бесполезен.
Формирование командной строки
Для выполнения операций посредством MCI нужно задать командную строку в качестве аргумента IpstrCommand. Формирование командных строк — это, по-види- мому, вторая по сложности задача при работе с интерфейсом командной строки. (Первая — это анализ возвращаемой информации!) Чем сложнее выполняемая операция, тем сложней становится процедура формирования командной строки. Командные строки формируются в соответствии со стандартным синтаксисом 'действие-объект-модификатор":
команда тип_ среды | имя_ файла I псевдоним [ па раме тры ]
Все командные строки начинаются с команды MCI. За ней следует дескриптор типа среды, зарегистрированное имя файла мультимедиа или псевдоним, который представляет собой просто уникальную строку. Задание псевдонима при открытии устройства упрощает повторное обращение к нему в последующих командах. После указания устройства или псевдонима могут следовать любые опции, необходимые для выполнения определенной команды.
В примере 7.4 приведена простая процедура, которая воспроизводит MIDI-файл. В этом случае командные строки формируются путем присоединения имени файла к командам MCI, а псевдоним используется для повторного обращения к открытому устройству.
Примечание: Во всех наших примерах с MCI программы необходимо выполнять пошагово, иначе устройство будет закрыто еще до того, как у вас появится возможность услышать или увидеть результаты работы программы.
Пример 7.4. Воспроизведение MIDI-файла при помощи интерфейса командной строки MCI
Sub dhPlayMIDIFile(strFile As String)
Dim strCommand As String
Dim strRet As String
Dim IngBytes As Long Dim IngRet As Long
* Открывает файл (файл должен иметь расширение .MID или .RMI). strCommand. = "open ” & strFile & ” alias seq” strRet = Space(255)
IngRet = mciSendString(strCommand, strRet, IngBytes, 0&)
* При успешном выполнении начинает воспроизведение файла. If IngRet = 0 Then
strCommand = "play seq"
strRet = Space(255)
IngRet = mciSendString(strCommand, strRet, IngBytes, 0&) End If
* Закрывает устройство. strCommand = "close seq"
strRet = Space(255)
IngRet = mciSendString(strCommand, strRet, IngBytes, 0&) End Sub
Примечание: Если переданное в командную строку MCI имя файла содержит пробелы, его необходимо заключить в двойные кавычки.
В приведенном примере MCI не возвращает никакой информации, поэтому нс нужно выделять в переменной strRet место под буфер. Для тех команд, которые возвращают информацию (например, команда where), непосредственно перед вызовом функции mciSendString необходимо назначить буфер и проверить его содержимое после того, как функция завершит свою работу. В этом разделе интерфейс командной строки рассмотрен в качестве простого метода воспроизведения объектов мультимедиа. Для получения более подробной информации об использовании интерфейса управляющих сообщений обратитесь к разделу "Информационные функции MCI" несколько ниже в этой главе.
Интерфейс управляющих сообщений MCI
Интерфейс управляющих сообщений MCI представляет собой простой, но мощный механизм управления устройствами мультимедиа. Как и в случае с интерфейсом командной строки, его целью является обеспечение возможности посылать устройствам определенные команды. Однако вместо текстовых строк для указания параметров интерфейс управляющих сообщений использует команды и структуры.
Функция mciSendCommand
Интерфейс управляющих сообщений реализуется путем вызова функции mciSendCommand, описание которой выглядит следующим образом:
Declare Function mciSendCommand _
Lib "winmm.dll” Alias "mciSendCommandA" _
(ByVai wDevicelD As Long, ByVai uMessage As Long, _
ByVai dwParaml As Long, ByVai dwParam2 As Any)As Long
В качестве четырех аргументов функции передается следующая информация:
идентификатор (ID) открытого устройства. Он может принимать значение 0 в том случае, если устройство еще не открыто, или значение MCIALLDEVICEID для отправления сообщения во все открытые устройства;
сообщение, выраженное числовой константой;
комбинация (битовая маска) флагов, связанных с сообщением;
указатель на структуру, содержащую подробную информацию, касающуюся сообщения.
Подобно своему двойнику mciSendString, функция mciSendCommand возвращает значение 0 в случае успешного выполнения и код ошибки MCI в противном случае. Функция mciSendCommand хороша тем, что благодаря своей возможности получать в качестве четвертого аргумента данные различных типов она может работать с самыми разнообразными устройствами.
Основные флаги сообщений
Третий аргумент функции mciSendCommand представляет собой комбинацию флагов, которые выполняют точную настройку заданной команды. Большая часть флагов связана с определенными командами и соответствующими структурами данных. (См. следующий раздел "Информационные структуры сообщений".) Однако существуют два флага, которые можно использовать со всеми командами: MCI_WAIT и MCI_NOTIFY.
Флаг MCI_WAIT дает MCI инструкцию ожидать до тех пор, пока нс выполнится указанная команда. В зависимости от конкретного устройства это ожидание может длиться достаточно долго. Например, устройства воспроизведения аудиокомпакт-дисков в ответ на команду MCI_OPEN должны включить раскрутку диска. Загрузка большого WAV-файла в устройство для работы с файлами аудиозаписи также может отнять немало времени. Если пропустить этот флаг, функция mciSendCommand завершается сразу. Однако будьте осторожны при использовании этой опции — при попытке выполнить другое, подчиненное действие прежде, чем первое полностью завершится (например, воспроизведение WAV-файла еще до окончания его загрузки), команда не выполнится.
Совет: Если флаг MCI_WAIT не используется, состояние устройства можно определить с помощью команды MCI STATUS. (Для получения более подробной информации обратитесь к разделу "Информационные функции МСГ, изложенному ниже в данной главе.)
Флаг MCI_NOTIFY указывает MCI информировать ваше приложение, когда команда завершит свою работу. В частности, он посылает сообщение MM_MCINOTIFY в указанное окно. При использовании инструментальных средств разработки, позволяющих выполнять перехват сообщений окон, используйте этот флаг, например, для того, чтобы определить, когда MCI закончит загрузку файла.
Информационные структуры сообщений
MCI определяет многочисленные типы данных, которые можно использовать в сочетании с различными командами. Описания этих типов включены в модуль basMCiTypes рабочей книги Excel с примерами. Хотя этих типов данных достаточно много, те из них, которые применяются в этой главе, анализируются в разделах с соответствующими примерами.
В общем случае, каждое управляющее сообщение связано с определенной структурой. Например, команда MCI_OPEN использует в своей работе структуру MCI_OPEN_PARMS:
Type MCI_OPEN_PARMS IngCallback As Long IngDevicelD As Long strDeviceType As String strElementName As String strAlias As String
End Type
-% f-% fПримечание: Каждая из структур, разработанных для взаимодействия с командами MCI, содержит элемент IngCallback. Он используется для определения дескриптора окна, которое получает сообщение MM_MCINOTIFY по завершении выполнения команды. Конечно, это свойство полезно, только если вы используете средство разработки, которое может получать очередь сообщений окна, или у вас есть элемент управления ActiveX, способный это делать.
-% f
-% f
Интерфейс сообщений MCI не требует использования каждой переменной —элемента структуры данных. Нужно заполнять только необходимые элементы и передавать комбинацию флагов функции mciSendCommand, которая по этой комбинации определяет, какие элементы используются. Например, открыв устройство для работы с файлами аудиозаписи и присвоив элементу strElementName имя WAV-файла, при вызове функции mciSendCommand в качестве одного из параметров необходимо передать флаг MCI_OPEN_ELEMENT. Если этот флаг установлен, но правильное значение переменной указанного элемента не присвоено, происходит ошибка.
Кроме того, некоторые устройства могут использовать для определенных команд более сложные структуры. Например, устройства оверлейного видео вместе с командой MCI_OPEN применяют структуру MCI_OVLY_OPEN_PARMS вместо структуры MCI_OPEN_PARMS. Такие структуры дают возможность предоставлять дополнительную информацию, специфичную для каждого устройства.
Ошибки MCI
Если при вызове функции mciSendCommand возникает ошибка, в результате возвращается значение кода ошибки, отличное от 0. В MCI для таких случаев предусмотрена функция mciGetErrorString, которая по заданному коду ошибки возвращает ее описание. Поэтому всегда нужно проверять результат вызова функции mciSendCommand, если он больше 0, вызывать функцию mciGetErrorString для получения описания ошибки. Это выполняется посредством функции-оболочки dhMCiError. Ее текст приведен в примере 7.5 вместе с описанием функции mciGetErrorString.
Пример 7.5. Функция для получения описания ошибок MCI
Declare Function mciGetErrorString _
Lib "winmm.dll" Alias "mciGetErrorStringA" _
(ByVai dwError As Long, ByVai IpstrBuffer As String, ByVai uLength As Long) As Long
Function dhMCiError(ByVai IngErr As Long, _
Optional varTag As Variant) As String
Dim strBuffer As String
Dim IngPos As Long
Dim IngRet As Long
If IngErr <> 0 Then
* Устанавливает буфер. strBuffer = Space(1024)
Call mciGetErrorString(IngErr, strBuffer, _ Len(strBuffer))
* Обрезает строку.
IngPos = InStr (strBuffer, vbNullChar)
If IngPos > 0 Then strBuffer = Left(strBuffer, IngPos - 1)
* Получить метку?
IngPos = InStr(strBuffer, " ")
If Not IsMissing (varTag) And. IngPos > 0 Then varTag = Left(strBuffer, IngPos - 1) End If
* Возвращает результат.
dhMCIError = Mid(strBuffer, IngPos + 1)
End If
End If
End Funotion
Функции dhMCIError можно передать необязательный аргумент типа Variant. Все сообщения об ошибках MCI начинаются со строки "mmsystemxxx", где ххх представляет собой код ошибки. Функция dhMCIError обычно отделяет эту закодированную строку с ошибкой от возвращаемого ею текста. Однако если в качестве аргумента varTag переменная передана, функция dhMCIError записывает в нее текст префикса.
Форматы времени MCI
В MCI промежутки времени для различных устройств могут быть представлены в нескольких форматах. Определение текущей позиции и управление воспроизведением во многом зависят от правильного использования этих форматов. В табл. 7.8 приведены числовые константы и описание каждой из них.
Таблица 7.8. Форматы времени MCI
Константа формата времени
Описание
MCI_FORMAT_BYTES
Байты (в формате файлов с импульсно-кодовой модуляцией [РСМ])
MCI_FORMAT_MILLISECONDS
Миллисекунды
MCI_FORMAT_MSF
Минута/секунда/кадр
MCI_FORMAT_SAMPLES
Выборки
MCI_FORMAT_SMPTE_24
SMPTE, 24 кадра
MCI_FORMAT_SMPTE_25
SMPTE, 25 кадров
MCI_FORMAT_SMPTE_30
SMPTE, 30 кадров
MCI_FORMAT_SMPTE_30DROP
SMPTE, 30 кадров (понижение)
MCI_FORMAT_TMSF
Дорожка/минута/секунда/кадр
MCI_SEQ_FORMAT_SONGPTR
Указатель на MIDI-запись
Не все форматы времени подходят каждому из устройств. В общем случае формат TMSF используется для воспроизведения аудиозаписи на компакт-диске, миллисекунды или выборки — для файлов аудиозаписи, а форматы SMPTE (Society of Motion Picture and Television Engineers) — для всех видеоустройств.
Независимо о формата, MCI сохраняет информацию о времени как значение типа Long. Для форматов TMSF, MSF и SMPTE каждый его байт представляет отдельную единицу времени. В табл. 7.9 приведен список этих форматов и информация, содержащаяся в каждом байте.
Таблица 7.9. Хранение данных для различных форматов времени
Позиция байта
TMSF
MSF
SMPTE
Старшее слово/старший байт
Кадры
Не используется
Кадры
Старшее слово/младший байт
Секунды
Кадры
Секунды
Младшее слово/старший байт
Минуты
Секунды
Минуты
Младшее слово/младший байт
Дорожки
Минуты
Часы
Для упрощения преобразования из формата времени MCI в формат времени VBA мы создали функцию, приведенную в примере 7.6. Она вызывается с определенным значением времени и форматом временного интервала MCI. Результатом выполнения этой функции является переменная VBA типа Date. Пример 7.6 содержит также пользовательские типы данных, которые были созданы для более быстрого и простого разбиения длинного целого на составляющие его байты.
Пример 7.6. Преобразование временных значений из формата MCI в формат УВА
Type dhDoubleWordByByte LowWordLowByte As Byte LowWordHighByte As Byte HighWordLowByte As Byte HighWordHighByte As Byte
End Type
Type dhDoubleWordLong DoubleWord As Long
End Type
Function dhMCITimeToVBATime(IngTime As Long, IngTimeFormat As Long) As Date
Dim dwb As dhDoubleWordByByte
Dim dwl As dhDoubleWordLong
Dim datResult As Date
* Разбивает значение типа Long на составляющие ’ четыре байта посредством функции LSet.
dwl.DoubleWord = IngTime
LSet dwb = dwl
* Использует эти байты.
With dwb
’ Какой формат времени используется? Select Case IngTimeFormat
’ Кадры (допустим, 30/сек). Case MCI_FORMAT_FRAMES
' datResult = TimeSerial(0, _ 0, IngTime / 30)
’ Минута/секунда/кадр.
Case MCI_FORMAT_MSF datResult = TimeSerial(0, _ .LowWordLowByte, _ .LowWordHighByte)
’ Дорожка/минута/секунда/кадр.
Case MCI_FORMAT_TMSF datResult = TimeSerial(0, _ .LowWordHighByte, _ .HighWordLowByte)
* Формат SMPE.
’ (час/минута/секунда/кадр)
Case MCI_FORMAT_SMPTE_24, _ MCI_FORMAT_SMPTE_25, _
MCI_FORMAT_SMPTE_30, _ MCI_FORMAT_SMPTE_30DROP, _ MCI_FORMAT_HMS
datResult = TimeSerial( _ .LowWordLowByte, _ .LowWordHighByte, _ .HighWordLowByte)
’ Миллисекунды.
Case MCI_FORMAT_MILLISECONDS datResult = IngTime / 86400000 End Select
End With
* Устанавливает возвращаемое значение. dhMCITimeToVBATime = datResult
End Function
Информационные функции MCI
В дополнение к рассмотренным примерам управления устройствами мультимедиа существует две команды, которые предоставляют возможность получения информации о любом устройстве. Мы включили эти команды, MCI_STATUS и MCI_INFO, в две функции-оболочки.
MCLSTATUS
Команда MCISTATUS используется для получения информации о состоянии устройства. Например, с ее помощью вы можете определить, готово ли устройство для работы с аудиокомпакт-дисками к воспроизведению. Функция-оболочка dhMCi- Status для этой команды приведена в примере 7.7.
Пример 7.7. Функция-оболочка для команды MCI STATUS
Function dhMCIStatus(IngDevID As Long, Ingltem As Long, _
Optional IngAddlFlags As Long = 0, _
Optional bytTrack As Byte = 0) As Long
Dim mst As MCI_STATUS_PARMS Dim IngRet As Long
* Проверяет правильность идентификатора устройства. If IngDevID Then
’ Устанавливает значения компонентов структуры.
* MCI_STATUS_ITEM With mst
.Ingltem = Ingltem
.IngTrack = bytTrack
* Вызывает функцию mciSendCommand.
IngRet = mciSendCommand(IngDevID, MCI_STATUS, _ MCI_STATUS_ITEM Or IngAddlFlags, mst)
* При успешном выполнении возвращает элемент
IngReturn структуры MCI_STATUS_ITEM.
If IngRet = 0 Then dhMCIStatus = .IngReturn End If End With
End If
End Function
Функция dhMCIStatus получает в качестве аргументов идентификатор устройства и номер элемента состояния. Полный список элементов состояния приведен в табл. 7.10. В качестве необязательного параметра можно также передать битовую маску дополнительных флагов, которую функция dhMCIStatus объединяет с обязательным флагом (MCISTATUSITEM) перед передачей функции mciSendCommand. Наконец, для получения информации о состоянии отдельной дорожки носителя функции передается ее номер.
Примечание: Для получения информации относительно какой-либо дорожки в дополнение к ее номеру передайте параметру IngAddlFlags флаг MCITRACK.
Таблица 7.10. Типы MCIJSTATUSJTEM
Константа элемента состоянияОписание
MCIJSTATUS LENGTHВремя воспроизведения определенного
медиа-объекта или дорожки
MC1_STATUS POSITIONТекущая позиция (в текущем формате времени)
MCI_STATUS_NUMBER_OF_TRACKS Число дорожек текущего аудиокомпакт-диска
Константа элемента состояния
MCISTATUSMODE
MCI_STATUS_MEDIA_PRESENT
MC1_STATUS_TIME_FORMAT
MCI_STATUS_READY
MCI_STATUS_CURRENT_TRACK
MCI_WAVE_STATUS_BLOCKALIGN MCI_WAVE_STATUS_FORMATTAG
MCI_WAVE_STATUS_CHANNELS
MCI_WAVE_STATUS_SAMPLESPERSEC
M CI_WAVE_STATUS_AVG BYTES PERS EC MCI_WAVE_STATUS_B ITS PERSAMPLE
MCI_WAVE_STATU S_LEVEL
MCI_SEQ_STATUS_TEMPO
MCI_SEQ_STATUS_PORT
MCI_SEQ_STATUS_OFFSET
MCISEQSTATUSDIVTYPE
Описание
Текущий режим устройства (остановлено, находится в состоянии воспроизведения и т.д.)
Возвращает значение 1, если присутствует носитель аудиокомпакт-дисков
Текущий формат времени
Возвращает значение 1, если устройство готово к воспроизведению/записи
Дорожка аудиокомпакт-диска, которая воспроизводится в данный момент
Определение блока файла аудиозаписи
Метка формата файла аудиозаписи (например, ”РСМ” для импульсно-кодовой модуляции)
Каналы файлов аудиозаписи (1 = моно, 2 = стерео)
Частота выборки для файлов аудиозаписи в
Герцах (11025, 22050 или 44100)
Средний объем 1 секунды аудиозаписи
Разрядность выборки для файлов аудиозаписи (8 или 16)
Уровень записи для файлов аудиозаписи
Темп MIDI-секвенсера
Порт MIDI-секвенсера
SMPTE-смещение MIDI-секвенсера
Тип разделения файлов для MIDI-секвенсора
Для того чтобы определить, готово ли к воспроизведению устройство для аудиокомпакт-дисков, можно воспользоваться следующим оператором:
fReady = Cbool(dhMCIStatus(IngDevicelD, MCI_STATUS_READY))
Команду MCI_STATUS можно применять для получения текущего режима работы устройства. Для этого следует вызвать функцию dhMCIStatus с константой MCI_STATUS_MODE, в результате чего получается одно из значений, приведенных в табл. 7.11. Например, можно определить, находится ли в данный момент проигрыватель аудиокомпакт-дисков в состоянии воспроизведения, воспользовавшись для этого следующим оператором:
flsPlaying = (dhMCIStatus(IngDevicelD, MCI_STATUS_MODE) =
MCI_MODE_PLAY)
Константа режима устройства
Описание
MCI_MODE_NOT_READY
Устройство не готово к воспроизведению или записи
MC1MODESTOP
Устройство в данный момент остановлено
MCI_MODE_PLAY
Устройство находится в состоянии воспроизведения
MCI_MODE_RECORD
Устройство находится в состоянии записи
MCI_MODE_SEEK
Устройство находится в состоянии поиска (перемещения на новую позицию)
MC1_MODE_PAUSE
Устройство находится в состоянии паузы
MC1_MODE_OPEN
Дверца устройства открыта
MCIJNFO
Команда MCIJNFO для получения текстовой информации об устройстве. Например, можно определить уникальный идентификатор носителя для аудиокомпакт-диска. В примере 7.8 представлена функция-оболочка dhMCIInfo для команды MCIJNFO.
Пример 7.8. Функция-оболочка для команды MCIJNFO
Function dhMCIInfo(IngDevID As Long, Inglnfo As Long) As String
Dim min As MCI_INFO_PARMS Dim IngRet As Long
If IngDevID Then With min ’ Устанавливает буфер. .strReturn = Space(255) .IngRetSize = Len(.strReturn) IngRet = mciSendCommand(IngDevID, MCI_INFO, _ Inglnfo, min)
’ При успешном выполнении возвращает часть
* буфера strReturn. If IngRet = 0 Then dhMCIInfo = Left(.strReturn, _ InStr(.strReturn, vbNullChar) - 1) End If End With End If
End Function
При вызове функции dhMCIInfo ей передается идентификатор устройства и одна из констант, приведенных в табл. 7.12. Для получения идентификатора носителя для компакт-диска можно воспользоваться следующим фрагментом программы:
strMedialD = dhMCIInfo (IngDevicelD, MCC_INFO_MEDIA_IDENTITY)
Таблица 7.12. Типы MC1JNF0
Константа элемента информации
Описание
М С I_ IN FOPRO DUCT
Описание аппаратного обеспечения устройства
MCIINFOFILE
Имя файла на носителе
MCI_INFO_MEDIA_UPC
UPC (Universal Product Code) носителя
MCI_INFO_MEDIA_IDENTITY
Уникальный идентификатор носителя
MC1_INFO_NAME
Имя текущей дорожки или MIDI-последовательности
MC MN FO_COPYRIGHT
Информация об авторских правах на носитель
Примечание: Не все константы элементов информации можно задавать при обращении к устройству или носителю конкретного типа. Если в запросе указать несоответствующий элемент, то в результате будет получена пустая строка.
Резюме
В этой главе рассмотрено несколько простых методов, позволяющих встроить элементы мультимедиа в VBА-приложения. Хотя мы не стремились охватить эту тему достаточно широко, вы все же познакомились с некоторыми средствами, позволяющими немного оживить разрабатываемые приложения. Мы описали возможности мультимедиав Windows, в частности средство Media Control Interface, предназ- наченое для выполнения следующих задач:
воспроизведения аудиокомпакт-дисков;
записи и воспроизведения аудиофайлов;
просмотра цифрового видео в окне.
Надеемся, что благодаря предоставленной в этой главе информации, вы самостоятельно разберетесь с тем, как добавлять в свои приложения и другие элементы мультимедиа.
Содержание
Введение5
ГЛАВА 1. Работа со строками9
Как VBA хранит строки?10
Unicode против ANSI11
Использование строк и байтовых массивов11
Встроенные функции для работы со строками12
Сравнение строк12
Преобразование строк15
Формирование строк: функции Space и String16
Вычисление длины строки16
Форматирование строк17
Выравнивание строк18
Поиск строк20
Работа с фрагментами строк21
Замена фрагментов строк23
Коды ANSI24
Работа с байтами24
Комбинирование функций26
Поиск и замена текста26
Замена символа в списке другим символом26
Удаление лишних пробелов30
Удаление из строки конечных нуль-символов31
Замена одной подстроки на другую32
Замена нумерованных меток в строке37
Сбор информации о строках39
Определение атрибутов символа39
Подсчет числа вхождений подстроки в строку42
Преобразование строк44
Реверсия строки44
Представление десятичных чисел римскими цифрами46
Кодирование и декодирование текста при помощи XOR-кодировки48
Дополнение строки слева или справа до заданной ширины51
Сравнение строк при помощи алгоритма Саундекса5 3
Работа с подстроками57
Возвращение определенного слова строки по индексу57
Выборка первого или последнего слова в строке
Поиск последнего вхождения подстроки в строку Резюме
60
62
64
ГЛАВА 2. Работа со значениями даты и времени
65
Что такое дата и как она хранится?
66
Дополнительные преимущества
66
Задание значений даты
67
Встроенные функции VBA для работы с датами
67
Текущая дата и время
68
А если вам нужна только составляющая даты или времени?
69
Выделение составляющих даты
69
Простые вычисления
71
Более сложные задачи
82
Определение конкретной даты
82
Определение начала или конца месяца
83
Определение начала или конца недели
84
Определение начала или конца года
86
Определение начала или конца квартала
87
Определение следующего или предыдущего дня недели
89
Определение следующей годовщины события
90
Операции со значениями даты и времени
91
Сколько дней в указанном месяце?
91
Сколько понедельников в июне?
92
Округление значения времени ыдо ближайшего значения с указанной точностью
94
Преобразование чисел или строк в значения даты и времени заданного формата
96
Работа с промежутками времени
99
Вычисление возраста
100
Форматирование значения промежутка времени
101
Форматирование суммарного времени
105
Резюме
108
ГЛАВА 3. Операции с числами
109
Как VBA хранит числовые значения?
ПО
Целые числа
112
Числа с плавающей точкой и ошибки при их использовании
112
Масштабируемые целые
115
Встроенные числовые функции
116
Математические и тригонометрические функции
116
Преобразование и округление чисел
119
Случайные числа
124
Финансовые функции
126
Преобразование основания системы счисления
130
Пользовательские математические и числовые функции
133
Математические функции
133
Геометрические расчеты
134
Статистические функции
137
Резюме
143
ГЛАВА 4. Поиск и сортировка в VBA
145
Время — деньги
146
Знакомство с классом StopWatch
146
Применение класса StopWatch
147
Использование массивов
148
Все-таки, что такое массив?
148
Создание массива
151
Использование данных в массиве
151
Определение размеров массива
152
Переменная типа Variant при работе с массивами
153
Сортировка массивов
155
Работа алгоритма Quicksort
156
Использование алгоритма Quicksort
165
Проблемы быстродействия
167
Сортировка семейств
169
Поиск
173
Почему применяется алгоритм Binary Search?
173
Как работает алгоритм Binary Search?
174
Использование алгоритма Binary Search
180
Резюме
181
ГЛАВА 5. Профессиональная разработка приложений на VBA
183
Корректная обработка ошибок184
Типы ошибок184
Перехват ошибок выполнения186
Реакция на перехваченные ошибки189
Применение стандартного обработчика ошибок192
Иерархия обработки ошибок в VBA195
Генерация ошибок196
Процесс отладки197
Средства VBA для отладки программ198
Применение окна отладки203
Использование точек останова205
Программы без ошибок207
Оперативная справка215
Резюме217
ГЛАВА 6. Windows Networking API219
Основные сетевые функции220
Использование диалоговых окон работы с сетью220
Управление сетевыми ресурсами227
Получение информации о сетевых ресурсах232
Дополнительные сетевые функции238
Функция WNetGetUniversalName238
Составление списков сетевых ресурсов244
Использование диалогового окна просмотра из библиотеки Shell32257
LAN Manager API262
Резюме270
ГЛАВА 7. Работа с дисками и файлами271
Встроенные функции УВА для работы с дисками и файлами272
Функция DIR272
Использование атрибутов файла274
Операторы перемещения файлов на диске277
Немного информации о файле: FileLen и FileDateTime278
Работа с каталогами279
Если нельзя обойтись без операций ввода-вывода282
Получение дескриптора файла282
Использование оператора Open283
Операции с указателем текущей позиции файла285
Операторы чтения и записи286
Windows API: настоящая эффективность294
Сравнение функций API с функциями VBA294
Получение информации о диске296
Операции с путями доступа304
Эффективная замена функции Dir309
Функции уведомления Windows
Поиск файлов
Определение имен временных файлов
Получение дескрипторов файлов (Windows)
Функции даты и времени Windows API
Операции с временами файлов
Использование стандартных диалоговых окон Windows
Резюме
315
320
326
328
331
334
339
346
ГЛАВА 8. Добавление средств мультимедиа в пользовательские приложения347
Средства мультимедиа для Windows348
Возможности мультимедиа и MCI348
Мультимедиа за один шаг352
Подача звукового сигнала352
MessageBeep: шаг вперед352
Воспроизведение WAV -файлов с помощью функции PlaySound353
Media Control Interface362
Работа с устройствами MCI362
Интерфейс командной строки MCI367
Интерфейс управляющих сообщений MCI369
Резюме378
Учебное пособие
К. Гетц, М. Джилберт
Программирование в Microsoft OfficeДля пользователя
Редактор И.В. Стеценко
Технический редактор С.О. Дынникова
Продукция соответствует требованиямМинистерства здравоохранения Российской Федерации.Гигиеническое заключение 77. ФЦ. 8.953. П. 197.3.99 от 12.03.1999
Лицензия на издательскую деятельность № 071405от 28 февраля 1997 г.
ООО «Спаррк».
123364, Москва, ул. Свободы, д. 28, корп. 2.
Подписано в печать 03.02.00. Формат 70Х
Печать офсетная. Усл. печ. л. 24. Тираж 4$0Фэкз.
Заказ № 175.
Отпечатано с диапозитивов в ГПП «Печатный Дв^Г
Министерства РФ по делам печати^телерадиовещания и средств массовых комму]
197110, Санкт-Петербург, Чкаловский пр., Г5^У * 1