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

WinApi

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

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

Несколько слов об API, Win32, DLL

API (Application Programming Interface - интерфейс прикладных программ) - это множество функций, организованных, обычно, в виде DLL. Функции API позволяют организовать интерфейс между прикладной программой и средой, в которой работает эта программа. Вызов функций API позволяет программе получать доступ к ресурсам среды и управлять ее работой. Как правило, API задает стандарт взаимодействия среды и прикладной программы.

Win32 - это название интерфейса, ориентированного на 32-х разрядные приложения и реализованного на таких известных платформах как Windows 95, Windows 98, Windows NT, Windows CE. Функции, составляющие этот интерфейс, позволяют прикладной программе получать доступ к ресурсам операционной системы и управлять ее работой. Более ранние версии Windows используют интерфейс, известный как Win16. Конечно, не все функции, составляющие интерфейс Win32, реализованы в полной мере на всех платформах, так что вызов одной и той же функции под NT приведет к определенному результату, а под Windows 95 работает как вызов заглушки. Любое из приложений, работающее в среде Windows, прямо или косвенно вызывает функции, входящие в Win32 API.

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

  • Kernel32.dll. Эта библиотека предназначена для работы с объектами ядра операционной системы и ее функции позволяют управлять памятью и другими системными ресурсами.
  • User32.dll. Здесь сосредоточены функции для управления окнами - основным видом объектов операционной системы. Обработка сообщений, работа с меню, таймерами, все это выполняют функции этой DLL.
  • GDI32.dll. Эта библиотека, обеспечивающая графический интерфейс операционной системы (Graphics Device Interface). Функции управления выводом на экран дисплея, управления выводом принтера, функции для работы со шрифтами - все они входят в состав этой библиотеки.
  • Заметьте, Win API функции находятся не только в этих библиотеках. С другой стороны API функции не обязательно входят в состав Win32 интерфейса. Например, MAPI интерфейс (Messaging Application Programming Interface) составляют функции, предназначенные для обработки сообщений электронной почты, TAPI (Telephone API) - функции работы с телефонными сообщениями. MAPI, TAPI, также как и Win32 это некоторый набор функций, задающий определенный стандарт взаимодействия

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

    .VBA и Win32 API

    Работая на VBA, неявно всегда приходится иметь дело с функциями Win32 API, только вызов их упрятан в вызываемых VBA функциях или методах объектов Office 2000. Так, например, при работе с объектом Shape так или иначе будут вызываться функции GDI32, обеспечивающие работу с графикой, при работе c функциями VBA.Interaction, например GetSettings, SaveSettings и другими, будет вызываться соответствующие функции работы с реестром Windows, хранящиеся в библиотеках User32 и advapi32. Такой косвенный вызов имеет свои преимущества, обеспечивая определенную безопасность в работе VBA программ. Но в ряде случаев VBA программисту необходим доступ ко всем возможностям операционной системы, предоставляемым Win32 API интерфейсом. Естественно, в этом случае он понимает, что на него ложится большая ответственность в обеспечении корректного вызова функций, поскольку ошибки в вызове могут привести к непредвиденным отказам в работе программы.

    Вызов функций и оператор Declare

    Элементы ActiveX, COM объекты могут экспонировать свой интерфейс, - свои свойства и методы. Это означает, что они уведомляют, предоставляют информацию клиентам о своем интерфейсе. Технически это обеспечивается тем, что эти объекты, наряду с DLL, сопровождаются TypeLib - библиотекой типов, в которой содержится в требуемом виде информация об интерфейсе объекта. В этом случае, для того чтобы начать работу с объектом, достаточно подключить ссылку на эту библиотеку в меню Tools|References в среде редактора VBE. Эта возможность не раз обсуждалась, когда речь шла о вызове, например, приложения Excel в документах Word. Напомним, что приложения Office 2000 представляют собой ActiveX объекты, построенные на основе COM технологии. Они явно экспонируют свой интерфейс, именно поэтому нет проблем при работе с такими приложениями, вызовами свойств и методов их многочисленных объектов. Библиотеки, составляющие Win32 интерфейс, не сопровождаются библиотеками типов TypeLib. Поэтому необходимо самому программисту уведомить VBA о том, где найти и как следует вызывать ту или иную функцию Win32 API Вызову каждой функции должен предшествовать оператор Declare, описывающий эту функцию. Этот оператор и сама схема вызова библиотечных функций используется при работе с любыми DLL, а не только с теми, которые содержат Win32 API функции. В общем случае в DLL могут храниться как функции, так и процедуры. Два варианта вызова этого оператора соответствуют ссылке на процедуру и на функцию, возвращающую значение. Первый вариант:

    [Public | Private] Declare Sub имя Lib "имя-библиотеки" [Alias "псевдоним"] [([параметры])]

    Во втором случае его синтаксис:

    [Public | Private] Declare Function имя Lib "имя-библиотеки" [Alias "псевдоним"] 
    [([параметры])] [As возвращаемый-тип]

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

  • Ключевое слово Public используется, чтобы сделать объявляемую процедуру доступной всем модулям проекта; ключевое слово Private ограничивает доступ к объявленной процедуре лишь модулем, в котором она объявлена. Заметьте, в стандартном модуле можно использовать оба описателя, но в модуле класса разрешается использовать только описатель Private.
  • Ключевое слово Sub в первом случае означает, что речь идет о процедуре; альтернативный ключ Function во втором случае указывает на функцию, возвращающую значение, которое может быть использовано в выражениях.
  • Обязательный параметр имя является именем объявляемой процедуры или функции. Это имя используется при вызовах в VBA программах. Оно может совпадать или отличаться от того имени, под которым процедура (функция) хранится в DLL. Заметьте, для имен функций Win32 API в отличие от Win 16 имеет значение различие между прописными и строчными буквами.
  • После обязательного ключевого слова Lib должно следовать в кавычках имя-библиотеки, содержащей объявляемую процедуру.
  • Ключ Alias позволяет указать, что процедура имеет в DLL другое имя - " псевдоним ", благодаря чему можно избежать коллизии имен в программе. Если первый символ параметра " псевдоним " не является признаком числа ( # ), псевдоним понимается как имя входной точки DLL для данной процедуры; если же первый символ псевдонима - ( # ), следующие за ним число должно задавать порядковый номер входной точки процедуры в DLL. Дело в том, что процедура может иметь несколько точек входа.
  • Необязательный список параметры задает список параметров, передаваемых процедуре при вызове.
  • As возвращаемый-тип во втором варианте оператора задает тип значения, возвращаемого функцией; им может быть любой из базисных типов VBA (не допускаются только строки фиксированной длины), тип объекта или определенный пользователем тип.
  • Список параметры - это список разделенных запятыми параметров процедуры; каждый элемент этого списка имеет вид:
    [Optional] [ByVal | ByRef] [ParamArray] переменная[()] [As тип]
  • Здесь ключ Optional означает, что данный параметр необязателен; при этом все следующие в списке параметры должны быть необязательными и сопровождаться этим же ключом Optional. Этот ключ нельзя применять, если среди параметров есть массив параметров ParamArray.
  • Ключевые слова ByVal и ByRef указывают на то, что параметр передается по значению или по ссылке; по умолчанию в VBA предполагается передача значения по ссылке ( ByRef ).
  • Ключевое слово ParamArray позволяет задать массив элементов типа Variant ; этот параметр должен быть последним в списке и не должен перед собой иметь ключей ByVal, ByRef или Optional ; такой массив позволяет передавать в процедуру произвольное (заранее неизвестное) число параметров.
  • Переменная - произвольное допустимое в VBA имя переменной; пустые скобки () после имени переменной означают, что соответствующий параметр - массив.
  • Необязательное определение As тип задает тип параметра, значения которого могут быть такими же, как и у описанного выше определения возвращаемый-тип. Задав произвольный тип ( As Any ), Вы исключите проверку при передаче параметра процедуре.
  • Вот пример задания оператора Declare для двух функций Win32 API:

    Private Declare Function CreateRectRgn Lib "gdi32" Alias "CreateRectRgn" _
    (ByVal X1 As Long, ByVal Y1 As Long, ByVal X2 As Long, ByVal Y2 As Long) As Long
    Private Declare Function GetTempPath Lib "kernel32" _
    		Alias "GetTempPathA" (ByVal nBufferLength As Long, _
    		ByVal lpBuffer As String) As Long

    Синтаксически оператор Declare прост и понятен, нужно указать библиотеку, имя функции, под которым она будет вызываться в VBA программе, ее имя (псевдоним) под которым она записана в библиотеке и параметры функции в привычном синтаксисе. Однако реальная жизнь не так проста и в организации вызова функций API есть много подводных камней. Дело в том, что DLL служат средством межязыкового взаимодействия. Сама DLL может быть разработана на одном языке, а вызываться в другом. Тогда возникает проблема правильной передачи параметров, поскольку может не быть точного соответствия между типами данных двух используемых языков. Функции Win32 API разработаны в ориентации на синтаксис языка C и C++. Поэтому при записи оператора Declare требуется корректно указать типы параметров, так чтобы они соответствовали типам, используемым в языке C. Еще одна проблема состоит в том, что помимо оператора Declare, в ряде случаев необходимо предварительно описать требуемые типы данных и константы, необходимые в процессе вызова функции. Так что, прежде чем вызвать функцию из DLL, необходимо корректно задать оператор Declare, описать необходимые типы и константы, - все это может быть не столь простой задачей.

    Две кодировки ANSI и Unicode

    Win32 API функции могут существовать в двух вариантах, ориентированных на две кодировки - ANSI, когда символы строк кодируются одним байтом, и Unicode, предполагающей двухбайтную кодировку. По этой причине в DLL возможны три варианта для имен функций:

  • Имя. В этом случае кодировка не играет значения.
  • ИмяA. Окончание A означает, что функция использует ANSI кодировку.
  • ИмяW. Окончание W (от Wide) свидетельствует об Unicode кодировке.
  • Чтобы выбрать функцию в нужной кодировке, нужно правильно задать Alias имя функции. Заметьте, что в VBA вызываются функции Win32 в ANSI кодировке, поэтому следует выбирать имена с окончанием A в тех случаях, когда при вызове функции используются строки и кодировка имеет значение.

    API Viewer

    Мы уже говорили об одной из основных проблем, возникающих при вызове Win32 API функций, - необходимо обеспечить корректное задание оператора Declare, описание типов данных и констант, для чего нужно уметь корректно транслировать описание этих объектов из языка C в язык VBA. Чтобы облегчить решение этой задачи, можно воспользоваться специальным инструментальным средством, называемым API Viewer. Это средство доступно в поставке Office 2000 Developer и в Microsoft Visual Basic. Этот обозреватель позволяет:

  • Загрузить прилагаемый текстовый файл Win32API.txt, содержащий информацию обо всех используемых при вызовах Win32 API константах, типах, функциях и операторах Declare.
  • Преобразовать этот файл в базу данных Access - Win32API.mdb файл, что позволяет ускорить работу, работать с формами и запросами к базе данных.
  • Получить корректную для VB и VBA форму записи операторов Declare.
  • Получить корректную для VB и VBA форму записи типов данных.
  • Получить корректную для VB и VBA форму записи констант.
  • Взгляните на начальный этап работы с обозревателем, где выбирается текстовый файл для просмотра. Обратите внимание, что обозреватель позволяет работать с текстовыми файлами, содержащими информацию о различных интерфейсах, в частности Win32 и Mapi интерфейсах:

    (рис 6.1) Выбор файла Win32API для просмотра в API Viewer

    На следующем рисунке можно увидеть результаты нашей дальнейшей работы с API Viewer. Вначале из верхнего выпадающего списка API Types мы выбрали элемент Declares, в окне Available Items из списка поочередно выбрали интересующие нас функции Win32 API, - соответствующая этим функциям корректная форма записи оператора Declare появилась в окне Selected Items. На следующем шаге из верхнего списка был выбран элемент Types, из списка типов был выбран тип, в нашем примере MenuItemInfo, и корректное для VBA его определение появилось в окне Selected Items. Аналогично, можно получить корректное определение констант. Обычная технология Copy - Paste позволяет перенести информацию непосредственно в программу VBA. Взгляните, как выглядят окна обозревателя на этом этапе работы:

    (рис 6.2) Получение корректной формы записи оператора Declare в окне API Viewer

    Хотя, несомненно, обозреватель полезен, у него есть один недостаток. Было бы полезнее, если бы выбрав имя функции из списка, вся необходимая для вызова этой функции информация, - оператор

    Для того чтобы получить справку по функции Win32 API, следует воспользоваться режимом Search. В окне поиска следует набрать имя нужной функции, предварительно задав требуемую область поиска. При задании области поиска необходимо, по крайней мере, включить флажки Platform SDK Documentation и Others SDK Documentation. При расширении этой области можно получить не только описание функций, но и статьи с примерами их использования. Заметьте, на этом сервере Вы всегда найдете много полезной информации.

    Два языка: C и VB. Различия при вызове функций

    Итак, чтобы корректно вызывать Win32 API функции на VBA, следует разбираться, чем отличаются вызовы функций в языках C и Visual Basic (VBA). Даже, если используется обозреватель API Viewer, который автоматически транслирует типы языка C в типы языка VBA, знание этих различий необходимо во многих ситуациях.

    Соответствие между простыми типами данных

    Нижеследующая таблица описывает соответствие между некоторыми, простыми типами данных двух языков:

    Соответствие между типами языков C и VBA
    C/C++ тип данных Венгерская нотация Описание Тип языка VBA
    BOOL b 8-и битное булево значение. Значение 0 эквивалентно False, ненулевое значение - True Boolean или Long
    BYTE ch 8-и битное целое без знака Byte
    HANDLE h 32 -х битное целое без знака, задающее описатель Windows -объектов Long
    int n 2-х байтное целое со знаком Integer
    UINT u 2-х байтное целое без знака Long
    DWORD dw 4-х байтное целое без знака Long
    long l 4-х байтное целое со знаком Long
    LP lp 32-х битный указатель на C/C++ структуры, строки, функции или другие данные в памяти Long
    LPZSTR lpsz 32-х битный указатель на C строку, завершаемую нулем Long

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

    Структуры языка C и тип, определенный пользователем, в языке VBA

    В языке C можно определять записи - совокупность данных разного типа. Такие записи в языке C называются структурами. Обычно, вначале определяется соответствующий структурный тип, а затем имя этого типа используется при объявлении конкретных переменных. Структуры могут передаваться в качестве аргументов при вызове функций, в том числе при вызове Win32 API функций. В VBA, как известно, для задания подобного структурного типа используется конструкция Type…End Type, называемая типом, определенным пользователем. Переменные такого типа передаются вызываемой функции обычным способом: X As T, где T - имя пользовательского типа. Позже мы приведем пример вызова функции Win32 API, которой передается структура в качестве аргумента.

    Об описателях языка C и объектах Windows

    В языке C особенно при работе с объектами Windows, широко используется тип Handle - задающий описатели объектов. Когда создаются объекты ядра операционной системы и такие основные объекты, как окна, - все они снабжаются описателем, имеющим тип Handle. Описатель представляет длинное целое и однозначно идентифицирует объект. Всегда, когда в функцию Win32 API необходимо передать такой объект, то реально передается его описатель. При трансляции описателей в тип языка VBA ему ставится в соответствие тип Long. При этом желательно понимать, что речь все-таки идет об описателях, создаваемых операционной системой, в момент создания соответствующего объекта. Соответствующие примеры будут даны чуть позже.

    Void функции языка C

    В языке С формально нет процедур, есть только функции. В тех случаях, когда по существу речь идет о процедурах, вычисляющих не один скалярный результат, а имеющих несколько выходных параметров, как правило, используется функция, результат которой говорит об успешности выполнения процедуры. Если результат функции имеет значение True, то все выходные параметры благополучно вычислены, в противном случае следует проанализировать причину неуспеха. Типично для языка C то, что вызов функций является условием оператора If, - оператор If задает упаковку вызова функции, позволяя не только вызвать функцию, но и проверить корректность завершения ее работы. При трансляции заголовков таких функций в язык VBA, они естественно транслируются в функции VBA и для работы с ними можно сохранить стиль языка C.

    Однако в языке C используются и функции, не возвращающие результата. Результат таких функций задается описателем Void, по существу, они являются процедурами. И при трансляции их в VBA их и следует задавать в виде процедур.

    Вызов аргументов по ссылке ByRef и по значению ByVal

    В языке C++ основным способом передачи параметров является передача их по значению, в VBA - по ссылке. Конечно, в обоих языках применяются оба способа. Тем не менее, нужно понимать, что описатель ByVal очень часто будет встречаться при вызове Win32 API функций, значительно чаще, чем при вызове обычных VBA функций. Если используется API Viewer, то этот описатель автоматически будет появляться для тех параметров, где необходима подобная форма вызова. Следует обратить внимание на одно обстоятельство. Несмотря на то, что такой описатель может появляться и для строковых аргументов, строки всегда передаются в функции Win32 API по ссылке. Последнее обстоятельство также связано со спецификой работы со строками в функциях языка C, на которых следует остановиться подробнее.

    Строковые аргументы при вызове функций Win32 API

    Как мы уже говорили, строки передаются по ссылке, даже если у параметра указан описатель ByVal. Не возникает никаких проблем при передаче строки в функцию Win32 API. Передаваемый аргумент может быть произвольным строковым выражением, в том числе переменной типа String или строковой константой. Сложнее дело обстоит, если функция должна вернуть строку, в качестве результата. Прежде всего, напомним, что в функциях Win32 API тип возвращаемого значения никогда не является строкой, - это всегда целочисленное значение, чаще всего булево значение, указывающее на то, удачно ли завершилось выполнение функции. Поэтому, если нужно получить строку в качестве результата, то функции передается два параметра - строка и ее длина, описанные чаще всего следующим образом:

    ByVal lpResultStr As String, ByVal LenResultString As Long

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

  • она должна быть строкой, завершаемой нулем. Чаще всего она вся состоит из нулевых символов.
  • Ее длина должна быть достаточной для того, чтобы вместить результирующую строку.
  • Параметр LenResultString должен указывать максимально допустимое число символов результирующей строки.
  • С учетом этих требований перед вызовом функций, возвращающих строковые значения, как правило, передаваемая функции строка инициализируется и набивается подходящим количеством нулевых символов. Напомним, нулевой символ задается константой vbNullChar. Обычно, это делается следующим образом:

    Const MaxSize = 255 As Long
    lpResultString = String$(MaxSize, vbNullChar)
    LenResultString = Len(lpResultString)

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

    Тип Any

    Иногда, параметр функции API может принимать значения различных типов. В этом случае в операторе Declare нельзя указать один конкретный тип этого параметра. Для решения подобной проблемы и был введен специальный тип Any. Если параметр имеет этот тип, то в период трансляции не проверяется правильность соответствия типов и аргумент может принимать значения любых типов. Конечно, нужно иметь в виду, что функция может принимать значения хоть и нескольких, но совершенно определенных типов. Поэтому при работе с типом Any на программиста возлагается вся ответственность за корректную передачу значения функции API. Альтернативой применения типа Any является задание для одной функции нескольких операторов Declare, - по одному на каждый возможный тип параметра. Тогда в каждой конкретной ситуации будет вызываться соответствующая функция с нужным типом параметра.

    Примеры работы с Win32 API функциями

    Разговор об особенностях вызова Win32 API функций еще не закончен. Тем не менее, пора прервать общее изложение и обратиться к примерам. Многое из того, что было сказано, целесообразно проиллюстрировать примерами работы с конкретными функциями Win 32 API. Каждый из наших примеров будет посвящен работе с некоторой группой функций.

    Работа с окнами

    Как мы уже говорили, окна - это один из основных объектов операционной системы. Функции для работы с ними находятся, в основном, в библиотеке User32. Из большого множества функций мы отобрали несколько функций, позволяющих продемонстрировать, как можно получать описатели окон, как, зная описатель, можно получать характеристики окон и как можно изменять характеристики окон. Начнем с приведения программного текста, а уж потом подробно прокомментируем его. Заметим, что для получения корректного описания операторов Declare, используемых типов данных и констант мы использовали API Viewer и описание функций, которое можно найти на уже упоминавшемся сервере Microsoft.

    В проекте нашего тестового документа был создан модуль "Окна" и в разделе его объявлений помещен следующий текст:

    Option Explicit
    'Константы
    Public Const SW_HIDE = 0
    Public Const SW_SHOWNORMAL = 1
    Public Const SW_SHOWMINIMIZED = 2
    Public Const SW_SHOWMAXIMIZED = 3
    
    'Типы
    Public Type RECT
    			Left As Long
    			Top As Long
    			Right As Long
    			Bottom As Long
    End Type
    
    'Функции
    Public Declare Function GetActiveWindow Lib "user32" () As Long
    
    Public Declare Function GetWindowRect Lib "user32" (ByVal hwnd As Long, _
    			lpRect As RECT) As Long
    
    Public Declare Function GetWindowText Lib "user32" Alias "GetWindowTextA" _
    			(ByVal hwnd As Long, ByVal lpString As String, ByVal cch As Long) As Long
    
    Public Declare Function FindWindow Lib "user32" Alias "FindWindowA" _
    			(ByVal lpClassName As String, ByVal lpWindowName As String) As Long
    
    Public Declare Function ShowWindow Lib "user32" (ByVal hwnd As Long, _
    			ByVal nCmdShow As Long) As Long
    
    Public Declare Function SetWindowText Lib "user32" Alias "SetWindowTextA" _
    			(ByVal hwnd As Long, ByVal lpString As String) As Long

    Дадим краткую характеристику используемых функций:

  • GetActiveWindow возвращает описатель активного окна.
  • GetWindowRect получает в качестве входного параметра описатель окна hwnd и возвращает значения полей структуры Rect, переданной функции в качестве второго параметра lpRect. Заметьте, тип Rect должен быть предварительно определен. Обратите внимание, первый параметр передается по значению, а второй по ссылке. Передача по ссылке позволяет функции заполнить значениями поля переданной ей структуры. Значения этих полей задают координаты прямоугольника, определяющего положение окна на экране дисплея. Возвращаемый функцией результат будет равен нулю, если выполнение функции закончится неуспехом, например, при некорректном задании описателя.
  • GetWindowText по описателю окна возвращает его заголовок. Поскольку функция должна вернуть строку, то, как мы говорили ранее, ей передаются два параметра - lpString и cch, задающие строку и число символов, доступных для заполнения. Обратите внимание, здесь используется псевдоним с окончанием A, указывающим на использование кодировки ANSI. Опять-таки, результат, возвращаемый функцией, зависит от успеха операции.
  • FindWindow возвращает описатель окна. Функция ищет окно и возвращает в случае успеха описатель этого окна. Окно ищется либо по имени класса, заданного параметром lpClassName, либо по заголовку, заданному параметром lpWindowName. При вызове может быть задан только один из этих параметров, второй может быть указателем с неопределенным значением.. Поскольку параметр может быть либо указателем, имеющим тип Long, либо строкой, то в предыдущих версиях по этой причине для обоих параметров следовало указывать тип Any. Теперь указывается тип String, поскольку введена специальная константа vbNullString, формально имеющая тип String, но позволяющая передать указатель со значением Null.
  • ShowWindow позволяет сделать видимым или невидимым окно, заданное описателем, может также минимизировать, максимизировать или нормализовать его размеры. Соответствующее действие определяется вторым параметром, значения которого задаются соответствующей константой. В разделе объявлений модуля мы определили возможные значения этих констант, которые будут использованы в процессе вызова этой функции.
  • SetWindowText позволяет установить новый заголовок окна. Является функцией, парной к функции GetWindowText, но, поскольку здесь строка заголовка передается функции, то функция устроена проще, имеет два, а не три параметра и передавать ей можно константу, что и будет продемонстрировано в нашем примере.
  • Приведем теперь процедуру, в которой поочередно запускаются описанные здесь функции (В примерах используются документы: DocOne6, DocTwo6

    Public Sub WorkWithWindows()
    		Dim Res As Long 'Результат выполнения функции
    		Dim HandleAW As Long		'Описатель активного окна
    		Dim RectAW As RECT	'Структура, задающая прямоугольник окна
    		Dim TextAW As String 'Заголовок активного окна
    		Dim LenTextAW As Long 'Длина строки
    		Dim HandleW As Long		'Описатель окна
    		Dim TextW As String 'Заголовок окна
    		
    		'Получить описатель активного окна
    		HandleAW = GetActiveWindow
    		Debug.Print HandleAW
    		
    		'Получить прямоугольник, задающий положение активного окна
    		Res = GetWindowRect(HandleAW, RectAW)
    		Debug.Print Res
    		If Res > 0 Then 'OK
    			Debug.Print "Размеры окна: Left = ", RectAW.Left, " Top = ", _
    			RectAW.Top, " Right = ", RectAW.Right, " Bottom = ", RectAW.Bottom
    		Else:
    		MsgBox ("Не удалось получить размеры активного окна")
    		End If
    		'Получить заголовок окна
    		'Предварительная набивка результирующей строки	нулевыми символами
    		TextAW = VBA.String$(255, vbNullChar)
    		LenTextAW = VBA.Len(TextAW)
    		Res = GetWindowText(HandleAW, TextAW, LenTextAW)
    		Debug.Print Res
    		If Res > 0 Then 'OK
    			TextAW = VBA.Left(TextAW, VBA.InStr(1, TextAW, vbNullChar) - 1)
    			Debug.Print TextAW
    		Else:
    		MsgBox ("Не удалось получить заголовок активного окна")
    		End If
    		
    		'Поиск окна документа по его заголовку
    		'Возвращается описатель окна
    		TextW = "DocOne6 - Microsoft Word"
    		HandleW = FindWindow(vbNullString, TextW)
    		If HandleW > 0 Then 'OK
    			Debug.Print HandleW
    		Else:
    		MsgBox ("Не удалось найти окно с указанным заголовком" _
    		 vbCrLf  TextW)
    		End If
    		
    		'Минимизация и нормализация окна документа
    		Res = ShowWindow(HandleW, SW_SHOWMINIMIZED)
    		If Res > 0 Then Debug.Print "Окно минимизировано"
    		Res = ShowWindow(HandleW, SW_SHOWNORMAL)
    		If Res > 0 Then Debug.Print "Окно	в нормальном состоянии"
    		
     	'Изменение заголовка окна
    		TextW = "Document1 - Microsoft Word"
    		HandleW = FindWindow(vbNullString, TextW)
    		If HandleW > 0 Then 'OK
    			Debug.Print HandleW
    		Else:
    		MsgBox ("Не удалось найти окно с указанным заголовком" _
    		 vbCrLf  TextW)
    		End If
    		Res = SetWindowText(HandleW, "DocTwo6 - Microsoft Word")
    End Sub

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

    655706 
     1 
    Размеры окна:      Left = 8      Top = 73      Right = 769      Bottom = 580 
     58 
    Microsoft Visual Basic - DocOne6 [running] - [Окна (Code)]
     6684884 
    Окно минимизировано
    Окно	в нормальном состоянии
     191103490

    Дадим комментарии к работе этой процедуры:

  • Вначале, при вызове функции API GetActiveWindow был получен описатель активного окна. Заметим, что это было окно кода выполняемой процедуры. Значение этого описателя равно 655706.
  • Затем, при вызове функции GetWindowRect, был получен прямоугольник, определяющий положение окна на экране. Функция успешно завершила работу и вернула результат, равный 1 (истина). Как и в остальных случаях, по окончании выполнения функции мы проверили результат на успешность завершения.
  • На следующем шаге был получен заголовок активного окна. Как видно из распечатки заголовка, активным являлось выполняемое окно кода. Результат 58, который вернула функция GetWindowText, задает число символов результирующей строки. Обратите внимание, перед вызовом функции передаваемая ей строка была инициализирована нулевыми символами. После успешного завершения из строки был выделен результат, задающий заголовок. Его печать показывает, что активным в момент запуска функции было выполняемое окно кода. Для выделения заголовка из строки использовался нулевой символ, как признак окончания заголовка. Для решения этой задачи можно было использовать и число возвращаемых символов - значение, возвращаемое функцией.
  • Следующим шагом было получение описателя окна по заданному заголовку. В качестве такового был использован заголовок окна с тестовым документом. Функция FindWindow нашла такое окно и вернула его описатель, равный 6684884. Обратите внимание, первый параметр был задан константой vbNullString.
  • Окно, описатель которого был получен в последнем вызове, дважды перестраивалось, - вначале минимизировалось, затем нормализовалось, - при двух вызовах функции ShowWindow с различными значениями констант.
  • На последнем этапе снова вызывалась функция FindWindow для нахождения описателя вновь открытого документа. Затем при вызове функции SetWindowText был изменен заголовок этого окна. Заметьте, новое значение заголовка передавалось функции в виде обычной строковой константы.
  • Характеристики окружения

    Наш следующий пример показывает, как получить, используя вызов Win32 API функций, следующие характеристики окружения:

  • Версию используемой операционной системы.
  • Тип процессора.
  • Общий и используемый объем физической и виртуальной памяти.
  • Функции, используемые в этом примере, принадлежат библиотеке Kernel32. Заметьте, что в отличие от большинства случаев в данном примере будут вызываться не только функции, но и процедуры, хранящиеся в библиотеке Kernel32. Начнем с описания констант, типов и операторов Declare, предшествующих вызову API функций:

    Option Explicit
     'Константы
    		Public Const PROCESSOR_INTEL_386 = 386
    		Public Const PROCESSOR_INTEL_486 = 486
    		Public Const PROCESSOR_INTEL_PENTIUM = 586
    		Public Const PROCESSOR_MIPS_R4000 = 4000
    		Public Const PROCESSOR_ALPHA_21064 = 21064
    'Типы
    		Type SYSTEM_INFO
    				dwOemID As Long
    				dwPageSize As Long
    				lpMinimumApplicationAddress As Long
    				lpMaximumApplicationAddress As Long
    				dwActiveProcessorMask As Long
    				dwNumberOrfProcessors As Long
    				dwProcessorType As Long
    				dwAllocationGranularity As Long
    				dwReserved As Long
    		End Type
    		Type OSVERSIONINFO
    				dwOSVersionInfoSize As Long
    				dwMajorVersion As Long
    				dwMinorVersion As Long
    				dwBuildNumber As Long
    				dwPlatformId As Long
    				szCSDVersion As String * 128
    		End Type
    		Type MEMORYSTATUS
    				dwLength As Long
    				dwMemoryLoad As Long
    				dwTotalPhys As Long
    				dwAvailPhys As Long
    				dwTotalPageFile As Long
    				dwAvailPageFile As Long
    				dwTotalVirtual As Long
    				dwAvailVirtual As Long
    		End Type
    'Операторы Declare
    		Declare Function GetVersionEx Lib "kernel32" Alias "GetVersionExA" _
    			(LpVersionInformation As OSVERSIONINFO) As Long
    		Declare Sub GlobalMemoryStatus Lib "kernel32" (lpBuffer As _
    			MEMORYSTATUS)
    		Declare Sub GetSystemInfo Lib "kernel32" (lpSystemInfo As _
    			SYSTEM_INFO)

    Для сбора информации об окружении - используемом процессоре, операционной системе, имеющейся памяти и других характеристиках компьютера используем три следующие функции (процедуры) Win32 API:

  • Функция GetVersionEx имеет в качестве единственного параметра, передаваемого по ссылке, структуру типа OSVERSIONINFO. В результате выполнения функции будут заполнены поля этой структуры, содержащие всю необходимую информацию об используемой версии операционной системы. Конечно, необходимо в раздел объявлений включить и объявление типа OSVERSIONINFO. Возвращаемый функцией результат позволяет проанализировать успешность ее выполнения.
  • Процедура GlobalMemoryStatus имеет единственный, передаваемый по ссылке параметр типа MEMORYSTATUS. В результате выполнения процедуры поля структуры заполняются собранной информацией об объеме физической и виртуальной памяти, общей и доступной для использования в текущий момент, размере слова и некоторых других характеристиках памяти.
  • Процедура GetSystemInfo имеет единственный, передаваемый по ссылке параметр типа SYSTEM_INFO. В результате выполнения процедуры поля структуры заполняются собранной системной информацией о типе процессора, числе процессоров, используемом размере страницы и других характеристиках компьютера. Нужно ли говорить, что для корректного вызова функции требуется объявление типа SYSTEM_INFO и констант, необходимых для анализа значений поля dwProcessorType, определяющего тип центрального процессора.
  • Приведем теперь процедуру, в которой вызываются указанные функции API, обрабатывается полученная информация, конечные результаты выводятся в окно отладки:

    Public Sub WorkWithStatus()
    		Dim res As Long		'Результат выполнения функции
    		Dim msg As String			' Формируемое сообщение
    		Dim verinfo As OSVERSIONINFO		'Информация об ОС и ее версиях
    		Dim sysinfo As SYSTEM_INFO	'Системная информация
    		Dim memstatus As MEMORYSTATUS 'Информация о статусе памяти
    
    		verinfo.dwOSVersionInfoSize = Len(verinfo)
    		res = GetVersionEx(verinfo)
    		If res > 0 Then
    			Select Case verinfo.dwPlatformId
    				Case 0
    						msg = "Windows 32s "
    				Case 1
    						msg = "Windows 95/98 "
    				Case 2
    						msg = "Windows NT "
    			End Select
    			msg = msg  verinfo.dwMajorVersion  "."  verinfo.dwMinorVersion
    			msg = msg  " (Build "  verinfo.dwBuildNumber  ")"  vbCrLf
    		Debug.Print msg
    		Else
    			MsgBox ("Не могу получить версию операционной системы")
    		End If
    		
    		' определение типа процессора
    		GetSystemInfo sysinfo
    		msg = "Процессор: "
    		Select Case sysinfo.dwProcessorType
    			Case PROCESSOR_INTEL_386
    				msg = msg  "Intel 386"  vbCrLf
    			Case PROCESSOR_INTEL_486
    				msg = msg  "Intel 486"  vbCrLf
    			Case PROCESSOR_INTEL_PENTIUM
    				msg = msg  "Intel Pentium"  vbCrLf
    			Case PROCESSOR_MIPS_R4000
    				msg = msg  "MIPS R4000"  vbCrLf
    			Case PROCESSOR_ALPHA_21064
    				msg = msg  "DEC Alpha 21064"  vbCrLf
    			Case Else
    				msg = msg  "(unknown)"  vbCrLf
    
    		End Select
    		Debug.Print msg
    		msg = "Число процессоров: "  sysinfo.dwNumberOrfProcessors  vbCrLf
    		Debug.Print msg
    		msg = "Размер страницы: "  sysinfo.dwPageSize  vbCrLf
    		Debug.Print msg
    		msg = "Минимальный адрес приложения: "  sysinfo.lpMinimumApplicationAddress  vbCrLf
    		Debug.Print msg
    		msg = "Максимальный адрес приложения: "  sysinfo.lpMaximumApplicationAddress  vbCrLf
    		Debug.Print msg
    
    		' Получение характеристик памяти
    		GlobalMemoryStatus memstatus
    		msg = "Физическая память. Всего: "  _
    			VBA.Format$(memstatus.dwTotalPhys \ 1024, "###,###,###")  "K"  vbCrLf
    		Debug.Print msg
    	
     	msg = "Физическая память. Доступно: "  _
    			VBA.Format$(memstatus.dwAvailPhys \ 1024, "###,###,###")  "K"  vbCrLf
    		Debug.Print msg
    
    		msg = "Виртуальная память. Всего: "  _
    			VBA.Format$(memstatus.dwTotalVirtual \ 1024, "###,###,###")  "K"  vbCrLf
    		Debug.Print msg
    		msg = "Виртуальная память. Доступно: "  _
    			VBA.Format$(memstatus.dwAvailVirtual \ 1024, "###,###,###")  "K"  vbCrLf
    		Debug.Print msg
    		msg = "Длина слова: "  memstatus.dwLength  vbCrLf
    		Debug.Print msg
    		msg = "Загрузка памяти: "  memstatus.dwMemoryLoad  vbCrLf
    		Debug.Print msg
    
    End Sub

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

    Windows NT 4.0 (Build 1381)
    
    Процессор: Intel Pentium
    
    Число процессоров: 1
    
    Размер страницы: 4096
    
    Минимальный адрес приложения: 65536
    
    Максимальный адрес приложения: 2147418111
    
    Физическая память. Всего: 32 180K
    
    Физическая память. Доступно: 4 772K
    
    Виртуальная память. Всего: 2 097 024K
    
    Виртуальная память. Доступно: 1 982 872K
    
    Длина слова: 32
    
    Загрузка памяти: 0

    Вызов функций Win32 API, работающих в Unicode кодировке

    Уже говорилось, что функции API, работающие со строками, вызываются в ANSI кодировке. Сейчас мы попытаемся объяснить причину этого факта, а, с другой стороны, покажем, как можно вызывать функции Win32 API, использующие Unicode кодировку. Заметим, что это может быть важным не столько для функций Win32 API, сколько для других внешних функций, которые могут существовать в кодировке Unicode и не иметь ANSI варианта. Начнем с объяснения ситуации, - почему в VBA внешние функции вызываются в кодировке ANSI. Следует понимать, что строки VBA хранятся в Unicode кодировке и передача строк при вызове внутренних функций внутри VBA происходит в кодировке Unicode. Однако VB и VBA предполагают, что внешний мир устроен по-другому и до сих пор использует кодировку ANSI. Поэтому всякий раз, когда вызываются внешние функции, при вызове происходит преобразование и строковая информация передается и возвращается в кодировке ANSI. По этой причине нельзя вызвать функцию в кодировке Unicode простым изменением псевдонима, задав у него окончание W. Покажем сейчас, как можно "обмануть" VBA, заставив его не выполнять указанных преобразований, что и позволит вызывать функции, корректно работающие в Unicode кодировке. Покажем также, что, как и всякий обман, не всегда все заканчивается благополучно. Тем не менее, с предлагаемым приемом полезно познакомиться. Решение задачи основывается на следующем:

  • При вызове функции вместо строки используется массив байтов, хранящий копию строки. Напомним, что внутри VBA строка хранится в Unicode кодировке, поэтому и массив байтов будет хранить строку в этой кодировке.
  • В операторе Declare необходимо тип String изменить на тип Any, что обеспечит отсутствие проверок и преобразований.
  • Если в операторе Declare для строкового параметра указан спецификатор ByVal, то его необходимо удалить или изменить на ByRef, явно указав передачу параметра по ссылке.
  • Остальные детали рассмотрим после приведения соответствующей программы. В качестве примера мы воспользуемся уже рассмотренными функциями:

  • FindWindow, которая позволяет найти окно по его заголовку, вернув описатель окна в качестве результата,
  • GetWindowText, SetWindowText, позволяющие получить и установить новый заголовок окна.
  • Заметим сразу же, что нам удалось успешно вызвать и корректно работать с двумя последними функциями в Unicode кодировке. Однако этот прием не работает при вызове функции FindWindowW. Несмотря на все попытки, переданная для поиска строка заголовка не приводила к успешному завершению поиска. Но обо всем по порядку. Приведем вначале раздел объявлений модуля с именем Unicode, созданного для работы с этим примером:

    Option Explicit
    'Объявление вызываемых функций в Unicode кодировке
     Public Declare Function FindWindowA Lib "user32" _
    			(ByVal lpClassName As String, ByVal lpWindowName As String) As Long
    'Функции в Unicode кодировке
    'Тип string заменен на Any. Передача аргумента по ссылке
     Public Declare Function FindWindowW Lib "user32" _
    			(lpClassName As Any, lpWindowName As Any) As Long
     
     Public Declare Function GetWindowText Lib "user32" Alias "GetWindowTextW" _
    			(ByVal hwnd As Long, lpString As Any, ByVal cch As Long) As Long
     
     Public Declare Function SetWindowText Lib "user32" Alias "SetWindowTextW" _
    			(ByVal hwnd As Long, lpString As Any) As Long
     
     Public ArCapt() As Byte 'Объявление динамического массива

    Все пояснения уже сделаны и поэтому приведем процедуру этого модуля, вызывающую функции API:

    Public Sub WorkWithUniFunc()
    	Dim res As Long
    	Dim Capt As String	'Заголовок
    	'Динамический массив байтов для передачи строки заголовка
    	Dim HandleW As Long 'Описатель окна
    	
    	'Поиск окна по заголовку
    	Capt = "Document1 - Microsoft Word"
    	HandleW = FindWindowA(vbNullString, Capt)
    	If HandleW > 0 Then 'OK
    		Debug.Print HandleW
    	Else: MsgBox ("FindWindowA не может найти окно с заголовком"  vbCrLf  Capt)
    	End If
    	'Попытки использовать для поиска Unicode функцию
    	'FindWindowW не увенчались успехом
    '	ReDim ArCapt(0 To 2 * VBA.Len(Capt)) As Byte
    '	ArCapt = Capt  vbNullChar
    '	Debug.Print ArCapt
    '	HandleW = FindWindowW(0, ArCapt(0))
    '	If HandleW > 0 Then 'OK
    '		Debug.Print HandleW
    '	Else: MsgBox ("Не могу вызвать UniCode FindWindowW")
    '	End If
    	'Получить заголовок окна
    	ArCapt = VBA.String$(128, vbNullChar)
    	res = GetWindowText(HandleW, ArCapt(0), 128)
    	If res > 0 Then 'OK
    		Debug.Print ArCapt
    	Else: MsgBox ("не получен заголовок окна")
    	End If
    		
    	'Изменить заголовок окна
    	Capt = "NewDoc"
    	ArCapt = Capt  vbNullChar
    	res = SetWindowText(HandleW, ArCapt(0))
    		
    	'Повторно получить заголовок окна
    	ArCapt = VBA.String$(128, vbNullChar)
    	res = GetWindowText(HandleW, ArCapt(0), 128)
    	If res > 0 Then 'OK
    		Debug.Print ArCapt
    	Else: MsgBox ("не получен заголовок окна")
    	End If
    End Sub

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

    327894 
    Document1 - Microsoft Word
    
    Document1 - Microsoft Word
    NewDoc

    Дадим краткие комментарии к тексту процедуры:

  • Работа процедуры начинается с вызова функции API FindWindowA, работающей в кодировке ANSI. Она успешно находит окно, заголовок которого задан переменной Capt. Функция возвращает его описатель.
  • На следующем шаге мы пытались решить эту же задачу, используя функцию API FindWindowW, работающую в кодировке Unicode. В тексте нашел отражение один из вариантов решения. К сожалению, поиск во всех случаях заканчивался неуспехом, хотя, как показал побайтный анализ в окне Watch и как показывает отладочная печать, массив байтов ArCapt содержит Unicode копию строки заголовка
  • Полученный описатель окна использовался при вызове Unicode варианта функции GetWindowTextW. Функция корректно работала, используя ссылку на переданный ей массив байтов ArCapt.
  • Затем, используя эту же технику, заголовок окна был изменен при вызове Unicode варианта функции SetWindowTextW.
  • Для контроля повторно была вызвана функция GetWindowTextW. Отладочная печать подтвердила корректность работы.
  • Неудача в вызове функции FindWindowW может быть вызвана разными причинами. Вероятнее всего, при выполнении поиска и проведения операций сравнения строк, применяемый способ не корректен, или, по крайней мере, требует дополнительных уточнений, возможно связанных с длиной строки. С другой стороны, сам прием является некоторой уловкой. Существует более честный способ, хотя, возможно, и более трудоемкий. Для решения задачи можно создать библиотеку типов TypeLib, содержащую описание функций Win32 API в Unicode кодировке, включить ссылку на эту библиотеку и вызывать функции без всяких уловок.

    Обработка ошибок, возникающих при вызове функций Win32 API

    Как мы уже говорили ранее, не бывает программ без ошибок. Если ошибка возникает при выполнении кода процедур и функций VBA, - ошибка периода выполнения (run time error), - то появляется окно сообщения об ошибке. Если ошибка периода выполнения появляется при работе функции Win32 API, то прерывания работы программы не происходит, окно сообщения об ошибке не появляется. Вместо этого функция возвращает значение 0 в качестве результата, свидетельствующее об ошибке периода выполнения. Тем не менее, большинство функций Win32 API сохраняют информацию о возникшей ошибке. Эту информацию можно получить стандартным способом, используя VBA объект Err. Свойство LastDLLErr этого объекта возвращает номер последней ошибки, возникшей в DLL. К сожалению, сам по себе номер мало что говорит. Необходимо знать описание ошибки, соответствующее этому номеру. Частично причину ошибки можно понять по имени константы, которую можно найти в уже неоднократно упоминавшемся файле Win32API.txt, используемом в API Viewer. Опять-таки, к сожалению, возможные значения констант приводятся независимо от функций, в которых они возникают. И, несмотря на то, что все такие константы начинаются со слова ERROR найти константу по ее значению не так то просто. Можно, конечно, воспользоваться возможностью создания базы данных по текстовому файлу и организовать специальный запрос, позволяющий найти имя константы по ее значению. Естественно, что лучше всего иметь полную информацию об используемых функциях Win32 API, включающую, в том числе, и сведения о возможных ошибках периода выполнения данных функций. Эту информацию можно найти, если под рукой есть подходящая литература, например, справочник программиста Win32, или поискать на упоминавшемся сервере Microsoft для разработчиков.

    Естественно, что пример ошибки времени выполнения в процессе работы DLL у нас уже под рукой. Нам и изобретать его не было необходимости. Как Вы помните, в последнем примере мы сетовали на возникновение подобной ошибки в процессе поиска описателя окна по его заголовку при вызове функции FindWindowW, работающей в Unicode кодировке. Давайте вернемся к этому примеру и попробуем обработать эту ошибку. В раздел объявлений ранее созданного модуля Unicode мы добавили объявление констант и функций и теперь он выглядит так:

    Option Explicit
    
    Public Const ERROR_INVALID_NAME = 123
    
    'Объявление вызываемых функций
     Public Declare Function FindWindowA Lib "user32" _
    			(ByVal lpClassName As String, ByVal lpWindowName As String) As Long
    'Функции в Unicode кодировке
    'Тип string заменен на Any. Передача аргумента по ссылке
     Public Declare Function FindWindowW Lib "user32" _
    			(lpClassName As Any, lpWindowName As Any) As Long
     
     Public Declare Function GetWindowText Lib "user32" Alias "GetWindowTextW" _
    			(ByVal hwnd As Long, lpString As Any, ByVal cch As Long) As Long
     
     Public Declare Function SetWindowText Lib "user32" Alias "SetWindowTextW" _
    			(ByVal hwnd As Long, lpString As Any) As Long
     
     Public Declare Function GetActiveWindow Lib "user32" () As Long
    
     Public ArCapt() As Byte 'Объявление динамического массива

    Приведем теперь процедуру, в которой вызывается функция FindWindowW, приводящая к ошибке периода выполнения:

    Public Sub WorkWithApiErr()
    		Dim Res As Long
    		Dim capt As String	'Заголовок
    		Dim HandleW As Long 'Описатель окна
    		
    		'Поиск окна по заголовку
    		capt = "DocOne6 - Microsoft Word"
    		ArCapt = capt  vbNullChar
    		Debug.Print ArCapt
    		HandleW = FindWindowW(0, ArCapt(0))
    		If HandleW > 0 Then 'OK
    			Debug.Print HandleW
    		Else: MsgBox ("Не могу корректно вызвать UniCode FindWindowW")
    			If Err.LastDllError = ERROR_INVALID_NAME Then
    				Debug.Print "Не корректно задано имя при вызове Unicode 
    				FindWindowW функции!"
    			End If
    		End If
    		'Еще один эксперимент: вначале получим заголовок активного окна,
    		'затем найдем окно по заголовку, работая в Unicode кодировке.
    		HandleW = GetActiveWindow()
    		
    			'Получить заголовок окна
    		ArCapt = VBA.String$(128, vbNullChar)
    		
    		Res = GetWindowText(HandleW, ArCapt(0), 128)
    		If Res > 0 Then 'OK
    			Debug.Print ArCapt
    		Else: MsgBox ("не получен заголовок окна")
    		End If
    		
    		ArCapt = VBA.Left(ArCapt, Res)
    		HandleW = FindWindowW(0, ArCapt(0))
    		If HandleW > 0 Then 'OK
    			Debug.Print HandleW
    		Else: MsgBox ("Не могу корректно вызвать UniCode FindWindowW")
    			If Err.LastDllError = ERROR_INVALID_NAME Then
    				Debug.Print "Не корректно задано имя при вызове Unicode 
    				FindWindowW функции!"
    			End If
    		End If
    End Sub

    Приведем результаты печати , появившиеся в окне отладки при выполнении этой процедуры:

    DocOne6 - Microsoft Word 
    Не корректно задано имя при вызове Unicode FindWindowW функции!
    Microsoft Visual Basic - DocOne6 [running] - [Unicode (Code)]	
    Не корректно задано имя при вызове Unicode FindWindowW функции!

    Прокомментируем теперь работу программы и полученные результаты:

  • Вначале мы попытались найти окно с заведомо существующим заголовком, - окно документа, содержащего тестовые примеры. В процессе работы функции Win32 API FindWindowW возникла ошибка периода выполнения, функция вернула нулевой результат. Ошибка была обработана, и как показывает константа ERROR_INVALID_NAME, причиной является ошибка в задании имени (передаваемый формат в виде массива байтов не годится для цели поиска и сравнения строк), о чем свидетельствует отладочная информация.
  • Далее проводится еще один эксперимент на ту же тему. Для активного окна находится заголовок, используя функцию GetWindowTextW, возвращающую строку в виде массива байтов. Тут же этот массив используется для поиска окна по заголовку. Однако ничего не помогает и снова при поиске окна возникает ошибка. Она обрабатывается, о чем выдается соответствующее сообщение.
  • Функции API и вызов Callback функций

    Мы уже говорили о функциях обратного вызова, называемых Callback функциями. Для "многослойного" способа построения программных систем, характерного для программирования, функции внешнего слоя могут вызывать функции ядра без особых проблем. Однако паритета между ядром и внешним слоем нет. Вызов функций внешнего слоя из ядра затруднен. Чтобы как-то решить эту проблему и вводятся функции обратного вызова. Если функции ядра, в ответ на ее вызов из внешнего слоя, в свою очередь необходимо вызвать функцию внешнего слоя, то ядро диктует условия, каким должна удовлетворять вызываемая функция. Есть специальные механизмы, обеспечивающие вызов таких Callback функций, но во всех случаях заголовок вызываемой функции жестко фиксирован и известен ядру. Этот механизм Callback функций применяется и для обеспечения двусторонней связи между функциями VBA и функциями Win32 API, которым в процессе их работы требуется обратный вызов функций VBA.

    Заметьте, в предыдущих версиях VBA не было возможности явным образом работать с функциями Win32 API, требующими вызова Callback функций. Теперь такая возможность появилась, благодаря включению в язык возможности передачи указателя функции в качестве параметра процедур и функций. Явное введение в язык конструкции AddressOf, возвращающей указатель на функцию, дало возможность при вызове функции Win32 API передать ей в качестве аргумента имя Callback функции. Попробуем разобраться в деталях того, как вызываются функции Win32 API, требующие Callback функции для своей работы, как пишутся такие функции на VBA, как передается информация между функциями, - как это все, в конечном итоге, согласуется между собой. Начнем, прежде всего, с ответа на вопрос, а как узнать, что функция Win32 API требует для своей работы вызова Callback функции. Подсказку можно получить от обозревателя, если проанализировать оператор Declare, созданный API Viewer. Когда имя параметра начинается префиксом lp, а заканчивается окончанием Func, это означает, что соответствующий аргумент является ссылкой на имя Callback функции. К сожалению, обозреватель не содержит необходимой информации о том, каким должен быть заголовок функции обратного вызова, так что необходимо обращаться к документации по Win32 API или идти на сервер. Заметьте, документация, как правило, ориентирована на C программистов, поэтому необходимо самому корректно транслировать заголовок к виду, понимаемому VBA. Ошибки в задании типов аргументов, пропуск описателя ByVal могут дорого стоить. Пожалуй, одна из наиболее сложных задач при работе с Callback функцией состоит в том, чтобы найти ее описание, а затем, используя документацию, ориентированную на язык C/C++, корректно описать на VBA заголовок этой функции.

    Еще одна, важная для понимания задача состоит в организации правильного обмена информацией между процедурой VBA, вызываемой ею функцией Win32 API и вызываемой ею Callback функцией. Прежде всего, следует понимать, что программисту никогда не приходится вызывать самому Callback функцию. Ее всегда вызывает соответствующая функция Win32 API. Она же передает ей текущие значения аргументов, необходимые для работы функции обратного вызова. Но, конечно же, в большинстве случаев Callback функция производит изменения в мире объектов VBA программы и, следовательно, она должна быть каким-то образом связана с этим миром. Иногда это делается за счет того, что в функции Win32 API предусмотрен специальный параметр, который вызывающая ее программа передает ей, а она, в свою очередь, передает его функции обратного вызова. Недостаток такого способа состоит в том, что передаваемый параметр один, а информация, связывающая функцию обратного вызова с миром VBA, может быть разнородной. В этих условиях более предпочтительным может быть способ передачи и получения данных в Callback функцию через глобальные переменные. Именно этот способ мы использовали в наших примерах. Прежде, чем перейти к примерам, давайте подведем итоги и еще раз сформулируем основные этапы организации работы при вызове функций Win32 API, требующих Callback функций. Итак, необходимо:

  • Определить, что функция Win32 API требует вызова Callback функции.
  • Найти документацию по этой функции, описывающую требования к заголовку этой функции. Если эта документация ориентирована на язык C/C++, то привести ее к виду, требуемому VBA.
  • Понять, как передать информацию об объектах VBA в Callback функцию.
  • Написать одну или несколько реализаций функций обратного вызова. Обращаем внимание, что функций обратного вызова может быть несколько. Имя функции не является жестко зафиксированным. Оно передается функции Win32 API как аргумент в момент вызова. Поэтому в зависимости от контекста одну и ту же функцию Win32 API можно вызывать с различными Callback функциями.
  • Вызвать функцию Win32 API, передав ей в момент вызова имя Callback функции и другие необходимые аргументы.
  • Функции высших порядков и конструкция AddressOf

    Функцией (процедурой) высших порядков в программировании называют функцию (процедуру), один из формальных параметров которой имеет тип функции или процедуры. Введение в язык функций высших порядков существенно повышает выразительную силу языка программирования. Классическим примером процедуры высшего порядка является процедура вычисления интеграла, одним из параметров которой выступает подынтегральная функция. Типичным примером функций высших порядков являются функции Win32 API, требующие вызова Callback функций. Существует несколько способов введения функций высших порядков в язык программирования, среди которых лучшим, видимо, является способ, основанный на введении функционального типа. В этом случае можно объявлять переменные типа Func или Proc, а затем уже передавать такие переменные, как аргументы при вызове функции высшего порядка. Другой классический способ основан на работе с указателями. Такой типизированный указатель может хранить ссылку на функцию, - содержать ее адрес, и может быть передан в качестве аргумента при вызове функции высшего порядка. Именно этот способ и реализован в VBA. С этой целью в язык введена конструкция:

    AddressOf имя

    Параметр имя может быть именем процедуры, функции или процедуры - свойства ( Property ). В качестве результата возвращается ссылка на объект с указанным именем.

    К сожалению, введя долгожданную конструкцию AddressOf, разработчики остановились на пол пути. С ее помощью можно передать функции высшего порядка в качестве аргумента имя функции. Однако, по-прежнему, нельзя описать на VB или VBA функцию высшего порядка. Такие функции должны быть внешними, как функции Win32 API или функции собственноручно разработанной DLL на языке C/C++. Можно было бы предъявить и еще одно "законное" требование к этой конструкции. Было бы весьма полезно, если бы она позволяла получать ссылку на объект любого произвольного типа, не ограничиваясь только функциональным типом. В этом случае работа с указателями в языке VBA стала бы полноценной.

    В настоящее время на конструкцию AddressOf накладывается целый ряд ограничений:

  • Эта конструкция может быть использована только в выражении, задающем вызов функции высшего порядка. Здесь она используется как аргумент, непосредственно предшествуя имени процедуры, передаваемой в качестве фактического параметра. Заметьте, эту конструкцию нельзя использовать при описании функции или процедуры, из-за чего невозможно определить функцию высшего порядка средствами VBA.
  • Процедуры, функции и свойства, которые вызываются конструкцией AddressOf, должны быть в том же проекте, в котором помещено объявление и вызов функции высшего порядка. Вызываемые процедуры и функции должны быть расположены в стандартном модуле, они не могут находиться в модуле класса или в модуле формы.
  • Эту конструкцию можно использовать только для вызова собственных процедур и функций, - ее нельзя использовать для вызова внешних или стандартных функций.
  • Функции перечисления Win32 API

    Группу функций Win32 API, требующих вызова Callback функций, составляют так называемые функции перечисления, - Enum функции. Эти функции позволяют перечислить в определенном порядке все объекты операционной системы заданной группы. Вызывая на каждом шаге перечисления функцию обратного вызова, функция Win32 API передает ей текущий объект группы в качестве аргумента. Функция обратного вызова уже может производить над этим объектом различные, но, естественно, допустимые действия. Тем самым у программиста появляется возможность работы с коллекцией объектов, возможность задать собственную обработку для каждого из объектов. К функциям перечисления относятся такие функции как: EnumWindows, EnumPrinters, EnumFontFamilies, EnumFonts, EnumPorts, EnumResourseNames и многие другие. В качестве примера, рассмотрим работу с одной из этих функций.

    Функция EnumWindows

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

    Начнем с описания функции EnumWindows в том виде, в каком оно представлено в документации Platform SDK:

    BOOL EnumWindows(
    	WNDENUMPROC lpEnumFunc,	// pointer to callback function
    	LPARAM lParam		// application-defined value
    );

    Функция EnumWindows перечисляет все окна верхнего уровня, передавая текущий описатель окна Callback функции, определенной в приложении. Функция не перечисляет дочерние окна - Child Windows. Функция выполняется, пока перечисление не будет закончено или Callback функция не вернет значение False. Ее параметры:

  • lpEnumFunc - указатель на определенную в приложении Callback функцию.
  • lParam - определенное приложением значение, передаваемое в Callback функцию.
  • Функция возвращает значение 0 в случае неуспеха и ненулевое значение при благоприятном исходе.

    Описание Callback функции EnumWindowsProc, полученное из той же документации, имеет вид:

    BOOL CALLBACK EnumWindowsProc(
    	HWND hwnd,		// handle to parent window
    	LPARAM lParam	// application-defined value
    );

    Функция EnumWindowsProc является Callback функцией, определенной в приложении, используемой при вызове функций Win32 API EnumWindows или EnumDesktopWindows. Она получает при вызове описатель окна верхнего уровня. Тип WNDENUMPROC определяет указатель на эту Callback функцию. Имя EnumWindowsProc является держателем места (placeholder) и должно быть замещено именем функции, определенной в приложении.

    Ее параметры:

  • hwnd - описатель окна верхнего уровня.
  • lParam - определенное приложением значение, данное в EnumWindows или EnumDesktopWindows.
  • Для продолжения перечисления функция возвращает значение True, для окончания - False.

    Как видите, представленные описания функций ориентированы на язык C/C++ и нуждаются в преобразовании для их использования в программах на VB/VBA. Используя обозреватель API Viewer, можно получить оператор Declare для функции EnumWindows. Вот как выглядит заголовок этой функции после соответствующей трансляции:

    Public Declare Function EnumWindows Lib "user32" _
    		(ByVal lpEnumFunc As Long, ByVal lParam As Long) As Long

    Заметьте, появились описатели ByVal, а все типы заменились в данном случае на тип Long. Поскольку API Viewer не помощник в деле преобразования описания Callback функций, то эту работу необходимо проделать самостоятельно. В результате, описание имеет вид:

    Public Function EnumWindowsProc(ByVal HandleW As Long, _
    		ByVal lParam As Long) As Long

    Заметим, формальная трансляция не вызывает затруднений, - описатель Callback следует опустить, имя можно дать произвольное, а трансляция типов в данном случае достаточно проста. Вместе с тем, с описанием типа параметра lParam не все так просто. Ведь он должен служить для передачи произвольной информации, поэтому теоретически допускается задание любого произвольного типа для этого параметра, например, этот параметр может быть объектом. В этом случае следует быть особо внимательным, так первая наша попытка передать функции параметр, отличный от типа Long, привела к критической ошибке и прекращении работы приложения. Как я уже говорил ранее, можно применить альтернативный способ и передавать информацию, пользуясь глобальными переменными.

    Перейдем теперь к описанию реализации нашего примера. В проекте тестового документа был создан модуль с именем " ОбратныйВызов ". Вот текст раздела объявлений этого модуля:

    Option Explicit
    'Операторы Declare вызываемых функций Win32 API
    
    Public Declare Function EnumWindows Lib "user32" _
    	(ByVal lpEnumFunc As Long, ByVal lParam As Long) As Long
    
    Public Declare Function GetWindowText Lib "user32" Alias "GetWindowTextA" _
    	(ByVal hwnd As Long, ByVal lpString As String, ByVal cch As Long) As Long
    
    Public Declare Function GetClassName Lib "user32" Alias "GetClassNameA" _
    	(ByVal hwnd As Long, ByVal lpClassName As String, ByVal nMaxCount As Long) As Long
    
    'Описание глобальных переменных, обеспечивающих связь
    'с функцией обратного вызова EnumWindowsProc
    Public HandleCol As New Collection
    Public CaptCol As New Collection
    Public ClassNameCol As New Collection

    Помимо функции EnumWindows, основной для нашего примера, но о которой уже много говорилось, в разделе приведено объявление новой, ранее не упоминавшейся Win32 API функции GetClassName. Она похожа на функцию GetWindowText и возвращает по описателю окна имя класса этого окна. Функцию GetWindowText мы объявляем повторно, соответствующий оператор Declare есть в разделе объявлений другого модуля проекта, но, заметьте, в этом есть необходимость, поскольку в других модулях эта функция использовалась с различными псевдонимами. Раздел объявлений модуля содержит объявление трех глобальных переменных - трех коллекций, с которыми будет работать Callback функция EnumWindowsProc, формируя на каждом шаге своего вызова очередной элемент каждой из коллекций. Коллекции будут содержать соответственно описатели окон, заголовки окон и имена классов. Заметьте, что все окна имеют имя класса, но не все окна имеют заголовок, так что число элементов в коллекциях будет различным в процессе работы. Приведем теперь текст Callback процедуры EnumWindowsProc:

    Public Function EnumWindowsProc(ByVal HandleW As Long, _
    		ByVal lParam As Long) As Long
     Dim TextW As String
     Dim LenTextW As Long
     Dim Res As Long
     
    		'Добавить описатель в коллекцию
    		HandleCol.Add HandleW
     
    		'Получить заголовок окна.
    		TextW = VBA.String$(255, vbNullChar)
    		LenTextW = VBA.Len(TextW)
    		Res = GetWindowText(HandleW, TextW, LenTextW)
    		If Res > 0 Then
    			'Добавить заголовок в коллекцию
    			TextW = VBA.Left(TextW, Res)
    			CaptCol.Add TextW
    		End If
     
    		'Получить класс окна.
    		TextW = VBA.String$(255, vbNullChar)
    		LenTextW = VBA.Len(TextW)
    		Res = GetClassName(HandleW, TextW, LenTextW)
    		If Res > 0 Then
    			'Добавить имя класса в коллекцию
    			TextW = VBA.Left(TextW, Res)
    			ClassNameCol.Add TextW
    		End If
    	EnumWindowsProc = 1
    End Function

    Напомним, эта процедура вызывается автоматически в процессе работы процедуры EnumWindows. Поскольку процедура всегда возвращает значение 1, означающее успешность ее работы, то число ее вызовов определяется размером перечисляемого множества окон. Обратите внимание и на то, что в процедуре используется только первый параметр - описатель текущего окна, который передается вызываемым Win32 API функциям GetWindowText и GetClassName. Второй параметр вообще не используется, вместо этого напрямую происходит заполнение коллекций, заданных глобальными переменными. Так обеспечивается связь с внешним миром.

    Чтобы закончить пример, нам осталось привести описание процедуры GetCaptions, вызывающей EnumWindows:

    Public Sub GetCaptions()
    		'Вызов Win32 API функции EnumWindows,
    		'вызывающей в свою очередь Callback функцию EnumWindowsProc
    		Dim item As Variant
    		Dim Res As Long
    		
    		Res = EnumWindows(AddressOf EnumWindowsProc, 0)
    		
    		'Обработка глобальных переменных, определенных в
    		'результате совместной работы EnumWindows и EnumWindowsProc
    		Debug.Print "Число окон = ", HandleCol.Count
    		Debug.Print "Описатели окон"
    		Res = 0
    		For Each item In HandleCol
    			Debug.Print item
    			Res = Res + 1
    			If Res > 10 Then Exit For
    		Next item
    		
    		Debug.Print "Число окон с заголовками= ", CaptCol.Count
    		Debug.Print "Заголовки окон"
    		Res = 0
    		For Each item In CaptCol
    			Debug.Print item
    			Res = Res + 1
    			If Res > 10 Then Exit For
    		Next item
    			
    	Debug.Print "Число окон, возвращающих класс = ", ClassNameCol.Count
    		Debug.Print "Имена классов окон"
    		Res = 0
    		For Each item In ClassNameCol
    			Debug.Print item
    			Res = Res + 1
    			If Res > 10 Then Exit For
    		Next item
    End Sub

    Несколько комментариев к этой процедуре:

  • Главное, на что следует обратить внимание, это на операцию " AddressOf " в момент вызова Win32 API функции EnumWindows. В результате ее выполнения создается ссылка на Callback функцию (адрес расположения функции в памяти). Заметьте, на имя передаваемой функции не накладывается ограничений, поэтому в разных вызовах могут быть разные имена, что и позволяет иметь при необходимости несколько Сallback функций.
  • Мы передаем 0 в качестве значения второго параметра. Это своего рода заглушка, поскольку, как говорилось ранее, передача информации производится через глобальные переменные. В следующем примере мы продемонстрируем возможность передачи информации и через этот параметр.
  • После завершения работы процедуры перечисления начинается обработка коллекций, созданных в процессе ее работы. В данном случае обработка проста и сводится к печати числа элементов коллекции и первых десяти элементов каждой из коллекций. Возможно, было бы интересно посмотреть, сколько же объектов - окон существует в момент выполнения обычного приложения. Но полная распечатка всех имен классов и заголовков заняла бы несколько страниц текста. Ведь таких объектов несколько сотен.
  • Давайте познакомимся с начальными элементами коллекций, содержащих описатели, заголовки и имена классов объектов - окон, существующих в момент запуска процедуры GetCaptions:

    Число окон =	254 
    Описатели окон
     3735790 
     131912 
     131888 
     131916 
     65684 
     40370412 
     917748 
     262866 
     852650 
     852668 
     131844 
    Число окон с заголовками =76 
    Заголовки окон
    Continue
    Microsoft Agent
    Microsoft Office Shortcut Bar
    Menu Parent Window
    NetDDE Agent
    Edit
    Microsoft Visual Basic - DocOne6 [running] - [ОбратныйВызов (Code)]
    Ch6 - Microsoft Word
    Edit
    Properties
    Microsoft Office Shortcut Bar
    Число окон, возвращающих класс =254 
    Имена классов окон
    OfficeTooltip
    tooltips_class32
    ComboLBox
    tooltips_class32
    tooltips_class32
    AgentAnimBalloon
    AgentAnim
    tooltips_class32
    tooltips_class32
    tooltips_class32
    tooltips_class32

    Еще один пример работы с функцией EnumWindows

    Наш следующий пример является упрощенным вариантом предыдущего примера. Его целью является демонстрация возможности передать информацию в Callback процедуру EnumWindowsProc через параметр lParam. В нашем примере вместо трех коллекций будет использоваться только одна - коллекция описателей, именно она и будет передана стандартным способом через параметр lParam. Коллекция будет передана в качестве аргумента при вызове процедуры EnumWindows, а та, в свою очередь передаст его функции обратного вызова EnumWindowsProc. Приведем текст модуля, содержащего процедуры нашего примера:

    Option Explicit
    
    Public Declare Function EnumWindows Lib "user32" _
    		(ByVal lpEnumFunc As Long, ByVal lParam As Long) As Long
    
    Public Declare Function EnumWindows1 Lib "user32" Alias "EnumWindows" _
    		(ByVal lpEnumFunc As Long, lParam As Any) As Long
    
    Public HandleCol As New Collection
    Public HandleCol1 As New Collection
    
    Public Function EnumWindowsProc(ByVal HandleW As Long, _
    		ByVal lParam As Long) As Long
    
    	HandleCol.Add HandleW
    	EnumWindowsProc = 1
    End Function
    
    Public Function EnumWindowsProc1(ByVal HandleW As Long, _
    		lParam As Collection) As Long
    
    	lParam.Add HandleW
    	EnumWindowsProc1 = 1
    End Function
    
    Public Sub GetHandles()
    	
    	Dim item As Variant
    	Dim Res As Long
    		
    	Res = EnumWindows(AddressOf EnumWindowsProc, 0)
    
    	Debug.Print "Number of windows - ", HandleCol.Count
    	Debug.Print "Their handles: "
    	Res = 0
    	For Each item In HandleCol
    		Debug.Print item
    		Res = Res + 1
    		If Res > 10 Then Exit For
    	Next item
    	
    End Sub
    
    Public Sub GetHandles1()
    	
    	Dim item As Variant
    	Dim Res As Long
    
    	Res = EnumWindows1(AddressOf EnumWindowsProc1, HandleCol1)
    		
    	Debug.Print "Number of windows - ", HandleCol1.Count
    	Debug.Print "Their handles: "
    	Res = 0
    	For Each item In HandleCol1
    		Debug.Print item
    		Res = Res + 1
    		If Res > 10 Then Exit For
    	Next item
    
    End Sub

    Дадим несколько комментариев:

  • В этом примере параллельно показаны оба способа передачи информации. Имена функций, оканчивающиеся на 1, связаны с передачей информации через параметр lParam.
  • В операторе Declare, описывающем функцию EnumWindows1, тип параметра lParam задан как Any, и в данном случае параметр передается по ссылке, а не по значению.
  • При описании Callback функции EnumWindowsProc1 для этого параметра указан уже конкретный тип Collection.
  • Аргумент HandleCol1 типа Collection передается при вызове EnumWindows в процедуре GetHandles1. Функция обратного вызова EnumWindowsProc1 заполнит эту коллекцию элементами.
  • Приведем результаты ее работы:

    Number of windows -184 
    Their handles: 
     131826 
     131824 
     131854 
     131868 
     36504034 
     2359854 
     65636 
     262764 
     65690 
     65626 
     3539122

    Функции Win32 API для работы с таймером

    Кроме функций перечисления, требующих в процессе своей работы вызов Callback функций, другим известным примером является функция SetTimer, создающая таймер. Во многих приложениях возникает необходимость синхронизировать его работу в соответствии с регулярно поступающими сообщениями от таймера. Общая схема такова: таймер посылает сообщения приложению с заданным интервалом, в ответ приложение выполняет определенную работу, вызывая ту или иную функцию ( Callback функцию). Класс таких диспетчерских приложений, регулярно обрабатывающих вновь поступившие заявки, весьма велик. При работе в приложении Access для этих целей введен специальный элемент управления - Timer. В приложениях Word или Excel такого элемента нет, но всегда можно воспользоваться соответствующими функциями Win32 API, чтобы создать один или несколько собственных таймеров и организовать работу приложения, реагирующего на их сообщения. Заметьте, несмотря на то, что физический таймер один, логических таймеров, посылающих приложению свои сообщения, может быть несколько.

    Функция SetTimer

    Эта функция создает таймер, посылающий сообщения с заданным интервалом. Ее описание, которое можно найти на Platform SDK, имеет вид:

    UINT SetTimer(
    	HWND hWnd,		// handle to window for timer messages
    	UINT nIDEvent,	        // timer identifier
    	UINT uElapse,		// time-out value
    	TIMERPROC lpTimerFunc	// pointer to timer procedure
    );

    Ее параметры:

  • hwnd - Описатель окна, которому будут посылаться сообщения таймера. В VBA программах таймер не связывается с окном и значение этого параметр задается как NULL.
  • nIDEvent - Задает идентификатор таймера. Его значение игнорируется, когда таймер не связан с окном, что имеет место в рассматриваемом нами случае.
  • uElapse - Задает интервал, с которым таймер будет посылать свои сообщения. Интервал задается в миллисекундах, так что значение 1000 соответствует одной секунде.
  • lpTimerFunc - Указатель на Callback функцию, которая будет вызываться всякий раз, когда обрабатывается сообщение WM_Timer, поступающее от таймера.
  • Если функция успешно завершает свою работу и создает таймер, то в качестве результата она возвращает уникальный идентификатор этого таймера, идентифицирующий его. Этот идентификатор запоминается и используется для уничтожения таймера при вызове Win32 API функции KillTimer. В случае неуспеха возвращается значение 0.

    Заметьте, функция SetTimer только создает таймер. В отличие от функций перечисления вызов Callback функции не происходит в ее теле. Вызов осуществляется более сложным путем. Созданный таймер посылает сообщения с заданным интервалом, сообщения, как обычно, поступают в очередь сообщений и в обработчике сообщения WM_Timer автоматически вызывается функция обратного вызова. Поскольку при обработке сообщений очереди могут происходить разные задержки, то вызываемая функция не всегда будет вызываться с заданным интервалом, - возможны задержки.

    Оператор Declare, задающий VBA описание этой функции имеет соответственно вид:

    Public Declare Function SetTimer Lib "user32" (ByVal hwnd As Long, _
    	ByVal nIDEvent As Long, ByVal uElapse As Long, ByVal lpTimerFunc As Long) As Long

    Функция обратного вызова TimerProc

    Функция TimerProc является Callback функцией, определенной приложением и вызываемой при обработке сообщений, поступающих от таймера. Ее определение имеет вид:

    VOID CALLBACK TimerProc(
    	HWND hwnd,	// handle of window for timer messages
    	UINT uMsg,	// WM_TIMER message
    	UINT idEvent,	// timer identifier
    	DWORD dwTime	// current system time
    );

    Ее параметры:

  • hwnd - Описатель окна.
  • uMsg - Указывает WM_Timer сообщение.
  • idEvent - Идентификатор таймера.
  • dwTime - Задает текущее системное время, возвращаемое функцией GetTickCount.
  • Заметьте, имя TimerProc является лишь держателем места. В конкретной ситуации необходимо будет определить одну или несколько Callback функций с подходящими именами. Поскольку вызов каждой из этих функций производится автоматически, то нет необходимости заботиться о корректной передаче аргументов в момент вызова. Необходимо лишь позаботиться о корректной трансляции приведенного определения, взятого из справочной системы Platform SDK, к виду, воспринимаемому в программах на VBA. Вот как выглядит возможное определение:

    Public Sub TimerProc(ByVal HandleW As Long, ByVal msg As Long, _
    	ByVal idEvent As Long, ByVal TimeSys As Long)

    Обратите внимание, мы транслировали функцию в процедуру, поскольку Callback функция TimerProc не возвращает значения. Все типы данных преобразованы в тип Long, в том числе UINT и DWORD. В данной ситуации нет причин для беспокойства о возможной некорректности передаваемых значений, поскольку их передачу обеспечивает сама система.

    Функция KillTimer

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

    BOOL KillTimer(
    	HWND hWnd,	// handle to window that installed timer
    	UINT nIDEvent,	// timer identifier
    );

    Ее параметры:

  • hwnd - Описатель окна, ассоциированного с таймером, совпадающий по значению с соответствующим параметром функции SetTimer. Напомним, в VBA программах таймер не связывается с окном и значение этого параметр задается как NULL.
  • nIDEvent - Задает идентификатор таймера, который должен быть удален. В нашем случае, когда первый параметр равен NULL, его значение задается идентификатором, возвращенным в качестве результата по окончании работы функции SetTimer.
  • Если функция успешно завершает свою работу и удаляет таймер, то в качестве результата она возвращает ненулевое значение. В случае неуспеха возвращается значение 0.

    Пример создания, работы и удаления таймера

    В свое время в книге по языку Visual C++ , демонстрируя работу с таймером и соответствующими функциями Win32 API, мы разработали проект " Жизнь ". В этом проекте моделируется известная компьютерная игра, где можно задать начальную конфигурацию "жизни". Затем эта конфигурация начинает жить, изменяя свое состояние по заданным правилам. Изменение состояния происходит в качестве ответной реакции на сообщения таймера. Другим подобным примером, по существу вариацией на эту же тему, является создание экранных заставок. Сейчас мы решили обойтись более простым примером, демонстрирующим суть проблемы, но не имеющим эффектной формы. В нашем тестовом примере есть две командные кнопки Start и Finish. В ответ на нажатие первой кнопки создается таймер, соответствующая ему Callback функция ведет подсчет числа ее вызов и уведомляет об этом, печатая значение счетчика в окне отладки. При нажатии кнопки Finish таймер удаляется. Кнопки можно нажимать многократно. Все процедуры обработки помещены в модуль Таймер. Вот его текст:

    'Функции работы с таймером
    Public Declare Function SetTimer Lib "user32" (ByVal hwnd As Long, _
    		ByVal nIDEvent As Long, ByVal uElapse As Long, ByVal lpTimerFunc As Long) As Long
    
    Public Declare Function KillTimer Lib "user32" (ByVal hwnd As Long, ByVal nIDEvent As Long) As Long
    
    'Глобальная информация
    Public Counter As Long	'Счетчик числа вызовов Callback функции
    Public IdEv As Long
    
    Public Sub HowManyProc(ByVal HandleW As Long, ByVal msg As Long, _
    			ByVal idEvent As Long, ByVal TimeSys As Long)
    		'Функция обратного вызова. Вызывается при обработке сообщения WM_Timer,
    		'посылаемого таймером, созданным процедурой SetTimer
    		
    		Counter = Counter + 1
    		Debug.Print "Hi", Counter
    		
    	End Sub
    
    Public Sub Start()
    		
    		'Создает таймер, вызывая Win32 Api функцию SetTimer
    		Counter = 0
    		IdEv = SetTimer(0, 0, 10000, AddressOf HowManyProc)
    		If IdEv = 0 Then
    			MsgBox ("Не удалось создать таймер!")
    		Else
    			Debug.Print "Создан Таймер: Идентификатор = ", IdEv
    		End If
     
    End Sub
    
    Public Sub Finish()
    		'Удаляет таймер
    		If IdEv > 0 Then
    			Call KillTimer(0, IdEv)
    			Debug.Print "Удален Таймер: Идентификатор = ", IdEv
    			IdEv = 0
    		End If
    		
    End Sub

    Комментариев, приведенных в тексте, полагаем достаточно для понимания всех деталей. Приведем еще результаты печати, периодически появляющиеся в окне отладки. Следует только сказать, что дважды были поочередно нажаты кнопки Start и Finish:

    Создан Таймер: Идентификатор =32578 
    Hi					1 
    Hi					2 
    Hi					3 
    Удален Таймер: Идентификатор =32578 
    Создан Таймер: Идентификатор =32573 
    Hi					1 
    Hi					2 
    Hi					3 
    Hi					4 
    Удален Таймер: Идентификатор =32573

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

    Классы как обертка вызовов функций Win32 API

    В предыдущих лекциях мы много говорили о классах и, в частности, рассматривали возможность применения класса в качестве упаковки тех или иных стандартных средств. Такой прием позволяет инкапсулировать в классе стандартные средства, - функции API, элементы управления, - скрыть многие детали работы с ними, а, зачастую, и предоставить конечному пользователю дополнительный сервис. Этот полезный прием целесообразно применять всякий раз, когда предполагается работа с функциями Win32 API. Все эти вопросы объявления функций, трансляции из одной формы описания в другую, описание многих параметров, не используемых по существу задачи, следует скрыть в описании класса, организовав интерфейс класса, более разумный с позиций конечного пользователя.

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

    Построение класса "ВашТаймер"

    Поскольку мы уже хорошо знакомы с тем, как строятся классы и как работать с функциями Win32 API, то нам осталось рассказать, как объединить эти две вещи в единое целое. Никаких особых деталей здесь нет. Общая стратегия такова:

  • Необходимо спроектировать интерфейс класса, ориентированный на конечного пользователя. Открытые свойства и методы класса должны позволять пользователю решать все задачи, которые можно решать с помощью скрытых в классе стандартных средств.
  • Встроить в класс стандартные средства, сделав их скрытыми, недоступными для внешнего использования.
  • Реализовать интерфейс класса, используя встроенные средства, возможно расширив их возможности.
  • Применим эту общую схему для создания класса ВашТаймер. Начнем с проектирования его интерфейса. Естественно, целью класса является предоставление пользователю возможности создавать таймер, посылающий сообщения с заданным интервалом, и удалять его, когда необходимость в нем исчезнет. Пользователь не должен ничего знать о функциях Win32 API, об операторах Declare, преобразовании типов. Кажется естественным с этих позиций в интерфейс класса включить два метода: " СоздатьТаймер " и " УдалитьТаймер " и свойство " ИнтервалТаймера ", доступное для чтения и записи. Методы не имеют параметров, что облегчает работу с ними. Чтобы созданный таймер посылал сообщения с заданным интервалом, необходимо предварительно установить подходящее значение свойства, но можно этого и не делать, - в этом случае будет использоваться значение по умолчанию.

    Рассматриваемый нами случай упаковки функций Win32 API особый, поскольку одна из этих функций требует вызова Callback функции. Заметьте, функции обратного вызова не должны принадлежать упаковке, - нашему классу. Они не являются стандартными средствами, это функции, создаваемые пользователем. Поэтому они должны находиться вне модуля класса, - в стандартном классе, созданном пользователем, там, где он будет создавать и объекты класса ВашТаймер. Чтобы не возникала соблазна поместить в класс функцию обратного вызова, такая возможность исключается синтаксически. Обратите, однако, внимание, что созданный класс предъявляет определенные требования к заголовку функции обратного вызова и даже диктует ее имя. Но обо всем по порядку и давайте вначале рассмотрим описание класса ВашТаймер:

    Option Explicit
    'Класс ВашТаймер служит упаковкой функций WIN32 API работы с таймером
    'Интерфейс класса будут составлять две функции:
    'СоздатьТаймер, УдалитьТаймер и свойство ИнтервалТаймера
    
    'При работе с классом необходимо описать Callback функцию по следующему образцу:
    
    'Public Sub TimerProc(ByVal HandleW As Long, ByVal msg As Long, _
    '			ByVal idEvent As Long, ByVal TimeSys As Long)
    '		'Функция обратного вызова. Вызывается при обработке сообщения WM_Timer,
    '		'посылаемого таймером, созданным процедурой SetTimer
    '
    '	'Поместите здесь свой код!
    '
    'End Sub
    
    'Функции Win32 API для работы с таймером
    Private Declare Function SetTimer Lib "user32" (ByVal hwnd As Long, _
    		ByVal nIDEvent As Long, ByVal uElapse As Long, ByVal lpTimerFunc As Long) As Long
    
    Private Declare Function KillTimer Lib "user32" (ByVal hwnd As Long, ByVal nIDEvent As Long) As Long
    
    'Свойства: Интервал - хранит значение интервала посылки сообщений
    Private Интервал As Long
    'Идентификатор таймера
    Private IdEv As Long
    
    
    Public Sub СоздатьТаймер()
     'Создает таймер, вызывая Win32 Api функцию SetTimer
    		IdEv = SetTimer(0, 0, Интервал, AddressOf TimerProc)
    		If IdEv = 0 Then
    			MsgBox ("Не удалось создать таймер!")
    		Else
    			Debug.Print "Создан Таймер: Идентификатор = ", IdEv
    		End If
     
    End Sub
    
    Public Sub УдалитьТаймер()
    'Удаляет таймер
    		If IdEv > 0 Then
    			Call KillTimer(0, IdEv)
    			Debug.Print "Удален Таймер: Идентификатор = ", IdEv
    			IdEv = 0
    		End If
    End Sub
    
    Public Property Get ИнтервалТаймера() As Long
    		ИнтервалТаймера = Интервал
    End Property
    
    Public Property Let ИнтервалТаймера(ByVal NewValue As Long)
    		Интервал = NewValue
    End Property
    
    Private Sub Class_Initialize()
    		Интервал = 1000
    End Sub
    
    Private Sub Class_Terminate()
    		УдалитьТаймер
    End Sub

    Некоторые комментарии к этому тексту:

  • О проектировании интерфейса класса мы уже говорили. Его составляют два метода, не имеющие параметров, - СоздатьТаймер и УдалитьТаймер, а также процедуры - свойства Property Get и Property Let ИнтервалТаймера, позволяющие взаимодействовать с закрытым свойством Интервал.
  • Закрытых свойств и методов больше. Закрытыми являются операторы Declare, описывающие функции Win32 API SetTimer и KillTimer, уже упомянутое свойство Интервал и свойство IdEv, хранящее идентификатор таймера, о котором конечный пользователь может и не знать.
  • Закрытыми являются конструктор класса по умолчанию и деструктор: Class_Initialize и Class_Terminate. В конструкторе инициализируется свойство Таймер, значение которого устанавливается по умолчанию, равным одной секунде. В деструкторе класса таймер уничтожается, если он не был удален до этого.
  • При создании таймера устанавливается ссылка на функцию обратного вызова с именем TimerProc. Процедура с таким именем и уже упоминавшимися требованиями к ее заголовку должна быть описана в стандартном модуле класса, созданным конечным пользователем.
  • В описание класса в качестве комментария вставлена заготовка функции обратного вызова, чтобы облегчить создание этой функции конечному пользователю.
  • Использование класса ВашТаймер

    Рассмотрим, как работать с классом ВашТаймер. Мы не стали изобретать ничего нового, - в тестовый документ добавили две кнопки Start1 и Finish1, которые работают также как и их тезки Start и Finish, но вызывают для этого методы и свойства объекта MyTimer класса ВашТаймер. В модуле, где объявлен соответствующий объект, находится и процедура обратного вызова TimerProc. Вот соответствующий текст этого модуля:

    Option Explicit
    'Модуль Таймер1
    'Глобальная информация
    Public Counter As Long	'Счетчик числа вызовов Callback функции
    Public MyTimer As New ВашТаймер
    
    Public Sub Start1()
    		MyTimer.ИнтервалТаймера = 5000
    		MyTimer.СоздатьТаймер
    End Sub
    
    Public Sub Finish1()
    		MyTimer.УдалитьТаймер
    End Sub
    
    Public Sub TimerProc(ByVal HandleW As Long, ByVal msg As Long, _
    			ByVal idEvent As Long, ByVal TimeSys As Long)
    		'Функция обратного вызова. Вызывается при обработке сообщения WM_Timer,
    		'посылаемого таймером, созданным процедурой SetTimer
    		
    		Counter = Counter + 1
    		Debug.Print "Hi", Counter
    
    End Sub

    Комментируя этот текст, следует заметить, что введение обертывающего класса облегчает работу с таймером. Единственной проблемой остается достаточно сложное и возможно непонятное конечному пользователю описание заголовка Callback функции TimerProc. Чтобы облегчить ее решение, можно, как это сделано в нашем примере, заготовку этой функции включить в описание класса в качестве комментария.

    В заключение приведем результаты эксперимента с нажатием кнопок Start1 и Finish1:

    Создан Таймер: Идентификатор =31711 
    Hi					1 
    Hi					2 
    Hi					3 
    Удален Таймер: Идентификатор =31711 
    Создан Таймер: Идентификатор =31704 
    Hi					4 
    Hi					5 
    Hi					6 
    Hi					7 
    Удален Таймер: Идентификатор =31704

    На этом мы закончим рассмотрение темы работы с функциями Win32 API.

    Страницы:

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

    Несколько слов об API, Win32, DLL

    API (Application Programming Interface - интерфейс прикладных программ) - это множество функций, организованных, обычно, в виде DLL. Функции API позволяют организовать интерфейс между прикладной программой и средой, в которой работает эта программа. Вызов функций API позволяет программе получать доступ к ресурсам среды и управлять ее работой. Как правило, API задает стандарт взаимодействия среды и прикладной программы.

    Win32 - это название интерфейса, ориентированного на 32-х разрядные приложения и реализованного на таких известных платформах как Windows 95, Windows 98, Windows NT, Windows CE. Функции, составляющие этот интерфейс, позволяют прикладной программе получать доступ к ресурсам операционной системы и управлять ее работой. Более ранние версии Windows используют интерфейс, известный как Win16. Конечно, не все функции, составляющие интерфейс Win32, реализованы в полной мере на всех платформах, так что вызов одной и той же функции под NT приведет к определенному результату, а под Windows 95 работает как вызов заглушки. Любое из приложений, работающее в среде Windows, прямо или косвенно вызывает функции, входящие в Win32 API.

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

  • Kernel32.dll. Эта библиотека предназначена для работы с объектами ядра операционной системы и ее функции позволяют управлять памятью и другими системными ресурсами.
  • User32.dll. Здесь сосредоточены функции для управления окнами - основным видом объектов операционной системы. Обработка сообщений, работа с меню, таймерами, все это выполняют функции этой DLL.
  • GDI32.dll. Эта библиотека, обеспечивающая графический интерфейс операционной системы (Graphics Device Interface). Функции управления выводом на экран дисплея, управления выводом принтера, функции для работы со шрифтами - все они входят в состав этой библиотеки.
  • Заметьте, Win API функции находятся не только в этих библиотеках. С другой стороны API функции не обязательно входят в состав Win32 интерфейса. Например, MAPI интерфейс (Messaging Application Programming Interface) составляют функции, предназначенные для обработки сообщений электронной почты, TAPI (Telephone API) - функции работы с телефонными сообщениями. MAPI, TAPI, также как и Win32 это некоторый набор функций, задающий определенный стандарт взаимодействия

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

    .VBA и Win32 API

    Работая на VBA, неявно всегда приходится иметь дело с функциями Win32 API, только вызов их упрятан в вызываемых VBA функциях или методах объектов Office 2000. Так, например, при работе с объектом Shape так или иначе будут вызываться функции GDI32, обеспечивающие работу с графикой, при работе c функциями VBA.Interaction, например GetSettings, SaveSettings и другими, будет вызываться соответствующие функции работы с реестром Windows, хранящиеся в библиотеках User32 и advapi32. Такой косвенный вызов имеет свои преимущества, обеспечивая определенную безопасность в работе VBA программ. Но в ряде случаев VBA программисту необходим доступ ко всем возможностям операционной системы, предоставляемым Win32 API интерфейсом. Естественно, в этом случае он понимает, что на него ложится большая ответственность в обеспечении корректного вызова функций, поскольку ошибки в вызове могут привести к непредвиденным отказам в работе программы.

    Вызов функций и оператор Declare

    Элементы ActiveX, COM объекты могут экспонировать свой интерфейс, - свои свойства и методы. Это означает, что они уведомляют, предоставляют информацию клиентам о своем интерфейсе. Технически это обеспечивается тем, что эти объекты, наряду с DLL, сопровождаются TypeLib - библиотекой типов, в которой содержится в требуемом виде информация об интерфейсе объекта. В этом случае, для того чтобы начать работу с объектом, достаточно подключить ссылку на эту библиотеку в меню Tools|References в среде редактора VBE. Эта возможность не раз обсуждалась, когда речь шла о вызове, например, приложения Excel в документах Word. Напомним, что приложения Office 2000 представляют собой ActiveX объекты, построенные на основе COM технологии. Они явно экспонируют свой интерфейс, именно поэтому нет проблем при работе с такими приложениями, вызовами свойств и методов их многочисленных объектов. Библиотеки, составляющие Win32 интерфейс, не сопровождаются библиотеками типов TypeLib. Поэтому необходимо самому программисту уведомить VBA о том, где найти и как следует вызывать ту или иную функцию Win32 API Вызову каждой функции должен предшествовать оператор Declare, описывающий эту функцию. Этот оператор и сама схема вызова библиотечных функций используется при работе с любыми DLL, а не только с теми, которые содержат Win32 API функции. В общем случае в DLL могут храниться как функции, так и процедуры. Два варианта вызова этого оператора соответствуют ссылке на процедуру и на функцию, возвращающую значение. Первый вариант:

    [Public | Private] Declare Sub имя Lib "имя-библиотеки" [Alias "псевдоним"] [([параметры])]

    Во втором случае его синтаксис:

    [Public | Private] Declare Function имя Lib "имя-библиотеки" [Alias "псевдоним"] 
    [([параметры])] [As возвращаемый-тип]

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

  • Ключевое слово Public используется, чтобы сделать объявляемую процедуру доступной всем модулям проекта; ключевое слово Private ограничивает доступ к объявленной процедуре лишь модулем, в котором она объявлена. Заметьте, в стандартном модуле можно использовать оба описателя, но в модуле класса разрешается использовать только описатель Private.
  • Ключевое слово Sub в первом случае означает, что речь идет о процедуре; альтернативный ключ Function во втором случае указывает на функцию, возвращающую значение, которое может быть использовано в выражениях.
  • Обязательный параметр имя является именем объявляемой процедуры или функции. Это имя используется при вызовах в VBA программах. Оно может совпадать или отличаться от того имени, под которым процедура (функция) хранится в DLL. Заметьте, для имен функций Win32 API в отличие от Win 16 имеет значение различие между прописными и строчными буквами.
  • После обязательного ключевого слова Lib должно следовать в кавычках имя-библиотеки, содержащей объявляемую процедуру.
  • Ключ Alias позволяет указать, что процедура имеет в DLL другое имя - " псевдоним ", благодаря чему можно избежать коллизии имен в программе. Если первый символ параметра " псевдоним " не является признаком числа ( # ), псевдоним понимается как имя входной точки DLL для данной процедуры; если же первый символ псевдонима - ( # ), следующие за ним число должно задавать порядковый номер входной точки процедуры в DLL. Дело в том, что процедура может иметь несколько точек входа.
  • Необязательный список параметры задает список параметров, передаваемых процедуре при вызове.
  • As возвращаемый-тип во втором варианте оператора задает тип значения, возвращаемого функцией; им может быть любой из базисных типов VBA (не допускаются только строки фиксированной длины), тип объекта или определенный пользователем тип.
  • Список параметры - это список разделенных запятыми параметров процедуры; каждый элемент этого списка имеет вид:
    [Optional] [ByVal | ByRef] [ParamArray] переменная[()] [As тип]
  • Здесь ключ Optional означает, что данный параметр необязателен; при этом все следующие в списке параметры должны быть необязательными и сопровождаться этим же ключом Optional. Этот ключ нельзя применять, если среди параметров есть массив параметров ParamArray.
  • Ключевые слова ByVal и ByRef указывают на то, что параметр передается по значению или по ссылке; по умолчанию в VBA предполагается передача значения по ссылке ( ByRef ).
  • Ключевое слово ParamArray позволяет задать массив элементов типа Variant ; этот параметр должен быть последним в списке и не должен перед собой иметь ключей ByVal, ByRef или Optional ; такой массив позволяет передавать в процедуру произвольное (заранее неизвестное) число параметров.
  • Переменная - произвольное допустимое в VBA имя переменной; пустые скобки () после имени переменной означают, что соответствующий параметр - массив.
  • Необязательное определение As тип задает тип параметра, значения которого могут быть такими же, как и у описанного выше определения возвращаемый-тип. Задав произвольный тип ( As Any ), Вы исключите проверку при передаче параметра процедуре.
  • Вот пример задания оператора Declare для двух функций Win32 API:

    Private Declare Function CreateRectRgn Lib "gdi32" Alias "CreateRectRgn" _
    (ByVal X1 As Long, ByVal Y1 As Long, ByVal X2 As Long, ByVal Y2 As Long) As Long
    Private Declare Function GetTempPath Lib "kernel32" _
    		Alias "GetTempPathA" (ByVal nBufferLength As Long, _
    		ByVal lpBuffer As String) As Long

    Синтаксически оператор Declare прост и понятен, нужно указать библиотеку, имя функции, под которым она будет вызываться в VBA программе, ее имя (псевдоним) под которым она записана в библиотеке и параметры функции в привычном синтаксисе. Однако реальная жизнь не так проста и в организации вызова функций API есть много подводных камней. Дело в том, что DLL служат средством межязыкового взаимодействия. Сама DLL может быть разработана на одном языке, а вызываться в другом. Тогда возникает проблема правильной передачи параметров, поскольку может не быть точного соответствия между типами данных двух используемых языков. Функции Win32 API разработаны в ориентации на синтаксис языка C и C++. Поэтому при записи оператора Declare требуется корректно указать типы параметров, так чтобы они соответствовали типам, используемым в языке C. Еще одна проблема состоит в том, что помимо оператора Declare, в ряде случаев необходимо предварительно описать требуемые типы данных и константы, необходимые в процессе вызова функции. Так что, прежде чем вызвать функцию из DLL, необходимо корректно задать оператор Declare, описать необходимые типы и константы, - все это может быть не столь простой задачей.

    Две кодировки ANSI и Unicode

    Win32 API функции могут существовать в двух вариантах, ориентированных на две кодировки - ANSI, когда символы строк кодируются одним байтом, и Unicode, предполагающей двухбайтную кодировку. По этой причине в DLL возможны три варианта для имен функций:

  • Имя. В этом случае кодировка не играет значения.
  • ИмяA. Окончание A означает, что функция использует ANSI кодировку.
  • ИмяW. Окончание W (от Wide) свидетельствует об Unicode кодировке.
  • Чтобы выбрать функцию в нужной кодировке, нужно правильно задать Alias имя функции. Заметьте, что в VBA вызываются функции Win32 в ANSI кодировке, поэтому следует выбирать имена с окончанием A в тех случаях, когда при вызове функции используются строки и кодировка имеет значение.

    API Viewer

    Мы уже говорили об одной из основных проблем, возникающих при вызове Win32 API функций, - необходимо обеспечить корректное задание оператора Declare, описание типов данных и констант, для чего нужно уметь корректно транслировать описание этих объектов из языка C в язык VBA. Чтобы облегчить решение этой задачи, можно воспользоваться специальным инструментальным средством, называемым API Viewer. Это средство доступно в поставке Office 2000 Developer и в Microsoft Visual Basic. Этот обозреватель позволяет:

  • Загрузить прилагаемый текстовый файл Win32API.txt, содержащий информацию обо всех используемых при вызовах Win32 API константах, типах, функциях и операторах Declare.
  • Преобразовать этот файл в базу данных Access - Win32API.mdb файл, что позволяет ускорить работу, работать с формами и запросами к базе данных.
  • Получить корректную для VB и VBA форму записи операторов Declare.
  • Получить корректную для VB и VBA форму записи типов данных.
  • Получить корректную для VB и VBA форму записи констант.
  • Взгляните на начальный этап работы с обозревателем, где выбирается текстовый файл для просмотра. Обратите внимание, что обозреватель позволяет работать с текстовыми файлами, содержащими информацию о различных интерфейсах, в частности Win32 и Mapi интерфейсах:

    (рис 6.1) Выбор файла Win32API для просмотра в API Viewer

    На следующем рисунке можно увидеть результаты нашей дальнейшей работы с API Viewer. Вначале из верхнего выпадающего списка API Types мы выбрали элемент Declares, в окне Available Items из списка поочередно выбрали интересующие нас функции Win32 API, - соответствующая этим функциям корректная форма записи оператора Declare появилась в окне Selected Items. На следующем шаге из верхнего списка был выбран элемент Types, из списка типов был выбран тип, в нашем примере MenuItemInfo, и корректное для VBA его определение появилось в окне Selected Items. Аналогично, можно получить корректное определение констант. Обычная технология Copy - Paste позволяет перенести информацию непосредственно в программу VBA. Взгляните, как выглядят окна обозревателя на этом этапе работы:

    (рис 6.2) Получение корректной формы записи оператора Declare в окне API Viewer

    Хотя, несомненно, обозреватель полезен, у него есть один недостаток. Было бы полезнее, если бы выбрав имя функции из списка, вся необходимая для вызова этой функции информация, - оператор

    Для того чтобы получить справку по функции Win32 API, следует воспользоваться режимом Search. В окне поиска следует набрать имя нужной функции, предварительно задав требуемую область поиска. При задании области поиска необходимо, по крайней мере, включить флажки Platform SDK Documentation и Others SDK Documentation. При расширении этой области можно получить не только описание функций, но и статьи с примерами их использования. Заметьте, на этом сервере Вы всегда найдете много полезной информации.

    Два языка: C и VB. Различия при вызове функций

    Итак, чтобы корректно вызывать Win32 API функции на VBA, следует разбираться, чем отличаются вызовы функций в языках C и Visual Basic (VBA). Даже, если используется обозреватель API Viewer, который автоматически транслирует типы языка C в типы языка VBA, знание этих различий необходимо во многих ситуациях.

    Соответствие между простыми типами данных

    Нижеследующая таблица описывает соответствие между некоторыми, простыми типами данных двух языков:

    Соответствие между типами языков C и VBA
    C/C++ тип данных Венгерская нотация Описание Тип языка VBA
    BOOL b 8-и битное булево значение. Значение 0 эквивалентно False, ненулевое значение - True Boolean или Long
    BYTE ch 8-и битное целое без знака Byte
    HANDLE h 32 -х битное целое без знака, задающее описатель Windows -объектов Long
    int n 2-х байтное целое со знаком Integer
    UINT u 2-х байтное целое без знака Long
    DWORD dw 4-х байтное целое без знака Long
    long l 4-х байтное целое со знаком Long
    LP lp 32-х битный указатель на C/C++ структуры, строки, функции или другие данные в памяти Long
    LPZSTR lpsz 32-х битный указатель на C строку, завершаемую нулем Long

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

    Структуры языка C и тип, определенный пользователем, в языке VBA

    В языке C можно определять записи - совокупность данных разного типа. Такие записи в языке C называются структурами. Обычно, вначале определяется соответствующий структурный тип, а затем имя этого типа используется при объявлении конкретных переменных. Структуры могут передаваться в качестве аргументов при вызове функций, в том числе при вызове Win32 API функций. В VBA, как известно, для задания подобного структурного типа используется конструкция Type…End Type, называемая типом, определенным пользователем. Переменные такого типа передаются вызываемой функции обычным способом: X As T, где T - имя пользовательского типа. Позже мы приведем пример вызова функции Win32 API, которой передается структура в качестве аргумента.

    Об описателях языка C и объектах Windows

    В языке C особенно при работе с объектами Windows, широко используется тип Handle - задающий описатели объектов. Когда создаются объекты ядра операционной системы и такие основные объекты, как окна, - все они снабжаются описателем, имеющим тип Handle. Описатель представляет длинное целое и однозначно идентифицирует объект. Всегда, когда в функцию Win32 API необходимо передать такой объект, то реально передается его описатель. При трансляции описателей в тип языка VBA ему ставится в соответствие тип Long. При этом желательно понимать, что речь все-таки идет об описателях, создаваемых операционной системой, в момент создания соответствующего объекта. Соответствующие примеры будут даны чуть позже.

    Void функции языка C

    В языке С формально нет процедур, есть только функции. В тех случаях, когда по существу речь идет о процедурах, вычисляющих не один скалярный результат, а имеющих несколько выходных параметров, как правило, используется функция, результат которой говорит об успешности выполнения процедуры. Если результат функции имеет значение True, то все выходные параметры благополучно вычислены, в противном случае следует проанализировать причину неуспеха. Типично для языка C то, что вызов функций является условием оператора If, - оператор If задает упаковку вызова функции, позволяя не только вызвать функцию, но и проверить корректность завершения ее работы. При трансляции заголовков таких функций в язык VBA, они естественно транслируются в функции VBA и для работы с ними можно сохранить стиль языка C.

    Однако в языке C используются и функции, не возвращающие результата. Результат таких функций задается описателем Void, по существу, они являются процедурами. И при трансляции их в VBA их и следует задавать в виде процедур.

    Вызов аргументов по ссылке ByRef и по значению ByVal

    В языке C++ основным способом передачи параметров является передача их по значению, в VBA - по ссылке. Конечно, в обоих языках применяются оба способа. Тем не менее, нужно понимать, что описатель ByVal очень часто будет встречаться при вызове Win32 API функций, значительно чаще, чем при вызове обычных VBA функций. Если используется API Viewer, то этот описатель автоматически будет появляться для тех параметров, где необходима подобная форма вызова. Следует обратить внимание на одно обстоятельство. Несмотря на то, что такой описатель может появляться и для строковых аргументов, строки всегда передаются в функции Win32 API по ссылке. Последнее обстоятельство также связано со спецификой работы со строками в функциях языка C, на которых следует остановиться подробнее.

    Строковые аргументы при вызове функций Win32 API

    Как мы уже говорили, строки передаются по ссылке, даже если у параметра указан описатель ByVal. Не возникает никаких проблем при передаче строки в функцию Win32 API. Передаваемый аргумент может быть произвольным строковым выражением, в том числе переменной типа String или строковой константой. Сложнее дело обстоит, если функция должна вернуть строку, в качестве результата. Прежде всего, напомним, что в функциях Win32 API тип возвращаемого значения никогда не является строкой, - это всегда целочисленное значение, чаще всего булево значение, указывающее на то, удачно ли завершилось выполнение функции. Поэтому, если нужно получить строку в качестве результата, то функции передается два параметра - строка и ее длина, описанные чаще всего следующим образом:

    ByVal lpResultStr As String, ByVal LenResultString As Long

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

  • она должна быть строкой, завершаемой нулем. Чаще всего она вся состоит из нулевых символов.
  • Ее длина должна быть достаточной для того, чтобы вместить результирующую строку.
  • Параметр LenResultString должен указывать максимально допустимое число символов результирующей строки.
  • С учетом этих требований перед вызовом функций, возвращающих строковые значения, как правило, передаваемая функции строка инициализируется и набивается подходящим количеством нулевых символов. Напомним, нулевой символ задается константой vbNullChar. Обычно, это делается следующим образом:

    Const MaxSize = 255 As Long
    lpResultString = String$(MaxSize, vbNullChar)
    LenResultString = Len(lpResultString)

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

    Тип Any

    Иногда, параметр функции API может принимать значения различных типов. В этом случае в операторе Declare нельзя указать один конкретный тип этого параметра. Для решения подобной проблемы и был введен специальный тип Any. Если параметр имеет этот тип, то в период трансляции не проверяется правильность соответствия типов и аргумент может принимать значения любых типов. Конечно, нужно иметь в виду, что функция может принимать значения хоть и нескольких, но совершенно определенных типов. Поэтому при работе с типом Any на программиста возлагается вся ответственность за корректную передачу значения функции API. Альтернативой применения типа Any является задание для одной функции нескольких операторов Declare, - по одному на каждый возможный тип параметра. Тогда в каждой конкретной ситуации будет вызываться соответствующая функция с нужным типом параметра.

    Примеры работы с Win32 API функциями

    Разговор об особенностях вызова Win32 API функций еще не закончен. Тем не менее, пора прервать общее изложение и обратиться к примерам. Многое из того, что было сказано, целесообразно проиллюстрировать примерами работы с конкретными функциями Win 32 API. Каждый из наших примеров будет посвящен работе с некоторой группой функций.

    Работа с окнами

    Как мы уже говорили, окна - это один из основных объектов операционной системы. Функции для работы с ними находятся, в основном, в библиотеке User32. Из большого множества функций мы отобрали несколько функций, позволяющих продемонстрировать, как можно получать описатели окон, как, зная описатель, можно получать характеристики окон и как можно изменять характеристики окон. Начнем с приведения программного текста, а уж потом подробно прокомментируем его. Заметим, что для получения корректного описания операторов Declare, используемых типов данных и констант мы использовали API Viewer и описание функций, которое можно найти на уже упоминавшемся сервере Microsoft.

    В проекте нашего тестового документа был создан модуль "Окна" и в разделе его объявлений помещен следующий текст:

    Option Explicit
    'Константы
    Public Const SW_HIDE = 0
    Public Const SW_SHOWNORMAL = 1
    Public Const SW_SHOWMINIMIZED = 2
    Public Const SW_SHOWMAXIMIZED = 3
    
    'Типы
    Public Type RECT
    			Left As Long
    			Top As Long
    			Right As Long
    			Bottom As Long
    End Type
    
    'Функции
    Public Declare Function GetActiveWindow Lib "user32" () As Long
    
    Public Declare Function GetWindowRect Lib "user32" (ByVal hwnd As Long, _
    			lpRect As RECT) As Long
    
    Public Declare Function GetWindowText Lib "user32" Alias "GetWindowTextA" _
    			(ByVal hwnd As Long, ByVal lpString As String, ByVal cch As Long) As Long
    
    Public Declare Function FindWindow Lib "user32" Alias "FindWindowA" _
    			(ByVal lpClassName As String, ByVal lpWindowName As String) As Long
    
    Public Declare Function ShowWindow Lib "user32" (ByVal hwnd As Long, _
    			ByVal nCmdShow As Long) As Long
    
    Public Declare Function SetWindowText Lib "user32" Alias "SetWindowTextA" _
    			(ByVal hwnd As Long, ByVal lpString As String) As Long

    Дадим краткую характеристику используемых функций:

  • GetActiveWindow возвращает описатель активного окна.
  • GetWindowRect получает в качестве входного параметра описатель окна hwnd и возвращает значения полей структуры Rect, переданной функции в качестве второго параметра lpRect. Заметьте, тип Rect должен быть предварительно определен. Обратите внимание, первый параметр передается по значению, а второй по ссылке. Передача по ссылке позволяет функции заполнить значениями поля переданной ей структуры. Значения этих полей задают координаты прямоугольника, определяющего положение окна на экране дисплея. Возвращаемый функцией результат будет равен нулю, если выполнение функции закончится неуспехом, например, при некорректном задании описателя.
  • GetWindowText по описателю окна возвращает его заголовок. Поскольку функция должна вернуть строку, то, как мы говорили ранее, ей передаются два параметра - lpString и cch, задающие строку и число символов, доступных для заполнения. Обратите внимание, здесь используется псевдоним с окончанием A, указывающим на использование кодировки ANSI. Опять-таки, результат, возвращаемый функцией, зависит от успеха операции.
  • FindWindow возвращает описатель окна. Функция ищет окно и возвращает в случае успеха описатель этого окна. Окно ищется либо по имени класса, заданного параметром lpClassName, либо по заголовку, заданному параметром lpWindowName. При вызове может быть задан только один из этих параметров, второй может быть указателем с неопределенным значением.. Поскольку параметр может быть либо указателем, имеющим тип Long, либо строкой, то в предыдущих версиях по этой причине для обоих параметров следовало указывать тип Any. Теперь указывается тип String, поскольку введена специальная константа vbNullString, формально имеющая тип String, но позволяющая передать указатель со значением Null.
  • ShowWindow позволяет сделать видимым или невидимым окно, заданное описателем, может также минимизировать, максимизировать или нормализовать его размеры. Соответствующее действие определяется вторым параметром, значения которого задаются соответствующей константой. В разделе объявлений модуля мы определили возможные значения этих констант, которые будут использованы в процессе вызова этой функции.
  • SetWindowText позволяет установить новый заголовок окна. Является функцией, парной к функции GetWindowText, но, поскольку здесь строка заголовка передается функции, то функция устроена проще, имеет два, а не три параметра и передавать ей можно константу, что и будет продемонстрировано в нашем примере.
  • Приведем теперь процедуру, в которой поочередно запускаются описанные здесь функции (В примерах используются документы: DocOne6, DocTwo6

    Public Sub WorkWithWindows()
    		Dim Res As Long 'Результат выполнения функции
    		Dim HandleAW As Long		'Описатель активного окна
    		Dim RectAW As RECT	'Структура, задающая прямоугольник окна
    		Dim TextAW As String 'Заголовок активного окна
    		Dim LenTextAW As Long 'Длина строки
    		Dim HandleW As Long		'Описатель окна
    		Dim TextW As String 'Заголовок окна
    		
    		'Получить описатель активного окна
    		HandleAW = GetActiveWindow
    		Debug.Print HandleAW
    		
    		'Получить прямоугольник, задающий положение активного окна
    		Res = GetWindowRect(HandleAW, RectAW)
    		Debug.Print Res
    		If Res > 0 Then 'OK
    			Debug.Print "Размеры окна: Left = ", RectAW.Left, " Top = ", _
    			RectAW.Top, " Right = ", RectAW.Right, " Bottom = ", RectAW.Bottom
    		Else:
    		MsgBox ("Не удалось получить размеры активного окна")
    		End If
    		'Получить заголовок окна
    		'Предварительная набивка результирующей строки	нулевыми символами
    		TextAW = VBA.String$(255, vbNullChar)
    		LenTextAW = VBA.Len(TextAW)
    		Res = GetWindowText(HandleAW, TextAW, LenTextAW)
    		Debug.Print Res
    		If Res > 0 Then 'OK
    			TextAW = VBA.Left(TextAW, VBA.InStr(1, TextAW, vbNullChar) - 1)
    			Debug.Print TextAW
    		Else:
    		MsgBox ("Не удалось получить заголовок активного окна")
    		End If
    		
    		'Поиск окна документа по его заголовку
    		'Возвращается описатель окна
    		TextW = "DocOne6 - Microsoft Word"
    		HandleW = FindWindow(vbNullString, TextW)
    		If HandleW > 0 Then 'OK
    			Debug.Print HandleW
    		Else:
    		MsgBox ("Не удалось найти окно с указанным заголовком" _
    		 vbCrLf  TextW)
    		End If
    		
    		'Минимизация и нормализация окна документа
    		Res = ShowWindow(HandleW, SW_SHOWMINIMIZED)
    		If Res > 0 Then Debug.Print "Окно минимизировано"
    		Res = ShowWindow(HandleW, SW_SHOWNORMAL)
    		If Res > 0 Then Debug.Print "Окно	в нормальном состоянии"
    		
     	'Изменение заголовка окна
    		TextW = "Document1 - Microsoft Word"
    		HandleW = FindWindow(vbNullString, TextW)
    		If HandleW > 0 Then 'OK
    			Debug.Print HandleW
    		Else:
    		MsgBox ("Не удалось найти окно с указанным заголовком" _
    		 vbCrLf  TextW)
    		End If
    		Res = SetWindowText(HandleW, "DocTwo6 - Microsoft Word")
    End Sub

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

    655706 
     1 
    Размеры окна:      Left = 8      Top = 73      Right = 769      Bottom = 580 
     58 
    Microsoft Visual Basic - DocOne6 [running] - [Окна (Code)]
     6684884 
    Окно минимизировано
    Окно	в нормальном состоянии
     191103490

    Дадим комментарии к работе этой процедуры:

  • Вначале, при вызове функции API GetActiveWindow был получен описатель активного окна. Заметим, что это было окно кода выполняемой процедуры. Значение этого описателя равно 655706.
  • Затем, при вызове функции GetWindowRect, был получен прямоугольник, определяющий положение окна на экране. Функция успешно завершила работу и вернула результат, равный 1 (истина). Как и в остальных случаях, по окончании выполнения функции мы проверили результат на успешность завершения.
  • На следующем шаге был получен заголовок активного окна. Как видно из распечатки заголовка, активным являлось выполняемое окно кода. Результат 58, который вернула функция GetWindowText, задает число символов результирующей строки. Обратите внимание, перед вызовом функции передаваемая ей строка была инициализирована нулевыми символами. После успешного завершения из строки был выделен результат, задающий заголовок. Его печать показывает, что активным в момент запуска функции было выполняемое окно кода. Для выделения заголовка из строки использовался нулевой символ, как признак окончания заголовка. Для решения этой задачи можно было использовать и число возвращаемых символов - значение, возвращаемое функцией.
  • Следующим шагом было получение описателя окна по заданному заголовку. В качестве такового был использован заголовок окна с тестовым документом. Функция FindWindow нашла такое окно и вернула его описатель, равный 6684884. Обратите внимание, первый параметр был задан константой vbNullString.
  • Окно, описатель которого был получен в последнем вызове, дважды перестраивалось, - вначале минимизировалось, затем нормализовалось, - при двух вызовах функции ShowWindow с различными значениями констант.
  • На последнем этапе снова вызывалась функция FindWindow для нахождения описателя вновь открытого документа. Затем при вызове функции SetWindowText был изменен заголовок этого окна. Заметьте, новое значение заголовка передавалось функции в виде обычной строковой константы.
  • Характеристики окружения

    Наш следующий пример показывает, как получить, используя вызов Win32 API функций, следующие характеристики окружения:

  • Версию используемой операционной системы.
  • Тип процессора.
  • Общий и используемый объем физической и виртуальной памяти.
  • Функции, используемые в этом примере, принадлежат библиотеке Kernel32. Заметьте, что в отличие от большинства случаев в данном примере будут вызываться не только функции, но и процедуры, хранящиеся в библиотеке Kernel32. Начнем с описания констант, типов и операторов Declare, предшествующих вызову API функций:

    Option Explicit
     'Константы
    		Public Const PROCESSOR_INTEL_386 = 386
    		Public Const PROCESSOR_INTEL_486 = 486
    		Public Const PROCESSOR_INTEL_PENTIUM = 586
    		Public Const PROCESSOR_MIPS_R4000 = 4000
    		Public Const PROCESSOR_ALPHA_21064 = 21064
    'Типы
    		Type SYSTEM_INFO
    				dwOemID As Long
    				dwPageSize As Long
    				lpMinimumApplicationAddress As Long
    				lpMaximumApplicationAddress As Long
    				dwActiveProcessorMask As Long
    				dwNumberOrfProcessors As Long
    				dwProcessorType As Long
    				dwAllocationGranularity As Long
    				dwReserved As Long
    		End Type
    		Type OSVERSIONINFO
    				dwOSVersionInfoSize As Long
    				dwMajorVersion As Long
    				dwMinorVersion As Long
    				dwBuildNumber As Long
    				dwPlatformId As Long
    				szCSDVersion As String * 128
    		End Type
    		Type MEMORYSTATUS
    				dwLength As Long
    				dwMemoryLoad As Long
    				dwTotalPhys As Long
    				dwAvailPhys As Long
    				dwTotalPageFile As Long
    				dwAvailPageFile As Long
    				dwTotalVirtual As Long
    				dwAvailVirtual As Long
    		End Type
    'Операторы Declare
    		Declare Function GetVersionEx Lib "kernel32" Alias "GetVersionExA" _
    			(LpVersionInformation As OSVERSIONINFO) As Long
    		Declare Sub GlobalMemoryStatus Lib "kernel32" (lpBuffer As _
    			MEMORYSTATUS)
    		Declare Sub GetSystemInfo Lib "kernel32" (lpSystemInfo As _
    			SYSTEM_INFO)

    Для сбора информации об окружении - используемом процессоре, операционной системе, имеющейся памяти и других характеристиках компьютера используем три следующие функции (процедуры) Win32 API:

  • Функция GetVersionEx имеет в качестве единственного параметра, передаваемого по ссылке, структуру типа OSVERSIONINFO. В результате выполнения функции будут заполнены поля этой структуры, содержащие всю необходимую информацию об используемой версии операционной системы. Конечно, необходимо в раздел объявлений включить и объявление типа OSVERSIONINFO. Возвращаемый функцией результат позволяет проанализировать успешность ее выполнения.
  • Процедура GlobalMemoryStatus имеет единственный, передаваемый по ссылке параметр типа MEMORYSTATUS. В результате выполнения процедуры поля структуры заполняются собранной информацией об объеме физической и виртуальной памяти, общей и доступной для использования в текущий момент, размере слова и некоторых других характеристиках памяти.
  • Процедура GetSystemInfo имеет единственный, передаваемый по ссылке параметр типа SYSTEM_INFO. В результате выполнения процедуры поля структуры заполняются собранной системной информацией о типе процессора, числе процессоров, используемом размере страницы и других характеристиках компьютера. Нужно ли говорить, что для корректного вызова функции требуется объявление типа SYSTEM_INFO и констант, необходимых для анализа значений поля dwProcessorType, определяющего тип центрального процессора.
  • Приведем теперь процедуру, в которой вызываются указанные функции API, обрабатывается полученная информация, конечные результаты выводятся в окно отладки:

    Public Sub WorkWithStatus()
    		Dim res As Long		'Результат выполнения функции
    		Dim msg As String			' Формируемое сообщение
    		Dim verinfo As OSVERSIONINFO		'Информация об ОС и ее версиях
    		Dim sysinfo As SYSTEM_INFO	'Системная информация
    		Dim memstatus As MEMORYSTATUS 'Информация о статусе памяти
    
    		verinfo.dwOSVersionInfoSize = Len(verinfo)
    		res = GetVersionEx(verinfo)
    		If res > 0 Then
    			Select Case verinfo.dwPlatformId
    				Case 0
    						msg = "Windows 32s "
    				Case 1
    						msg = "Windows 95/98 "
    				Case 2
    						msg = "Windows NT "
    			End Select
    			msg = msg  verinfo.dwMajorVersion  "."  verinfo.dwMinorVersion
    			msg = msg  " (Build "  verinfo.dwBuildNumber  ")"  vbCrLf
    		Debug.Print msg
    		Else
    			MsgBox ("Не могу получить версию операционной системы")
    		End If
    		
    		' определение типа процессора
    		GetSystemInfo sysinfo
    		msg = "Процессор: "
    		Select Case sysinfo.dwProcessorType
    			Case PROCESSOR_INTEL_386
    				msg = msg  "Intel 386"  vbCrLf
    			Case PROCESSOR_INTEL_486
    				msg = msg  "Intel 486"  vbCrLf
    			Case PROCESSOR_INTEL_PENTIUM
    				msg = msg  "Intel Pentium"  vbCrLf
    			Case PROCESSOR_MIPS_R4000
    				msg = msg  "MIPS R4000"  vbCrLf
    			Case PROCESSOR_ALPHA_21064
    				msg = msg  "DEC Alpha 21064"  vbCrLf
    			Case Else
    				msg = msg  "(unknown)"  vbCrLf
    
    		End Select
    		Debug.Print msg
    		msg = "Число процессоров: "  sysinfo.dwNumberOrfProcessors  vbCrLf
    		Debug.Print msg
    		msg = "Размер страницы: "  sysinfo.dwPageSize  vbCrLf
    		Debug.Print msg
    		msg = "Минимальный адрес приложения: "  sysinfo.lpMinimumApplicationAddress  vbCrLf
    		Debug.Print msg
    		msg = "Максимальный адрес приложения: "  sysinfo.lpMaximumApplicationAddress  vbCrLf
    		Debug.Print msg
    
    		' Получение характеристик памяти
    		GlobalMemoryStatus memstatus
    		msg = "Физическая память. Всего: "  _
    			VBA.Format$(memstatus.dwTotalPhys \ 1024, "###,###,###")  "K"  vbCrLf
    		Debug.Print msg
    	
     	msg = "Физическая память. Доступно: "  _
    			VBA.Format$(memstatus.dwAvailPhys \ 1024, "###,###,###")  "K"  vbCrLf
    		Debug.Print msg
    
    		msg = "Виртуальная память. Всего: "  _
    			VBA.Format$(memstatus.dwTotalVirtual \ 1024, "###,###,###")  "K"  vbCrLf
    		Debug.Print msg
    		msg = "Виртуальная память. Доступно: "  _
    			VBA.Format$(memstatus.dwAvailVirtual \ 1024, "###,###,###")  "K"  vbCrLf
    		Debug.Print msg
    		msg = "Длина слова: "  memstatus.dwLength  vbCrLf
    		Debug.Print msg
    		msg = "Загрузка памяти: "  memstatus.dwMemoryLoad  vbCrLf
    		Debug.Print msg
    
    End Sub

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

    Windows NT 4.0 (Build 1381)
    
    Процессор: Intel Pentium
    
    Число процессоров: 1
    
    Размер страницы: 4096
    
    Минимальный адрес приложения: 65536
    
    Максимальный адрес приложения: 2147418111
    
    Физическая память. Всего: 32 180K
    
    Физическая память. Доступно: 4 772K
    
    Виртуальная память. Всего: 2 097 024K
    
    Виртуальная память. Доступно: 1 982 872K
    
    Длина слова: 32
    
    Загрузка памяти: 0

    Вызов функций Win32 API, работающих в Unicode кодировке

    Уже говорилось, что функции API, работающие со строками, вызываются в ANSI кодировке. Сейчас мы попытаемся объяснить причину этого факта, а, с другой стороны, покажем, как можно вызывать функции Win32 API, использующие Unicode кодировку. Заметим, что это может быть важным не столько для функций Win32 API, сколько для других внешних функций, которые могут существовать в кодировке Unicode и не иметь ANSI варианта. Начнем с объяснения ситуации, - почему в VBA внешние функции вызываются в кодировке ANSI. Следует понимать, что строки VBA хранятся в Unicode кодировке и передача строк при вызове внутренних функций внутри VBA происходит в кодировке Unicode. Однако VB и VBA предполагают, что внешний мир устроен по-другому и до сих пор использует кодировку ANSI. Поэтому всякий раз, когда вызываются внешние функции, при вызове происходит преобразование и строковая информация передается и возвращается в кодировке ANSI. По этой причине нельзя вызвать функцию в кодировке Unicode простым изменением псевдонима, задав у него окончание W. Покажем сейчас, как можно "обмануть" VBA, заставив его не выполнять указанных преобразований, что и позволит вызывать функции, корректно работающие в Unicode кодировке. Покажем также, что, как и всякий обман, не всегда все заканчивается благополучно. Тем не менее, с предлагаемым приемом полезно познакомиться. Решение задачи основывается на следующем:

  • При вызове функции вместо строки используется массив байтов, хранящий копию строки. Напомним, что внутри VBA строка хранится в Unicode кодировке, поэтому и массив байтов будет хранить строку в этой кодировке.
  • В операторе Declare необходимо тип String изменить на тип Any, что обеспечит отсутствие проверок и преобразований.
  • Если в операторе Declare для строкового параметра указан спецификатор ByVal, то его необходимо удалить или изменить на ByRef, явно указав передачу параметра по ссылке.
  • Остальные детали рассмотрим после приведения соответствующей программы. В качестве примера мы воспользуемся уже рассмотренными функциями:

  • FindWindow, которая позволяет найти окно по его заголовку, вернув описатель окна в качестве результата,
  • GetWindowText, SetWindowText, позволяющие получить и установить новый заголовок окна.
  • Заметим сразу же, что нам удалось успешно вызвать и корректно работать с двумя последними функциями в Unicode кодировке. Однако этот прием не работает при вызове функции FindWindowW. Несмотря на все попытки, переданная для поиска строка заголовка не приводила к успешному завершению поиска. Но обо всем по порядку. Приведем вначале раздел объявлений модуля с именем Unicode, созданного для работы с этим примером:

    Option Explicit
    'Объявление вызываемых функций в Unicode кодировке
     Public Declare Function FindWindowA Lib "user32" _
    			(ByVal lpClassName As String, ByVal lpWindowName As String) As Long
    'Функции в Unicode кодировке
    'Тип string заменен на Any. Передача аргумента по ссылке
     Public Declare Function FindWindowW Lib "user32" _
    			(lpClassName As Any, lpWindowName As Any) As Long
     
     Public Declare Function GetWindowText Lib "user32" Alias "GetWindowTextW" _
    			(ByVal hwnd As Long, lpString As Any, ByVal cch As Long) As Long
     
     Public Declare Function SetWindowText Lib "user32" Alias "SetWindowTextW" _
    			(ByVal hwnd As Long, lpString As Any) As Long
     
     Public ArCapt() As Byte 'Объявление динамического массива

    Все пояснения уже сделаны и поэтому приведем процедуру этого модуля, вызывающую функции API:

    Public Sub WorkWithUniFunc()
    	Dim res As Long
    	Dim Capt As String	'Заголовок
    	'Динамический массив байтов для передачи строки заголовка
    	Dim HandleW As Long 'Описатель окна
    	
    	'Поиск окна по заголовку
    	Capt = "Document1 - Microsoft Word"
    	HandleW = FindWindowA(vbNullString, Capt)
    	If HandleW > 0 Then 'OK
    		Debug.Print HandleW
    	Else: MsgBox ("FindWindowA не может найти окно с заголовком"  vbCrLf  Capt)
    	End If
    	'Попытки использовать для поиска Unicode функцию
    	'FindWindowW не увенчались успехом
    '	ReDim ArCapt(0 To 2 * VBA.Len(Capt)) As Byte
    '	ArCapt = Capt  vbNullChar
    '	Debug.Print ArCapt
    '	HandleW = FindWindowW(0, ArCapt(0))
    '	If HandleW > 0 Then 'OK
    '		Debug.Print HandleW
    '	Else: MsgBox ("Не могу вызвать UniCode FindWindowW")
    '	End If
    	'Получить заголовок окна
    	ArCapt = VBA.String$(128, vbNullChar)
    	res = GetWindowText(HandleW, ArCapt(0), 128)
    	If res > 0 Then 'OK
    		Debug.Print ArCapt
    	Else: MsgBox ("не получен заголовок окна")
    	End If
    		
    	'Изменить заголовок окна
    	Capt = "NewDoc"
    	ArCapt = Capt  vbNullChar
    	res = SetWindowText(HandleW, ArCapt(0))
    		
    	'Повторно получить заголовок окна
    	ArCapt = VBA.String$(128, vbNullChar)
    	res = GetWindowText(HandleW, ArCapt(0), 128)
    	If res > 0 Then 'OK
    		Debug.Print ArCapt
    	Else: MsgBox ("не получен заголовок окна")
    	End If
    End Sub

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

    327894 
    Document1 - Microsoft Word
    
    Document1 - Microsoft Word
    NewDoc

    Дадим краткие комментарии к тексту процедуры:

  • Работа процедуры начинается с вызова функции API FindWindowA, работающей в кодировке ANSI. Она успешно находит окно, заголовок которого задан переменной Capt. Функция возвращает его описатель.
  • На следующем шаге мы пытались решить эту же задачу, используя функцию API FindWindowW, работающую в кодировке Unicode. В тексте нашел отражение один из вариантов решения. К сожалению, поиск во всех случаях заканчивался неуспехом, хотя, как показал побайтный анализ в окне Watch и как показывает отладочная печать, массив байтов ArCapt содержит Unicode копию строки заголовка
  • Полученный описатель окна использовался при вызове Unicode варианта функции GetWindowTextW. Функция корректно работала, используя ссылку на переданный ей массив байтов ArCapt.
  • Затем, используя эту же технику, заголовок окна был изменен при вызове Unicode варианта функции SetWindowTextW.
  • Для контроля повторно была вызвана функция GetWindowTextW. Отладочная печать подтвердила корректность работы.
  • Неудача в вызове функции FindWindowW может быть вызвана разными причинами. Вероятнее всего, при выполнении поиска и проведения операций сравнения строк, применяемый способ не корректен, или, по крайней мере, требует дополнительных уточнений, возможно связанных с длиной строки. С другой стороны, сам прием является некоторой уловкой. Существует более честный способ, хотя, возможно, и более трудоемкий. Для решения задачи можно создать библиотеку типов TypeLib, содержащую описание функций Win32 API в Unicode кодировке, включить ссылку на эту библиотеку и вызывать функции без всяких уловок.

    Обработка ошибок, возникающих при вызове функций Win32 API

    Как мы уже говорили ранее, не бывает программ без ошибок. Если ошибка возникает при выполнении кода процедур и функций VBA, - ошибка периода выполнения (run time error), - то появляется окно сообщения об ошибке. Если ошибка периода выполнения появляется при работе функции Win32 API, то прерывания работы программы не происходит, окно сообщения об ошибке не появляется. Вместо этого функция возвращает значение 0 в качестве результата, свидетельствующее об ошибке периода выполнения. Тем не менее, большинство функций Win32 API сохраняют информацию о возникшей ошибке. Эту информацию можно получить стандартным способом, используя VBA объект Err. Свойство LastDLLErr этого объекта возвращает номер последней ошибки, возникшей в DLL. К сожалению, сам по себе номер мало что говорит. Необходимо знать описание ошибки, соответствующее этому номеру. Частично причину ошибки можно понять по имени константы, которую можно найти в уже неоднократно упоминавшемся файле Win32API.txt, используемом в API Viewer. Опять-таки, к сожалению, возможные значения констант приводятся независимо от функций, в которых они возникают. И, несмотря на то, что все такие константы начинаются со слова ERROR найти константу по ее значению не так то просто. Можно, конечно, воспользоваться возможностью создания базы данных по текстовому файлу и организовать специальный запрос, позволяющий найти имя константы по ее значению. Естественно, что лучше всего иметь полную информацию об используемых функциях Win32 API, включающую, в том числе, и сведения о возможных ошибках периода выполнения данных функций. Эту информацию можно найти, если под рукой есть подходящая литература, например, справочник программиста Win32, или поискать на упоминавшемся сервере Microsoft для разработчиков.

    Естественно, что пример ошибки времени выполнения в процессе работы DLL у нас уже под рукой. Нам и изобретать его не было необходимости. Как Вы помните, в последнем примере мы сетовали на возникновение подобной ошибки в процессе поиска описателя окна по его заголовку при вызове функции FindWindowW, работающей в Unicode кодировке. Давайте вернемся к этому примеру и попробуем обработать эту ошибку. В раздел объявлений ранее созданного модуля Unicode мы добавили объявление констант и функций и теперь он выглядит так:

    Option Explicit
    
    Public Const ERROR_INVALID_NAME = 123
    
    'Объявление вызываемых функций
     Public Declare Function FindWindowA Lib "user32" _
    			(ByVal lpClassName As String, ByVal lpWindowName As String) As Long
    'Функции в Unicode кодировке
    'Тип string заменен на Any. Передача аргумента по ссылке
     Public Declare Function FindWindowW Lib "user32" _
    			(lpClassName As Any, lpWindowName As Any) As Long
     
     Public Declare Function GetWindowText Lib "user32" Alias "GetWindowTextW" _
    			(ByVal hwnd As Long, lpString As Any, ByVal cch As Long) As Long
     
     Public Declare Function SetWindowText Lib "user32" Alias "SetWindowTextW" _
    			(ByVal hwnd As Long, lpString As Any) As Long
     
     Public Declare Function GetActiveWindow Lib "user32" () As Long
    
     Public ArCapt() As Byte 'Объявление динамического массива

    Приведем теперь процедуру, в которой вызывается функция FindWindowW, приводящая к ошибке периода выполнения:

    Public Sub WorkWithApiErr()
    		Dim Res As Long
    		Dim capt As String	'Заголовок
    		Dim HandleW As Long 'Описатель окна
    		
    		'Поиск окна по заголовку
    		capt = "DocOne6 - Microsoft Word"
    		ArCapt = capt  vbNullChar
    		Debug.Print ArCapt
    		HandleW = FindWindowW(0, ArCapt(0))
    		If HandleW > 0 Then 'OK
    			Debug.Print HandleW
    		Else: MsgBox ("Не могу корректно вызвать UniCode FindWindowW")
    			If Err.LastDllError = ERROR_INVALID_NAME Then
    				Debug.Print "Не корректно задано имя при вызове Unicode 
    				FindWindowW функции!"
    			End If
    		End If
    		'Еще один эксперимент: вначале получим заголовок активного окна,
    		'затем найдем окно по заголовку, работая в Unicode кодировке.
    		HandleW = GetActiveWindow()
    		
    			'Получить заголовок окна
    		ArCapt = VBA.String$(128, vbNullChar)
    		
    		Res = GetWindowText(HandleW, ArCapt(0), 128)
    		If Res > 0 Then 'OK
    			Debug.Print ArCapt
    		Else: MsgBox ("не получен заголовок окна")
    		End If
    		
    		ArCapt = VBA.Left(ArCapt, Res)
    		HandleW = FindWindowW(0, ArCapt(0))
    		If HandleW > 0 Then 'OK
    			Debug.Print HandleW
    		Else: MsgBox ("Не могу корректно вызвать UniCode FindWindowW")
    			If Err.LastDllError = ERROR_INVALID_NAME Then
    				Debug.Print "Не корректно задано имя при вызове Unicode 
    				FindWindowW функции!"
    			End If
    		End If
    End Sub

    Приведем результаты печати , появившиеся в окне отладки при выполнении этой процедуры:

    DocOne6 - Microsoft Word 
    Не корректно задано имя при вызове Unicode FindWindowW функции!
    Microsoft Visual Basic - DocOne6 [running] - [Unicode (Code)]	
    Не корректно задано имя при вызове Unicode FindWindowW функции!

    Прокомментируем теперь работу программы и полученные результаты:

  • Вначале мы попытались найти окно с заведомо существующим заголовком, - окно документа, содержащего тестовые примеры. В процессе работы функции Win32 API FindWindowW возникла ошибка периода выполнения, функция вернула нулевой результат. Ошибка была обработана, и как показывает константа ERROR_INVALID_NAME, причиной является ошибка в задании имени (передаваемый формат в виде массива байтов не годится для цели поиска и сравнения строк), о чем свидетельствует отладочная информация.
  • Далее проводится еще один эксперимент на ту же тему. Для активного окна находится заголовок, используя функцию GetWindowTextW, возвращающую строку в виде массива байтов. Тут же этот массив используется для поиска окна по заголовку. Однако ничего не помогает и снова при поиске окна возникает ошибка. Она обрабатывается, о чем выдается соответствующее сообщение.
  • Функции API и вызов Callback функций

    Мы уже говорили о функциях обратного вызова, называемых Callback функциями. Для "многослойного" способа построения программных систем, характерного для программирования, функции внешнего слоя могут вызывать функции ядра без особых проблем. Однако паритета между ядром и внешним слоем нет. Вызов функций внешнего слоя из ядра затруднен. Чтобы как-то решить эту проблему и вводятся функции обратного вызова. Если функции ядра, в ответ на ее вызов из внешнего слоя, в свою очередь необходимо вызвать функцию внешнего слоя, то ядро диктует условия, каким должна удовлетворять вызываемая функция. Есть специальные механизмы, обеспечивающие вызов таких Callback функций, но во всех случаях заголовок вызываемой функции жестко фиксирован и известен ядру. Этот механизм Callback функций применяется и для обеспечения двусторонней связи между функциями VBA и функциями Win32 API, которым в процессе их работы требуется обратный вызов функций VBA.

    Заметьте, в предыдущих версиях VBA не было возможности явным образом работать с функциями Win32 API, требующими вызова Callback функций. Теперь такая возможность появилась, благодаря включению в язык возможности передачи указателя функции в качестве параметра процедур и функций. Явное введение в язык конструкции AddressOf, возвращающей указатель на функцию, дало возможность при вызове функции Win32 API передать ей в качестве аргумента имя Callback функции. Попробуем разобраться в деталях того, как вызываются функции Win32 API, требующие Callback функции для своей работы, как пишутся такие функции на VBA, как передается информация между функциями, - как это все, в конечном итоге, согласуется между собой. Начнем, прежде всего, с ответа на вопрос, а как узнать, что функция Win32 API требует для своей работы вызова Callback функции. Подсказку можно получить от обозревателя, если проанализировать оператор Declare, созданный API Viewer. Когда имя параметра начинается префиксом lp, а заканчивается окончанием Func, это означает, что соответствующий аргумент является ссылкой на имя Callback функции. К сожалению, обозреватель не содержит необходимой информации о том, каким должен быть заголовок функции обратного вызова, так что необходимо обращаться к документации по Win32 API или идти на сервер. Заметьте, документация, как правило, ориентирована на C программистов, поэтому необходимо самому корректно транслировать заголовок к виду, понимаемому VBA. Ошибки в задании типов аргументов, пропуск описателя ByVal могут дорого стоить. Пожалуй, одна из наиболее сложных задач при работе с Callback функцией состоит в том, чтобы найти ее описание, а затем, используя документацию, ориентированную на язык C/C++, корректно описать на VBA заголовок этой функции.

    Еще одна, важная для понимания задача состоит в организации правильного обмена информацией между процедурой VBA, вызываемой ею функцией Win32 API и вызываемой ею Callback функцией. Прежде всего, следует понимать, что программисту никогда не приходится вызывать самому Callback функцию. Ее всегда вызывает соответствующая функция Win32 API. Она же передает ей текущие значения аргументов, необходимые для работы функции обратного вызова. Но, конечно же, в большинстве случаев Callback функция производит изменения в мире объектов VBA программы и, следовательно, она должна быть каким-то образом связана с этим миром. Иногда это делается за счет того, что в функции Win32 API предусмотрен специальный параметр, который вызывающая ее программа передает ей, а она, в свою очередь, передает его функции обратного вызова. Недостаток такого способа состоит в том, что передаваемый параметр один, а информация, связывающая функцию обратного вызова с миром VBA, может быть разнородной. В этих условиях более предпочтительным может быть способ передачи и получения данных в Callback функцию через глобальные переменные. Именно этот способ мы использовали в наших примерах. Прежде, чем перейти к примерам, давайте подведем итоги и еще раз сформулируем основные этапы организации работы при вызове функций Win32 API, требующих Callback функций. Итак, необходимо:

  • Определить, что функция Win32 API требует вызова Callback функции.
  • Найти документацию по этой функции, описывающую требования к заголовку этой функции. Если эта документация ориентирована на язык C/C++, то привести ее к виду, требуемому VBA.
  • Понять, как передать информацию об объектах VBA в Callback функцию.
  • Написать одну или несколько реализаций функций обратного вызова. Обращаем внимание, что функций обратного вызова может быть несколько. Имя функции не является жестко зафиксированным. Оно передается функции Win32 API как аргумент в момент вызова. Поэтому в зависимости от контекста одну и ту же функцию Win32 API можно вызывать с различными Callback функциями.
  • Вызвать функцию Win32 API, передав ей в момент вызова имя Callback функции и другие необходимые аргументы.
  • Функции высших порядков и конструкция AddressOf

    Функцией (процедурой) высших порядков в программировании называют функцию (процедуру), один из формальных параметров которой имеет тип функции или процедуры. Введение в язык функций высших порядков существенно повышает выразительную силу языка программирования. Классическим примером процедуры высшего порядка является процедура вычисления интеграла, одним из параметров которой выступает подынтегральная функция. Типичным примером функций высших порядков являются функции Win32 API, требующие вызова Callback функций. Существует несколько способов введения функций высших порядков в язык программирования, среди которых лучшим, видимо, является способ, основанный на введении функционального типа. В этом случае можно объявлять переменные типа Func или Proc, а затем уже передавать такие переменные, как аргументы при вызове функции высшего порядка. Другой классический способ основан на работе с указателями. Такой типизированный указатель может хранить ссылку на функцию, - содержать ее адрес, и может быть передан в качестве аргумента при вызове функции высшего порядка. Именно этот способ и реализован в VBA. С этой целью в язык введена конструкция:

    AddressOf имя

    Параметр имя может быть именем процедуры, функции или процедуры - свойства ( Property ). В качестве результата возвращается ссылка на объект с указанным именем.

    К сожалению, введя долгожданную конструкцию AddressOf, разработчики остановились на пол пути. С ее помощью можно передать функции высшего порядка в качестве аргумента имя функции. Однако, по-прежнему, нельзя описать на VB или VBA функцию высшего порядка. Такие функции должны быть внешними, как функции Win32 API или функции собственноручно разработанной DLL на языке C/C++. Можно было бы предъявить и еще одно "законное" требование к этой конструкции. Было бы весьма полезно, если бы она позволяла получать ссылку на объект любого произвольного типа, не ограничиваясь только функциональным типом. В этом случае работа с указателями в языке VBA стала бы полноценной.

    В настоящее время на конструкцию AddressOf накладывается целый ряд ограничений:

  • Эта конструкция может быть использована только в выражении, задающем вызов функции высшего порядка. Здесь она используется как аргумент, непосредственно предшествуя имени процедуры, передаваемой в качестве фактического параметра. Заметьте, эту конструкцию нельзя использовать при описании функции или процедуры, из-за чего невозможно определить функцию высшего порядка средствами VBA.
  • Процедуры, функции и свойства, которые вызываются конструкцией AddressOf, должны быть в том же проекте, в котором помещено объявление и вызов функции высшего порядка. Вызываемые процедуры и функции должны быть расположены в стандартном модуле, они не могут находиться в модуле класса или в модуле формы.
  • Эту конструкцию можно использовать только для вызова собственных процедур и функций, - ее нельзя использовать для вызова внешних или стандартных функций.
  • Функции перечисления Win32 API

    Группу функций Win32 API, требующих вызова Callback функций, составляют так называемые функции перечисления, - Enum функции. Эти функции позволяют перечислить в определенном порядке все объекты операционной системы заданной группы. Вызывая на каждом шаге перечисления функцию обратного вызова, функция Win32 API передает ей текущий объект группы в качестве аргумента. Функция обратного вызова уже может производить над этим объектом различные, но, естественно, допустимые действия. Тем самым у программиста появляется возможность работы с коллекцией объектов, возможность задать собственную обработку для каждого из объектов. К функциям перечисления относятся такие функции как: EnumWindows, EnumPrinters, EnumFontFamilies, EnumFonts, EnumPorts, EnumResourseNames и многие другие. В качестве примера, рассмотрим работу с одной из этих функций.

    Функция EnumWindows

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

    Начнем с описания функции EnumWindows в том виде, в каком оно представлено в документации Platform SDK:

    BOOL EnumWindows(
    	WNDENUMPROC lpEnumFunc,	// pointer to callback function
    	LPARAM lParam		// application-defined value
    );

    Функция EnumWindows перечисляет все окна верхнего уровня, передавая текущий описатель окна Callback функции, определенной в приложении. Функция не перечисляет дочерние окна - Child Windows. Функция выполняется, пока перечисление не будет закончено или Callback функция не вернет значение False. Ее параметры:

  • lpEnumFunc - указатель на определенную в приложении Callback функцию.
  • lParam - определенное приложением значение, передаваемое в Callback функцию.
  • Функция возвращает значение 0 в случае неуспеха и ненулевое значение при благоприятном исходе.

    Описание Callback функции EnumWindowsProc, полученное из той же документации, имеет вид:

    BOOL CALLBACK EnumWindowsProc(
    	HWND hwnd,		// handle to parent window
    	LPARAM lParam	// application-defined value
    );

    Функция EnumWindowsProc является Callback функцией, определенной в приложении, используемой при вызове функций Win32 API EnumWindows или EnumDesktopWindows. Она получает при вызове описатель окна верхнего уровня. Тип WNDENUMPROC определяет указатель на эту Callback функцию. Имя EnumWindowsProc является держателем места (placeholder) и должно быть замещено именем функции, определенной в приложении.

    Ее параметры:

  • hwnd - описатель окна верхнего уровня.
  • lParam - определенное приложением значение, данное в EnumWindows или EnumDesktopWindows.
  • Для продолжения перечисления функция возвращает значение True, для окончания - False.

    Как видите, представленные описания функций ориентированы на язык C/C++ и нуждаются в преобразовании для их использования в программах на VB/VBA. Используя обозреватель API Viewer, можно получить оператор Declare для функции EnumWindows. Вот как выглядит заголовок этой функции после соответствующей трансляции:

    Public Declare Function EnumWindows Lib "user32" _
    		(ByVal lpEnumFunc As Long, ByVal lParam As Long) As Long

    Заметьте, появились описатели ByVal, а все типы заменились в данном случае на тип Long. Поскольку API Viewer не помощник в деле преобразования описания Callback функций, то эту работу необходимо проделать самостоятельно. В результате, описание имеет вид:

    Public Function EnumWindowsProc(ByVal HandleW As Long, _
    		ByVal lParam As Long) As Long

    Заметим, формальная трансляция не вызывает затруднений, - описатель Callback следует опустить, имя можно дать произвольное, а трансляция типов в данном случае достаточно проста. Вместе с тем, с описанием типа параметра lParam не все так просто. Ведь он должен служить для передачи произвольной информации, поэтому теоретически допускается задание любого произвольного типа для этого параметра, например, этот параметр может быть объектом. В этом случае следует быть особо внимательным, так первая наша попытка передать функции параметр, отличный от типа Long, привела к критической ошибке и прекращении работы приложения. Как я уже говорил ранее, можно применить альтернативный способ и передавать информацию, пользуясь глобальными переменными.

    Перейдем теперь к описанию реализации нашего примера. В проекте тестового документа был создан модуль с именем " ОбратныйВызов ". Вот текст раздела объявлений этого модуля:

    Option Explicit
    'Операторы Declare вызываемых функций Win32 API
    
    Public Declare Function EnumWindows Lib "user32" _
    	(ByVal lpEnumFunc As Long, ByVal lParam As Long) As Long
    
    Public Declare Function GetWindowText Lib "user32" Alias "GetWindowTextA" _
    	(ByVal hwnd As Long, ByVal lpString As String, ByVal cch As Long) As Long
    
    Public Declare Function GetClassName Lib "user32" Alias "GetClassNameA" _
    	(ByVal hwnd As Long, ByVal lpClassName As String, ByVal nMaxCount As Long) As Long
    
    'Описание глобальных переменных, обеспечивающих связь
    'с функцией обратного вызова EnumWindowsProc
    Public HandleCol As New Collection
    Public CaptCol As New Collection
    Public ClassNameCol As New Collection

    Помимо функции EnumWindows, основной для нашего примера, но о которой уже много говорилось, в разделе приведено объявление новой, ранее не упоминавшейся Win32 API функции GetClassName. Она похожа на функцию GetWindowText и возвращает по описателю окна имя класса этого окна. Функцию GetWindowText мы объявляем повторно, соответствующий оператор Declare есть в разделе объявлений другого модуля проекта, но, заметьте, в этом есть необходимость, поскольку в других модулях эта функция использовалась с различными псевдонимами. Раздел объявлений модуля содержит объявление трех глобальных переменных - трех коллекций, с которыми будет работать Callback функция EnumWindowsProc, формируя на каждом шаге своего вызова очередной элемент каждой из коллекций. Коллекции будут содержать соответственно описатели окон, заголовки окон и имена классов. Заметьте, что все окна имеют имя класса, но не все окна имеют заголовок, так что число элементов в коллекциях будет различным в процессе работы. Приведем теперь текст Callback процедуры EnumWindowsProc:

    Public Function EnumWindowsProc(ByVal HandleW As Long, _
    		ByVal lParam As Long) As Long
     Dim TextW As String
     Dim LenTextW As Long
     Dim Res As Long
     
    		'Добавить описатель в коллекцию
    		HandleCol.Add HandleW
     
    		'Получить заголовок окна.
    		TextW = VBA.String$(255, vbNullChar)
    		LenTextW = VBA.Len(TextW)
    		Res = GetWindowText(HandleW, TextW, LenTextW)
    		If Res > 0 Then
    			'Добавить заголовок в коллекцию
    			TextW = VBA.Left(TextW, Res)
    			CaptCol.Add TextW
    		End If
     
    		'Получить класс окна.
    		TextW = VBA.String$(255, vbNullChar)
    		LenTextW = VBA.Len(TextW)
    		Res = GetClassName(HandleW, TextW, LenTextW)
    		If Res > 0 Then
    			'Добавить имя класса в коллекцию
    			TextW = VBA.Left(TextW, Res)
    			ClassNameCol.Add TextW
    		End If
    	EnumWindowsProc = 1
    End Function

    Напомним, эта процедура вызывается автоматически в процессе работы процедуры EnumWindows. Поскольку процедура всегда возвращает значение 1, означающее успешность ее работы, то число ее вызовов определяется размером перечисляемого множества окон. Обратите внимание и на то, что в процедуре используется только первый параметр - описатель текущего окна, который передается вызываемым Win32 API функциям GetWindowText и GetClassName. Второй параметр вообще не используется, вместо этого напрямую происходит заполнение коллекций, заданных глобальными переменными. Так обеспечивается связь с внешним миром.

    Чтобы закончить пример, нам осталось привести описание процедуры GetCaptions, вызывающей EnumWindows:

    Public Sub GetCaptions()
    		'Вызов Win32 API функции EnumWindows,
    		'вызывающей в свою очередь Callback функцию EnumWindowsProc
    		Dim item As Variant
    		Dim Res As Long
    		
    		Res = EnumWindows(AddressOf EnumWindowsProc, 0)
    		
    		'Обработка глобальных переменных, определенных в
    		'результате совместной работы EnumWindows и EnumWindowsProc
    		Debug.Print "Число окон = ", HandleCol.Count
    		Debug.Print "Описатели окон"
    		Res = 0
    		For Each item In HandleCol
    			Debug.Print item
    			Res = Res + 1
    			If Res > 10 Then Exit For
    		Next item
    		
    		Debug.Print "Число окон с заголовками= ", CaptCol.Count
    		Debug.Print "Заголовки окон"
    		Res = 0
    		For Each item In CaptCol
    			Debug.Print item
    			Res = Res + 1
    			If Res > 10 Then Exit For
    		Next item
    			
    	Debug.Print "Число окон, возвращающих класс = ", ClassNameCol.Count
    		Debug.Print "Имена классов окон"
    		Res = 0
    		For Each item In ClassNameCol
    			Debug.Print item
    			Res = Res + 1
    			If Res > 10 Then Exit For
    		Next item
    End Sub

    Несколько комментариев к этой процедуре:

  • Главное, на что следует обратить внимание, это на операцию " AddressOf " в момент вызова Win32 API функции EnumWindows. В результате ее выполнения создается ссылка на Callback функцию (адрес расположения функции в памяти). Заметьте, на имя передаваемой функции не накладывается ограничений, поэтому в разных вызовах могут быть разные имена, что и позволяет иметь при необходимости несколько Сallback функций.
  • Мы передаем 0 в качестве значения второго параметра. Это своего рода заглушка, поскольку, как говорилось ранее, передача информации производится через глобальные переменные. В следующем примере мы продемонстрируем возможность передачи информации и через этот параметр.
  • После завершения работы процедуры перечисления начинается обработка коллекций, созданных в процессе ее работы. В данном случае обработка проста и сводится к печати числа элементов коллекции и первых десяти элементов каждой из коллекций. Возможно, было бы интересно посмотреть, сколько же объектов - окон существует в момент выполнения обычного приложения. Но полная распечатка всех имен классов и заголовков заняла бы несколько страниц текста. Ведь таких объектов несколько сотен.
  • Давайте познакомимся с начальными элементами коллекций, содержащих описатели, заголовки и имена классов объектов - окон, существующих в момент запуска процедуры GetCaptions:

    Число окон =	254 
    Описатели окон
     3735790 
     131912 
     131888 
     131916 
     65684 
     40370412 
     917748 
     262866 
     852650 
     852668 
     131844 
    Число окон с заголовками =76 
    Заголовки окон
    Continue
    Microsoft Agent
    Microsoft Office Shortcut Bar
    Menu Parent Window
    NetDDE Agent
    Edit
    Microsoft Visual Basic - DocOne6 [running] - [ОбратныйВызов (Code)]
    Ch6 - Microsoft Word
    Edit
    Properties
    Microsoft Office Shortcut Bar
    Число окон, возвращающих класс =254 
    Имена классов окон
    OfficeTooltip
    tooltips_class32
    ComboLBox
    tooltips_class32
    tooltips_class32
    AgentAnimBalloon
    AgentAnim
    tooltips_class32
    tooltips_class32
    tooltips_class32
    tooltips_class32

    Еще один пример работы с функцией EnumWindows

    Наш следующий пример является упрощенным вариантом предыдущего примера. Его целью является демонстрация возможности передать информацию в Callback процедуру EnumWindowsProc через параметр lParam. В нашем примере вместо трех коллекций будет использоваться только одна - коллекция описателей, именно она и будет передана стандартным способом через параметр lParam. Коллекция будет передана в качестве аргумента при вызове процедуры EnumWindows, а та, в свою очередь передаст его функции обратного вызова EnumWindowsProc. Приведем текст модуля, содержащего процедуры нашего примера:

    Option Explicit
    
    Public Declare Function EnumWindows Lib "user32" _
    		(ByVal lpEnumFunc As Long, ByVal lParam As Long) As Long
    
    Public Declare Function EnumWindows1 Lib "user32" Alias "EnumWindows" _
    		(ByVal lpEnumFunc As Long, lParam As Any) As Long
    
    Public HandleCol As New Collection
    Public HandleCol1 As New Collection
    
    Public Function EnumWindowsProc(ByVal HandleW As Long, _
    		ByVal lParam As Long) As Long
    
    	HandleCol.Add HandleW
    	EnumWindowsProc = 1
    End Function
    
    Public Function EnumWindowsProc1(ByVal HandleW As Long, _
    		lParam As Collection) As Long
    
    	lParam.Add HandleW
    	EnumWindowsProc1 = 1
    End Function
    
    Public Sub GetHandles()
    	
    	Dim item As Variant
    	Dim Res As Long
    		
    	Res = EnumWindows(AddressOf EnumWindowsProc, 0)
    
    	Debug.Print "Number of windows - ", HandleCol.Count
    	Debug.Print "Their handles: "
    	Res = 0
    	For Each item In HandleCol
    		Debug.Print item
    		Res = Res + 1
    		If Res > 10 Then Exit For
    	Next item
    	
    End Sub
    
    Public Sub GetHandles1()
    	
    	Dim item As Variant
    	Dim Res As Long
    
    	Res = EnumWindows1(AddressOf EnumWindowsProc1, HandleCol1)
    		
    	Debug.Print "Number of windows - ", HandleCol1.Count
    	Debug.Print "Their handles: "
    	Res = 0
    	For Each item In HandleCol1
    		Debug.Print item
    		Res = Res + 1
    		If Res > 10 Then Exit For
    	Next item
    
    End Sub

    Дадим несколько комментариев:

  • В этом примере параллельно показаны оба способа передачи информации. Имена функций, оканчивающиеся на 1, связаны с передачей информации через параметр lParam.
  • В операторе Declare, описывающем функцию EnumWindows1, тип параметра lParam задан как Any, и в данном случае параметр передается по ссылке, а не по значению.
  • При описании Callback функции EnumWindowsProc1 для этого параметра указан уже конкретный тип Collection.
  • Аргумент HandleCol1 типа Collection передается при вызове EnumWindows в процедуре GetHandles1. Функция обратного вызова EnumWindowsProc1 заполнит эту коллекцию элементами.
  • Приведем результаты ее работы:

    Number of windows -184 
    Their handles: 
     131826 
     131824 
     131854 
     131868 
     36504034 
     2359854 
     65636 
     262764 
     65690 
     65626 
     3539122

    Функции Win32 API для работы с таймером

    Кроме функций перечисления, требующих в процессе своей работы вызов Callback функций, другим известным примером является функция SetTimer, создающая таймер. Во многих приложениях возникает необходимость синхронизировать его работу в соответствии с регулярно поступающими сообщениями от таймера. Общая схема такова: таймер посылает сообщения приложению с заданным интервалом, в ответ приложение выполняет определенную работу, вызывая ту или иную функцию ( Callback функцию). Класс таких диспетчерских приложений, регулярно обрабатывающих вновь поступившие заявки, весьма велик. При работе в приложении Access для этих целей введен специальный элемент управления - Timer. В приложениях Word или Excel такого элемента нет, но всегда можно воспользоваться соответствующими функциями Win32 API, чтобы создать один или несколько собственных таймеров и организовать работу приложения, реагирующего на их сообщения. Заметьте, несмотря на то, что физический таймер один, логических таймеров, посылающих приложению свои сообщения, может быть несколько.

    Функция SetTimer

    Эта функция создает таймер, посылающий сообщения с заданным интервалом. Ее описание, которое можно найти на Platform SDK, имеет вид:

    UINT SetTimer(
    	HWND hWnd,		// handle to window for timer messages
    	UINT nIDEvent,	        // timer identifier
    	UINT uElapse,		// time-out value
    	TIMERPROC lpTimerFunc	// pointer to timer procedure
    );

    Ее параметры:

  • hwnd - Описатель окна, которому будут посылаться сообщения таймера. В VBA программах таймер не связывается с окном и значение этого параметр задается как NULL.
  • nIDEvent - Задает идентификатор таймера. Его значение игнорируется, когда таймер не связан с окном, что имеет место в рассматриваемом нами случае.
  • uElapse - Задает интервал, с которым таймер будет посылать свои сообщения. Интервал задается в миллисекундах, так что значение 1000 соответствует одной секунде.
  • lpTimerFunc - Указатель на Callback функцию, которая будет вызываться всякий раз, когда обрабатывается сообщение WM_Timer, поступающее от таймера.
  • Если функция успешно завершает свою работу и создает таймер, то в качестве результата она возвращает уникальный идентификатор этого таймера, идентифицирующий его. Этот идентификатор запоминается и используется для уничтожения таймера при вызове Win32 API функции KillTimer. В случае неуспеха возвращается значение 0.

    Заметьте, функция SetTimer только создает таймер. В отличие от функций перечисления вызов Callback функции не происходит в ее теле. Вызов осуществляется более сложным путем. Созданный таймер посылает сообщения с заданным интервалом, сообщения, как обычно, поступают в очередь сообщений и в обработчике сообщения WM_Timer автоматически вызывается функция обратного вызова. Поскольку при обработке сообщений очереди могут происходить разные задержки, то вызываемая функция не всегда будет вызываться с заданным интервалом, - возможны задержки.

    Оператор Declare, задающий VBA описание этой функции имеет соответственно вид:

    Public Declare Function SetTimer Lib "user32" (ByVal hwnd As Long, _
    	ByVal nIDEvent As Long, ByVal uElapse As Long, ByVal lpTimerFunc As Long) As Long

    Функция обратного вызова TimerProc

    Функция TimerProc является Callback функцией, определенной приложением и вызываемой при обработке сообщений, поступающих от таймера. Ее определение имеет вид:

    VOID CALLBACK TimerProc(
    	HWND hwnd,	// handle of window for timer messages
    	UINT uMsg,	// WM_TIMER message
    	UINT idEvent,	// timer identifier
    	DWORD dwTime	// current system time
    );

    Ее параметры:

  • hwnd - Описатель окна.
  • uMsg - Указывает WM_Timer сообщение.
  • idEvent - Идентификатор таймера.
  • dwTime - Задает текущее системное время, возвращаемое функцией GetTickCount.
  • Заметьте, имя TimerProc является лишь держателем места. В конкретной ситуации необходимо будет определить одну или несколько Callback функций с подходящими именами. Поскольку вызов каждой из этих функций производится автоматически, то нет необходимости заботиться о корректной передаче аргументов в момент вызова. Необходимо лишь позаботиться о корректной трансляции приведенного определения, взятого из справочной системы Platform SDK, к виду, воспринимаемому в программах на VBA. Вот как выглядит возможное определение:

    Public Sub TimerProc(ByVal HandleW As Long, ByVal msg As Long, _
    	ByVal idEvent As Long, ByVal TimeSys As Long)

    Обратите внимание, мы транслировали функцию в процедуру, поскольку Callback функция TimerProc не возвращает значения. Все типы данных преобразованы в тип Long, в том числе UINT и DWORD. В данной ситуации нет причин для беспокойства о возможной некорректности передаваемых значений, поскольку их передачу обеспечивает сама система.

    Функция KillTimer

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

    BOOL KillTimer(
    	HWND hWnd,	// handle to window that installed timer
    	UINT nIDEvent,	// timer identifier
    );

    Ее параметры:

  • hwnd - Описатель окна, ассоциированного с таймером, совпадающий по значению с соответствующим параметром функции SetTimer. Напомним, в VBA программах таймер не связывается с окном и значение этого параметр задается как NULL.
  • nIDEvent - Задает идентификатор таймера, который должен быть удален. В нашем случае, когда первый параметр равен NULL, его значение задается идентификатором, возвращенным в качестве результата по окончании работы функции SetTimer.
  • Если функция успешно завершает свою работу и удаляет таймер, то в качестве результата она возвращает ненулевое значение. В случае неуспеха возвращается значение 0.

    Пример создания, работы и удаления таймера

    В свое время в книге по языку Visual C++ , демонстрируя работу с таймером и соответствующими функциями Win32 API, мы разработали проект " Жизнь ". В этом проекте моделируется известная компьютерная игра, где можно задать начальную конфигурацию "жизни". Затем эта конфигурация начинает жить, изменяя свое состояние по заданным правилам. Изменение состояния происходит в качестве ответной реакции на сообщения таймера. Другим подобным примером, по существу вариацией на эту же тему, является создание экранных заставок. Сейчас мы решили обойтись более простым примером, демонстрирующим суть проблемы, но не имеющим эффектной формы. В нашем тестовом примере есть две командные кнопки Start и Finish. В ответ на нажатие первой кнопки создается таймер, соответствующая ему Callback функция ведет подсчет числа ее вызов и уведомляет об этом, печатая значение счетчика в окне отладки. При нажатии кнопки Finish таймер удаляется. Кнопки можно нажимать многократно. Все процедуры обработки помещены в модуль Таймер. Вот его текст:

    'Функции работы с таймером
    Public Declare Function SetTimer Lib "user32" (ByVal hwnd As Long, _
    		ByVal nIDEvent As Long, ByVal uElapse As Long, ByVal lpTimerFunc As Long) As Long
    
    Public Declare Function KillTimer Lib "user32" (ByVal hwnd As Long, ByVal nIDEvent As Long) As Long
    
    'Глобальная информация
    Public Counter As Long	'Счетчик числа вызовов Callback функции
    Public IdEv As Long
    
    Public Sub HowManyProc(ByVal HandleW As Long, ByVal msg As Long, _
    			ByVal idEvent As Long, ByVal TimeSys As Long)
    		'Функция обратного вызова. Вызывается при обработке сообщения WM_Timer,
    		'посылаемого таймером, созданным процедурой SetTimer
    		
    		Counter = Counter + 1
    		Debug.Print "Hi", Counter
    		
    	End Sub
    
    Public Sub Start()
    		
    		'Создает таймер, вызывая Win32 Api функцию SetTimer
    		Counter = 0
    		IdEv = SetTimer(0, 0, 10000, AddressOf HowManyProc)
    		If IdEv = 0 Then
    			MsgBox ("Не удалось создать таймер!")
    		Else
    			Debug.Print "Создан Таймер: Идентификатор = ", IdEv
    		End If
     
    End Sub
    
    Public Sub Finish()
    		'Удаляет таймер
    		If IdEv > 0 Then
    			Call KillTimer(0, IdEv)
    			Debug.Print "Удален Таймер: Идентификатор = ", IdEv
    			IdEv = 0
    		End If
    		
    End Sub

    Комментариев, приведенных в тексте, полагаем достаточно для понимания всех деталей. Приведем еще результаты печати, периодически появляющиеся в окне отладки. Следует только сказать, что дважды были поочередно нажаты кнопки Start и Finish:

    Создан Таймер: Идентификатор =32578 
    Hi					1 
    Hi					2 
    Hi					3 
    Удален Таймер: Идентификатор =32578 
    Создан Таймер: Идентификатор =32573 
    Hi					1 
    Hi					2 
    Hi					3 
    Hi					4 
    Удален Таймер: Идентификатор =32573

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

    Классы как обертка вызовов функций Win32 API

    В предыдущих лекциях мы много говорили о классах и, в частности, рассматривали возможность применения класса в качестве упаковки тех или иных стандартных средств. Такой прием позволяет инкапсулировать в классе стандартные средства, - функции API, элементы управления, - скрыть многие детали работы с ними, а, зачастую, и предоставить конечному пользователю дополнительный сервис. Этот полезный прием целесообразно применять всякий раз, когда предполагается работа с функциями Win32 API. Все эти вопросы объявления функций, трансляции из одной формы описания в другую, описание многих параметров, не используемых по существу задачи, следует скрыть в описании класса, организовав интерфейс класса, более разумный с позиций конечного пользователя.

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

    Построение класса "ВашТаймер"

    Поскольку мы уже хорошо знакомы с тем, как строятся классы и как работать с функциями Win32 API, то нам осталось рассказать, как объединить эти две вещи в единое целое. Никаких особых деталей здесь нет. Общая стратегия такова:

  • Необходимо спроектировать интерфейс класса, ориентированный на конечного пользователя. Открытые свойства и методы класса должны позволять пользователю решать все задачи, которые можно решать с помощью скрытых в классе стандартных средств.
  • Встроить в класс стандартные средства, сделав их скрытыми, недоступными для внешнего использования.
  • Реализовать интерфейс класса, используя встроенные средства, возможно расширив их возможности.
  • Применим эту общую схему для создания класса ВашТаймер. Начнем с проектирования его интерфейса. Естественно, целью класса является предоставление пользователю возможности создавать таймер, посылающий сообщения с заданным интервалом, и удалять его, когда необходимость в нем исчезнет. Пользователь не должен ничего знать о функциях Win32 API, об операторах Declare, преобразовании типов. Кажется естественным с этих позиций в интерфейс класса включить два метода: " СоздатьТаймер " и " УдалитьТаймер " и свойство " ИнтервалТаймера ", доступное для чтения и записи. Методы не имеют параметров, что облегчает работу с ними. Чтобы созданный таймер посылал сообщения с заданным интервалом, необходимо предварительно установить подходящее значение свойства, но можно этого и не делать, - в этом случае будет использоваться значение по умолчанию.

    Рассматриваемый нами случай упаковки функций Win32 API особый, поскольку одна из этих функций требует вызова Callback функции. Заметьте, функции обратного вызова не должны принадлежать упаковке, - нашему классу. Они не являются стандартными средствами, это функции, создаваемые пользователем. Поэтому они должны находиться вне модуля класса, - в стандартном классе, созданном пользователем, там, где он будет создавать и объекты класса ВашТаймер. Чтобы не возникала соблазна поместить в класс функцию обратного вызова, такая возможность исключается синтаксически. Обратите, однако, внимание, что созданный класс предъявляет определенные требования к заголовку функции обратного вызова и даже диктует ее имя. Но обо всем по порядку и давайте вначале рассмотрим описание класса ВашТаймер:

    Option Explicit
    'Класс ВашТаймер служит упаковкой функций WIN32 API работы с таймером
    'Интерфейс класса будут составлять две функции:
    'СоздатьТаймер, УдалитьТаймер и свойство ИнтервалТаймера
    
    'При работе с классом необходимо описать Callback функцию по следующему образцу:
    
    'Public Sub TimerProc(ByVal HandleW As Long, ByVal msg As Long, _
    '			ByVal idEvent As Long, ByVal TimeSys As Long)
    '		'Функция обратного вызова. Вызывается при обработке сообщения WM_Timer,
    '		'посылаемого таймером, созданным процедурой SetTimer
    '
    '	'Поместите здесь свой код!
    '
    'End Sub
    
    'Функции Win32 API для работы с таймером
    Private Declare Function SetTimer Lib "user32" (ByVal hwnd As Long, _
    		ByVal nIDEvent As Long, ByVal uElapse As Long, ByVal lpTimerFunc As Long) As Long
    
    Private Declare Function KillTimer Lib "user32" (ByVal hwnd As Long, ByVal nIDEvent As Long) As Long
    
    'Свойства: Интервал - хранит значение интервала посылки сообщений
    Private Интервал As Long
    'Идентификатор таймера
    Private IdEv As Long
    
    
    Public Sub СоздатьТаймер()
     'Создает таймер, вызывая Win32 Api функцию SetTimer
    		IdEv = SetTimer(0, 0, Интервал, AddressOf TimerProc)
    		If IdEv = 0 Then
    			MsgBox ("Не удалось создать таймер!")
    		Else
    			Debug.Print "Создан Таймер: Идентификатор = ", IdEv
    		End If
     
    End Sub
    
    Public Sub УдалитьТаймер()
    'Удаляет таймер
    		If IdEv > 0 Then
    			Call KillTimer(0, IdEv)
    			Debug.Print "Удален Таймер: Идентификатор = ", IdEv
    			IdEv = 0
    		End If
    End Sub
    
    Public Property Get ИнтервалТаймера() As Long
    		ИнтервалТаймера = Интервал
    End Property
    
    Public Property Let ИнтервалТаймера(ByVal NewValue As Long)
    		Интервал = NewValue
    End Property
    
    Private Sub Class_Initialize()
    		Интервал = 1000
    End Sub
    
    Private Sub Class_Terminate()
    		УдалитьТаймер
    End Sub

    Некоторые комментарии к этому тексту:

  • О проектировании интерфейса класса мы уже говорили. Его составляют два метода, не имеющие параметров, - СоздатьТаймер и УдалитьТаймер, а также процедуры - свойства Property Get и Property Let ИнтервалТаймера, позволяющие взаимодействовать с закрытым свойством Интервал.
  • Закрытых свойств и методов больше. Закрытыми являются операторы Declare, описывающие функции Win32 API SetTimer и KillTimer, уже упомянутое свойство Интервал и свойство IdEv, хранящее идентификатор таймера, о котором конечный пользователь может и не знать.
  • Закрытыми являются конструктор класса по умолчанию и деструктор: Class_Initialize и Class_Terminate. В конструкторе инициализируется свойство Таймер, значение которого устанавливается по умолчанию, равным одной секунде. В деструкторе класса таймер уничтожается, если он не был удален до этого.
  • При создании таймера устанавливается ссылка на функцию обратного вызова с именем TimerProc. Процедура с таким именем и уже упоминавшимися требованиями к ее заголовку должна быть описана в стандартном модуле класса, созданным конечным пользователем.
  • В описание класса в качестве комментария вставлена заготовка функции обратного вызова, чтобы облегчить создание этой функции конечному пользователю.
  • Использование класса ВашТаймер

    Рассмотрим, как работать с классом ВашТаймер. Мы не стали изобретать ничего нового, - в тестовый документ добавили две кнопки Start1 и Finish1, которые работают также как и их тезки Start и Finish, но вызывают для этого методы и свойства объекта MyTimer класса ВашТаймер. В модуле, где объявлен соответствующий объект, находится и процедура обратного вызова TimerProc. Вот соответствующий текст этого модуля:

    Option Explicit
    'Модуль Таймер1
    'Глобальная информация
    Public Counter As Long	'Счетчик числа вызовов Callback функции
    Public MyTimer As New ВашТаймер
    
    Public Sub Start1()
    		MyTimer.ИнтервалТаймера = 5000
    		MyTimer.СоздатьТаймер
    End Sub
    
    Public Sub Finish1()
    		MyTimer.УдалитьТаймер
    End Sub
    
    Public Sub TimerProc(ByVal HandleW As Long, ByVal msg As Long, _
    			ByVal idEvent As Long, ByVal TimeSys As Long)
    		'Функция обратного вызова. Вызывается при обработке сообщения WM_Timer,
    		'посылаемого таймером, созданным процедурой SetTimer
    		
    		Counter = Counter + 1
    		Debug.Print "Hi", Counter
    
    End Sub

    Комментируя этот текст, следует заметить, что введение обертывающего класса облегчает работу с таймером. Единственной проблемой остается достаточно сложное и возможно непонятное конечному пользователю описание заголовка Callback функции TimerProc. Чтобы облегчить ее решение, можно, как это сделано в нашем примере, заготовку этой функции включить в описание класса в качестве комментария.

    В заключение приведем результаты эксперимента с нажатием кнопок Start1 и Finish1:

    Создан Таймер: Идентификатор =31711 
    Hi					1 
    Hi					2 
    Hi					3 
    Удален Таймер: Идентификатор =31711 
    Создан Таймер: Идентификатор =31704 
    Hi					4 
    Hi					5 
    Hi					6 
    Hi					7 
    Удален Таймер: Идентификатор =31704

    На этом мы закончим рассмотрение темы работы с функциями Win32 API.

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